mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-09 04:25:57 +00:00
Compare commits
20 Commits
users/lang
...
explorer-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f8c36bbf0 | ||
|
|
661fb66f7b | ||
|
|
2eabc377b0 | ||
|
|
1aa3fb8e7b | ||
|
|
57aef782d8 | ||
|
|
ea39c1d092 | ||
|
|
c21f42159f | ||
|
|
31e4b49f11 | ||
|
|
40491ec9c5 | ||
|
|
e133df18dd | ||
|
|
0532ed26a2 | ||
|
|
fd60c9c15e | ||
|
|
04ab1f3918 | ||
|
|
b784ac0f86 | ||
|
|
28899f63d7 | ||
|
|
9cbf632577 | ||
|
|
17fd2185dc | ||
|
|
a93c8509cd | ||
|
|
5c93c11bd9 | ||
|
|
85d2378d3a |
@@ -3,7 +3,11 @@ PORTAL_RUNNER_PASSWORD=
|
|||||||
PORTAL_RUNNER_SUBSCRIPTION=
|
PORTAL_RUNNER_SUBSCRIPTION=
|
||||||
PORTAL_RUNNER_RESOURCE_GROUP=
|
PORTAL_RUNNER_RESOURCE_GROUP=
|
||||||
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
||||||
|
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY=
|
||||||
PORTAL_RUNNER_CONNECTION_STRING=
|
PORTAL_RUNNER_CONNECTION_STRING=
|
||||||
|
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
|
||||||
|
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
|
||||||
|
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
|
||||||
CASSANDRA_CONNECTION_STRING=
|
CASSANDRA_CONNECTION_STRING=
|
||||||
MONGO_CONNECTION_STRING=
|
MONGO_CONNECTION_STRING=
|
||||||
TABLES_CONNECTION_STRING=
|
TABLES_CONNECTION_STRING=
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ src/Common/DataAccessUtilityBase.ts
|
|||||||
src/Common/DeleteFeedback.ts
|
src/Common/DeleteFeedback.ts
|
||||||
src/Common/DocumentClientUtilityBase.ts
|
src/Common/DocumentClientUtilityBase.ts
|
||||||
src/Common/EditableUtility.ts
|
src/Common/EditableUtility.ts
|
||||||
src/Common/EnvironmentUtility.ts
|
|
||||||
src/Common/HashMap.test.ts
|
src/Common/HashMap.test.ts
|
||||||
src/Common/HashMap.ts
|
src/Common/HashMap.ts
|
||||||
src/Common/HeadersUtility.test.ts
|
src/Common/HeadersUtility.test.ts
|
||||||
@@ -202,8 +201,6 @@ src/Explorer/Tabs/QueryTab.test.ts
|
|||||||
src/Explorer/Tabs/QueryTab.ts
|
src/Explorer/Tabs/QueryTab.ts
|
||||||
src/Explorer/Tabs/QueryTablesTab.ts
|
src/Explorer/Tabs/QueryTablesTab.ts
|
||||||
src/Explorer/Tabs/ScriptTabBase.ts
|
src/Explorer/Tabs/ScriptTabBase.ts
|
||||||
src/Explorer/Tabs/SettingsTab.test.ts
|
|
||||||
src/Explorer/Tabs/SettingsTab.ts
|
|
||||||
src/Explorer/Tabs/SparkMasterTab.ts
|
src/Explorer/Tabs/SparkMasterTab.ts
|
||||||
src/Explorer/Tabs/StoredProcedureTab.ts
|
src/Explorer/Tabs/StoredProcedureTab.ts
|
||||||
src/Explorer/Tabs/TabComponents.ts
|
src/Explorer/Tabs/TabComponents.ts
|
||||||
@@ -290,8 +287,6 @@ src/Utils/DatabaseAccountUtils.ts
|
|||||||
src/Utils/JunoUtils.ts
|
src/Utils/JunoUtils.ts
|
||||||
src/Utils/MessageValidation.ts
|
src/Utils/MessageValidation.ts
|
||||||
src/Utils/NotebookConfigurationUtils.ts
|
src/Utils/NotebookConfigurationUtils.ts
|
||||||
src/Utils/OfferUtils.test.ts
|
|
||||||
src/Utils/OfferUtils.ts
|
|
||||||
src/Utils/PricingUtils.test.ts
|
src/Utils/PricingUtils.test.ts
|
||||||
src/Utils/QueryUtils.test.ts
|
src/Utils/QueryUtils.test.ts
|
||||||
src/Utils/QueryUtils.ts
|
src/Utils/QueryUtils.ts
|
||||||
|
|||||||
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -146,6 +146,13 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
|
PORTAL_RUNNER_SUBSCRIPTION: ${{ secrets.PORTAL_RUNNER_SUBSCRIPTION }}
|
||||||
|
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
|
||||||
|
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
|
||||||
|
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }}
|
||||||
|
NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }}
|
||||||
|
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
|
||||||
|
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
||||||
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
||||||
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
||||||
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
|
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
|
||||||
|
|||||||
25
.github/workflows/runners.yml
vendored
25
.github/workflows/runners.yml
vendored
@@ -1,25 +0,0 @@
|
|||||||
name: Runners
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 * 1 * *"
|
|
||||||
jobs:
|
|
||||||
sqlcreatecollection:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: "SQL | Create Collection"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v1
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run test:e2e
|
|
||||||
env:
|
|
||||||
PORTAL_RUNNER_APP_INSIGHTS_KEY: ${{ secrets.PORTAL_RUNNER_APP_INSIGHTS_KEY }}
|
|
||||||
PORTAL_RUNNER_USERNAME: ${{ secrets.PORTAL_RUNNER_USERNAME }}
|
|
||||||
PORTAL_RUNNER_PASSWORD: ${{ secrets.PORTAL_RUNNER_PASSWORD }}
|
|
||||||
PORTAL_RUNNER_SUBSCRIPTION: 69e02f2d-f059-4409-9eac-97e8a276ae2c
|
|
||||||
PORTAL_RUNNER_RESOURCE_GROUP: runners
|
|
||||||
PORTAL_RUNNER_DATABASE_ACCOUNT: portal-sql-runner
|
|
||||||
- uses: actions/upload-artifact@v2
|
|
||||||
if: failure()
|
|
||||||
with:
|
|
||||||
name: screenshots
|
|
||||||
path: failure.png
|
|
||||||
37
README.md
37
README.md
@@ -13,29 +13,18 @@ UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), ht
|
|||||||
|
|
||||||
### Watch mode
|
### Watch mode
|
||||||
|
|
||||||
Run `npm run watch` to start the development server and automatically rebuild on changes
|
Run `npm start` to start the development server and automatically rebuild on changes
|
||||||
|
|
||||||
### Specifying Development Platform
|
### Hosted Development (https://cosmos.azure.com)
|
||||||
|
|
||||||
Setting the environment variable `PLATFORM` during the build process will force the explorer to load the specified platform. By default in development it will run in `Hosted` mode. Valid options:
|
- Visit: `https://localhost:1234/hostedExplorer.html`
|
||||||
|
- Local sign in via AAD will NOT work. Connection string only in dev mode. Use the Portal if you need AAD auth.
|
||||||
- Hosted
|
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
|
||||||
- Emulator
|
|
||||||
- Portal
|
|
||||||
|
|
||||||
`PLATFORM=Emulator npm run watch`
|
|
||||||
|
|
||||||
### Hosted Development
|
|
||||||
|
|
||||||
The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
|
|
||||||
|
|
||||||
To run pure hosted mode, in `webpack.config.js` change index HtmlWebpackPlugin to use hostedExplorer.html and change entry for index to use HostedExplorer.ts.
|
|
||||||
|
|
||||||
### Emulator Development
|
### Emulator Development
|
||||||
|
|
||||||
In a window environment, running `npm run build` will automatically copy the built files from `/dist` over to the default emulator install paths. In a non-windows environment you can specify an alternate endpoint using `EMULATOR_ENDPOINT` and webpack dev server will proxy requests for you.
|
- Start the Cosmos Emulator
|
||||||
|
- Visit: https://localhost:1234/index.html
|
||||||
`PLATFORM=Emulator EMULATOR_ENDPOINT=https://my-vm.azure.com:8081 npm run watch`
|
|
||||||
|
|
||||||
#### Setting up a Remote Emulator
|
#### Setting up a Remote Emulator
|
||||||
|
|
||||||
@@ -55,16 +44,8 @@ The Cosmos emulator currently only runs in Windows environments. You can still d
|
|||||||
|
|
||||||
### Portal Development
|
### Portal Development
|
||||||
|
|
||||||
The Cosmos Portal that consumes this repo is not currently open source. If you have access to this project, `npm run build` will copy the built files over to the portal where they will be loaded by the portal development environment
|
- Visit: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html
|
||||||
|
- You may have to manually visit https://localhost:1234/explorer.html first and click through any SSL certificate warnings
|
||||||
You can however load a local running instance of data explorer in the production portal.
|
|
||||||
|
|
||||||
1. Turn off browser SSL validation for localhost: chrome://flags/#allow-insecure-localhost OR Install valid SSL certs for localhost (on IE, follow these [instructions](https://www.technipages.com/ie-bypass-problem-with-this-websites-security-certificate) to install the localhost certificate in the right place)
|
|
||||||
2. Allowlist `https://localhost:1234` domain for CORS in the Azure Cosmos DB portal
|
|
||||||
3. Start the project in portal mode: `PLATFORM=Portal npm run watch`
|
|
||||||
4. Load the portal using the following link: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html
|
|
||||||
|
|
||||||
Live reload will occur, but data explorer will not properly integrate again with the parent iframe. You will have to manually reload the page.
|
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
|
|||||||
3660
package-lock.json
generated
3660
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,10 @@
|
|||||||
"description": "Cosmos Explorer",
|
"description": "Cosmos Explorer",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@azure/arm-cosmosdb": "9.1.0",
|
||||||
"@azure/cosmos": "3.9.0",
|
"@azure/cosmos": "3.9.0",
|
||||||
"@azure/cosmos-language-service": "0.0.4",
|
"@azure/identity": "1.1.0",
|
||||||
|
"@azure/cosmos-language-service": "0.0.5",
|
||||||
"@jupyterlab/services": "6.0.0-rc.2",
|
"@jupyterlab/services": "6.0.0-rc.2",
|
||||||
"@jupyterlab/terminal": "3.0.0-rc.2",
|
"@jupyterlab/terminal": "3.0.0-rc.2",
|
||||||
"@microsoft/applicationinsights-web": "2.5.9",
|
"@microsoft/applicationinsights-web": "2.5.9",
|
||||||
@@ -66,7 +68,7 @@
|
|||||||
"jquery-ui-dist": "1.12.1",
|
"jquery-ui-dist": "1.12.1",
|
||||||
"knockout": "3.5.1",
|
"knockout": "3.5.1",
|
||||||
"mkdirp": "1.0.4",
|
"mkdirp": "1.0.4",
|
||||||
"monaco-editor": "0.15.6",
|
"monaco-editor": "0.18.1",
|
||||||
"object.entries": "1.1.0",
|
"object.entries": "1.1.0",
|
||||||
"office-ui-fabric-react": "7.134.1",
|
"office-ui-fabric-react": "7.134.1",
|
||||||
"p-retry": "4.2.0",
|
"p-retry": "4.2.0",
|
||||||
@@ -115,7 +117,7 @@
|
|||||||
"@types/prop-types": "15.5.8",
|
"@types/prop-types": "15.5.8",
|
||||||
"@types/puppeteer": "3.0.1",
|
"@types/puppeteer": "3.0.1",
|
||||||
"@types/q": "1.5.1",
|
"@types/q": "1.5.1",
|
||||||
"@types/react": "16.9.49",
|
"@types/react": "16.9.56",
|
||||||
"@types/react-dom": "16.0.7",
|
"@types/react-dom": "16.0.7",
|
||||||
"@types/react-notification-system": "0.2.39",
|
"@types/react-notification-system": "0.2.39",
|
||||||
"@types/react-redux": "7.1.7",
|
"@types/react-redux": "7.1.7",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
"offerThroughput": 400,
|
"offerThroughput": 400,
|
||||||
"databaseLevelThroughput": false,
|
"databaseLevelThroughput": false,
|
||||||
"collectionId": "Persons",
|
"collectionId": "Persons",
|
||||||
"rupmEnabled": false,
|
|
||||||
"partitionKey": { "kind": "Hash", "paths": ["/name"] },
|
"partitionKey": { "kind": "Hash", "paths": ["/name"] },
|
||||||
"data": [
|
"data": [
|
||||||
"g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)",
|
"g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)",
|
||||||
@@ -13,4 +12,4 @@
|
|||||||
"g.V('1').addE('knows').to(g.V('2')).outV().addE('knows').to(g.V('3'))",
|
"g.V('1').addE('knows').to(g.V('2')).outV().addE('knows').to(g.V('3'))",
|
||||||
"g.V('3').addE('knows').to(g.V('4'))"
|
"g.V('3').addE('knows').to(g.V('4'))"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,13 +108,11 @@ export class CapabilityNames {
|
|||||||
export class Features {
|
export class Features {
|
||||||
public static readonly cosmosdb = "cosmosdb";
|
public static readonly cosmosdb = "cosmosdb";
|
||||||
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
|
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
|
||||||
public static readonly enableRupm = "enablerupm";
|
|
||||||
public static readonly executeSproc = "dataexplorerexecutesproc";
|
public static readonly executeSproc = "dataexplorerexecutesproc";
|
||||||
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
||||||
public static readonly enableTtl = "enablettl";
|
public static readonly enableTtl = "enablettl";
|
||||||
public static readonly enableNotebooks = "enablenotebooks";
|
public static readonly enableNotebooks = "enablenotebooks";
|
||||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||||
public static readonly enableCodeOfConduct = "enablecodeofconduct";
|
|
||||||
public static readonly enableLinkInjection = "enablelinkinjection";
|
public static readonly enableLinkInjection = "enablelinkinjection";
|
||||||
public static readonly enableSpark = "enablespark";
|
public static readonly enableSpark = "enablespark";
|
||||||
public static readonly livyEndpoint = "livyendpoint";
|
public static readonly livyEndpoint = "livyendpoint";
|
||||||
@@ -181,11 +179,6 @@ export class CassandraBackend {
|
|||||||
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RUPMStates {
|
|
||||||
public static on: string = "on";
|
|
||||||
public static off: string = "off";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Queries {
|
export class Queries {
|
||||||
public static CustomPageOption: string = "custom";
|
public static CustomPageOption: string = "custom";
|
||||||
public static UnlimitedPageOption: string = "unlimited";
|
public static UnlimitedPageOption: string = "unlimited";
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
import {
|
import { ConflictDefinition, FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
ConflictDefinition,
|
|
||||||
FeedOptions,
|
|
||||||
ItemDefinition,
|
|
||||||
OfferDefinition,
|
|
||||||
QueryIterator,
|
|
||||||
Resource
|
|
||||||
} from "@azure/cosmos";
|
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import Q from "q";
|
import Q from "q";
|
||||||
import { configContext, Platform } from "../ConfigContext";
|
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||||
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
||||||
import { OfferUtils } from "../Utils/OfferUtils";
|
|
||||||
import * as Constants from "./Constants";
|
import * as Constants from "./Constants";
|
||||||
import { client } from "./CosmosClient";
|
import { client } from "./CosmosClient";
|
||||||
import * as HeadersUtility from "./HeadersUtility";
|
|
||||||
import { sendCachedDataMessage } from "./MessageHandler";
|
|
||||||
|
|
||||||
export function getCommonQueryOptions(options: FeedOptions): any {
|
export function getCommonQueryOptions(options: FeedOptions): any {
|
||||||
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
|
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
export default class EnvironmentUtility {
|
export function normalizeArmEndpoint(uri: string): string {
|
||||||
public static normalizeArmEndpointUri(uri: string): string {
|
if (uri && uri.slice(-1) !== "/") {
|
||||||
if (uri && uri.slice(-1) !== "/") {
|
return `${uri}/`;
|
||||||
return `${uri}/`;
|
|
||||||
}
|
|
||||||
return uri;
|
|
||||||
}
|
}
|
||||||
|
return uri;
|
||||||
}
|
}
|
||||||
|
|||||||
64
src/Common/OfferUtility.test.ts
Normal file
64
src/Common/OfferUtility.test.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import * as OfferUtility from "./OfferUtility";
|
||||||
|
import { SDKOfferDefinition, Offer } from "../Contracts/DataModels";
|
||||||
|
import { OfferResponse } from "@azure/cosmos";
|
||||||
|
|
||||||
|
describe("parseSDKOfferResponse", () => {
|
||||||
|
it("manual throughput", () => {
|
||||||
|
const mockOfferDefinition = {
|
||||||
|
content: {
|
||||||
|
offerThroughput: 500,
|
||||||
|
collectionThroughputInfo: {
|
||||||
|
minimumRUForCollection: 400,
|
||||||
|
numPhysicalPartitions: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
id: "test"
|
||||||
|
} as SDKOfferDefinition;
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
resource: mockOfferDefinition
|
||||||
|
} as OfferResponse;
|
||||||
|
|
||||||
|
const expectedResult: Offer = {
|
||||||
|
manualThroughput: 500,
|
||||||
|
autoscaleMaxThroughput: undefined,
|
||||||
|
minimumThroughput: 400,
|
||||||
|
id: "test",
|
||||||
|
offerDefinition: mockOfferDefinition,
|
||||||
|
offerReplacePending: false
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("autoscale throughput", () => {
|
||||||
|
const mockOfferDefinition = {
|
||||||
|
content: {
|
||||||
|
offerThroughput: 400,
|
||||||
|
collectionThroughputInfo: {
|
||||||
|
minimumRUForCollection: 400,
|
||||||
|
numPhysicalPartitions: 1
|
||||||
|
},
|
||||||
|
offerAutopilotSettings: {
|
||||||
|
maxThroughput: 5000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
id: "test"
|
||||||
|
} as SDKOfferDefinition;
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
resource: mockOfferDefinition
|
||||||
|
} as OfferResponse;
|
||||||
|
|
||||||
|
const expectedResult: Offer = {
|
||||||
|
manualThroughput: undefined,
|
||||||
|
autoscaleMaxThroughput: 5000,
|
||||||
|
minimumThroughput: 400,
|
||||||
|
id: "test",
|
||||||
|
offerDefinition: mockOfferDefinition,
|
||||||
|
offerReplacePending: false
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
34
src/Common/OfferUtility.ts
Normal file
34
src/Common/OfferUtility.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
|
||||||
|
import { OfferResponse } from "@azure/cosmos";
|
||||||
|
import { HttpHeaders } from "./Constants";
|
||||||
|
|
||||||
|
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
|
||||||
|
const offerDefinition: SDKOfferDefinition = offerResponse?.resource;
|
||||||
|
const offerContent = offerDefinition.content;
|
||||||
|
if (!offerContent) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection;
|
||||||
|
const autopilotSettings = offerContent.offerAutopilotSettings;
|
||||||
|
|
||||||
|
if (autopilotSettings) {
|
||||||
|
return {
|
||||||
|
id: offerDefinition.id,
|
||||||
|
autoscaleMaxThroughput: autopilotSettings.maxThroughput,
|
||||||
|
manualThroughput: undefined,
|
||||||
|
minimumThroughput,
|
||||||
|
offerDefinition,
|
||||||
|
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: offerDefinition.id,
|
||||||
|
autoscaleMaxThroughput: undefined,
|
||||||
|
manualThroughput: offerContent.offerThroughput,
|
||||||
|
minimumThroughput,
|
||||||
|
offerDefinition,
|
||||||
|
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true"
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -16,7 +16,7 @@ const notificationsPath = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
|
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
|
||||||
if (configContext.platform === Platform.Emulator) {
|
if (configContext.platform === Platform.Emulator || configContext.platform === Platform.Hosted) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
92
src/Common/dataAccess/getCollectionDataUsageSize.ts
Normal file
92
src/Common/dataAccess/getCollectionDataUsageSize.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { armRequest } from "../../Utils/arm/request";
|
||||||
|
import { configContext } from "../../ConfigContext";
|
||||||
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
|
interface TimeSeriesData {
|
||||||
|
data: {
|
||||||
|
timeStamp: string;
|
||||||
|
total: number;
|
||||||
|
}[];
|
||||||
|
metadatavalues: {
|
||||||
|
name: {
|
||||||
|
localizedValue: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MetricsData {
|
||||||
|
displayDescription: string;
|
||||||
|
errorCode: string;
|
||||||
|
id: string;
|
||||||
|
name: {
|
||||||
|
value: string;
|
||||||
|
localizedValue: string;
|
||||||
|
};
|
||||||
|
timeseries: TimeSeriesData[];
|
||||||
|
type: string;
|
||||||
|
unit: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MetricsResponse {
|
||||||
|
cost: number;
|
||||||
|
interval: string;
|
||||||
|
namespace: string;
|
||||||
|
resourceregion: string;
|
||||||
|
timespan: string;
|
||||||
|
value: MetricsData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
|
||||||
|
if (window.authType !== AuthType.AAD) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptionId = userContext.subscriptionId;
|
||||||
|
const resourceGroup = userContext.resourceGroup;
|
||||||
|
const accountName = userContext.databaseAccount.name;
|
||||||
|
const filter = `DatabaseName eq '${databaseName}' and CollectionName eq '${containerName}'`;
|
||||||
|
const metricNames = "DataUsage,IndexUsage";
|
||||||
|
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/providers/microsoft.insights/metrics`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const metricsResponse: MetricsResponse = await armRequest({
|
||||||
|
host: configContext.ARM_ENDPOINT,
|
||||||
|
path,
|
||||||
|
method: "GET",
|
||||||
|
apiVersion: "2018-01-01",
|
||||||
|
queryParams: {
|
||||||
|
filter,
|
||||||
|
metricNames
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (metricsResponse?.value?.length !== 2) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataUsageData: MetricsData = metricsResponse.value[0];
|
||||||
|
const indexUsagedata: MetricsData = metricsResponse.value[1];
|
||||||
|
const dataUsageSizeInKb: number = getUsageSizeInKb(dataUsageData);
|
||||||
|
const indexUsageSizeInKb: number = getUsageSizeInKb(indexUsagedata);
|
||||||
|
|
||||||
|
return dataUsageSizeInKb + indexUsageSizeInKb;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, "getCollectionUsageSize");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUsageSizeInKb = (metricsData: MetricsData): number => {
|
||||||
|
if (metricsData?.errorCode !== "Success") {
|
||||||
|
throw Error(`Get collection usage size failed: ${metricsData.errorCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeSeriesData: TimeSeriesData = metricsData?.timeseries?.[0];
|
||||||
|
const usageSizeInBytes: number = timeSeriesData?.data?.[0]?.total;
|
||||||
|
|
||||||
|
return usageSizeInBytes ? usageSizeInBytes / 1024 : 0;
|
||||||
|
};
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { HttpHeaders } from "../Constants";
|
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import { client } from "../CosmosClient";
|
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
@@ -11,50 +8,22 @@ import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/20
|
|||||||
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { readOffers } from "./readOffers";
|
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export const readCollectionOffer = async (
|
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
|
||||||
params: DataModels.ReadCollectionOfferParams
|
|
||||||
): Promise<DataModels.OfferWithHeaders> => {
|
|
||||||
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
|
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
|
||||||
let offerId = params.offerId;
|
|
||||||
if (!offerId) {
|
|
||||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
|
||||||
try {
|
|
||||||
offerId = await getCollectionOfferIdWithARM(params.databaseId, params.collectionId);
|
|
||||||
} catch (error) {
|
|
||||||
clearMessage();
|
|
||||||
if (error.code !== "NotFound") {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
offerId = await getCollectionOfferIdWithSDK(params.collectionResourceId);
|
|
||||||
if (!offerId) {
|
|
||||||
clearMessage();
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: RequestOptions = {
|
|
||||||
initialHeaders: {
|
|
||||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client()
|
if (
|
||||||
.offer(offerId)
|
window.authType === AuthType.AAD &&
|
||||||
.read(options);
|
!userContext.useSDKOperations &&
|
||||||
return (
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||||
response && {
|
) {
|
||||||
...response.resource,
|
return await readCollectionOfferWithARM(params.databaseId, params.collectionId);
|
||||||
headers: response.headers
|
}
|
||||||
}
|
|
||||||
);
|
return await readOfferWithSDK(params.offerId, params.collectionResourceId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
|
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -63,61 +32,92 @@ export const readCollectionOffer = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCollectionOfferIdWithARM = async (databaseId: string, collectionId: string): Promise<string> => {
|
const readCollectionOfferWithARM = async (databaseId: string, collectionId: string): Promise<Offer> => {
|
||||||
let rpResponse;
|
|
||||||
const subscriptionId = userContext.subscriptionId;
|
const subscriptionId = userContext.subscriptionId;
|
||||||
const resourceGroup = userContext.resourceGroup;
|
const resourceGroup = userContext.resourceGroup;
|
||||||
const accountName = userContext.databaseAccount.name;
|
const accountName = userContext.databaseAccount.name;
|
||||||
const defaultExperience = userContext.defaultExperience;
|
const defaultExperience = userContext.defaultExperience;
|
||||||
switch (defaultExperience) {
|
|
||||||
case DefaultAccountExperienceType.DocumentDB:
|
let rpResponse;
|
||||||
rpResponse = await getSqlContainerThroughput(
|
try {
|
||||||
subscriptionId,
|
switch (defaultExperience) {
|
||||||
resourceGroup,
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
accountName,
|
rpResponse = await getSqlContainerThroughput(
|
||||||
databaseId,
|
subscriptionId,
|
||||||
collectionId
|
resourceGroup,
|
||||||
);
|
accountName,
|
||||||
break;
|
databaseId,
|
||||||
case DefaultAccountExperienceType.MongoDB:
|
collectionId
|
||||||
rpResponse = await getMongoDBCollectionThroughput(
|
);
|
||||||
subscriptionId,
|
break;
|
||||||
resourceGroup,
|
case DefaultAccountExperienceType.MongoDB:
|
||||||
accountName,
|
rpResponse = await getMongoDBCollectionThroughput(
|
||||||
databaseId,
|
subscriptionId,
|
||||||
collectionId
|
resourceGroup,
|
||||||
);
|
accountName,
|
||||||
break;
|
databaseId,
|
||||||
case DefaultAccountExperienceType.Cassandra:
|
collectionId
|
||||||
rpResponse = await getCassandraTableThroughput(
|
);
|
||||||
subscriptionId,
|
break;
|
||||||
resourceGroup,
|
case DefaultAccountExperienceType.Cassandra:
|
||||||
accountName,
|
rpResponse = await getCassandraTableThroughput(
|
||||||
databaseId,
|
subscriptionId,
|
||||||
collectionId
|
resourceGroup,
|
||||||
);
|
accountName,
|
||||||
break;
|
databaseId,
|
||||||
case DefaultAccountExperienceType.Graph:
|
collectionId
|
||||||
rpResponse = await getGremlinGraphThroughput(
|
);
|
||||||
subscriptionId,
|
break;
|
||||||
resourceGroup,
|
case DefaultAccountExperienceType.Graph:
|
||||||
accountName,
|
rpResponse = await getGremlinGraphThroughput(
|
||||||
databaseId,
|
subscriptionId,
|
||||||
collectionId
|
resourceGroup,
|
||||||
);
|
accountName,
|
||||||
break;
|
databaseId,
|
||||||
case DefaultAccountExperienceType.Table:
|
collectionId
|
||||||
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
case DefaultAccountExperienceType.Table:
|
||||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "NotFound") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return rpResponse?.name;
|
const resource = rpResponse?.properties?.resource;
|
||||||
};
|
if (resource) {
|
||||||
|
const offerId: string = rpResponse.name;
|
||||||
|
const minimumThroughput: number =
|
||||||
|
typeof resource.minimumThroughput === "string"
|
||||||
|
? parseInt(resource.minimumThroughput)
|
||||||
|
: resource.minimumThroughput;
|
||||||
|
const autoscaleSettings = resource.autoscaleSettings;
|
||||||
|
|
||||||
const getCollectionOfferIdWithSDK = async (collectionResourceId: string): Promise<string> => {
|
if (autoscaleSettings) {
|
||||||
const offers = await readOffers();
|
return {
|
||||||
const offer = offers.find(offer => offer.resource === collectionResourceId);
|
id: offerId,
|
||||||
return offer?.id;
|
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
||||||
|
manualThroughput: undefined,
|
||||||
|
minimumThroughput,
|
||||||
|
offerReplacePending: resource.offerReplacePending === "true"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: offerId,
|
||||||
|
autoscaleMaxThroughput: undefined,
|
||||||
|
manualThroughput: resource.throughput,
|
||||||
|
minimumThroughput,
|
||||||
|
offerReplacePending: resource.offerReplacePending === "true"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import * as HeadersUtility from "../HeadersUtility";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
import { ContainerDefinition, Resource } from "@azure/cosmos";
|
|
||||||
import { HttpHeaders } from "../Constants";
|
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import { client } from "../CosmosClient";
|
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
|
|
||||||
interface ResourceWithStatistics {
|
|
||||||
statistics: DataModels.Statistic[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const readCollectionQuotaInfo = async (
|
|
||||||
collection: ViewModels.Collection
|
|
||||||
): Promise<DataModels.CollectionQuotaInfo> => {
|
|
||||||
const clearMessage = logConsoleProgress(`Querying containers for database ${collection.id}`);
|
|
||||||
const options: RequestOptions = {};
|
|
||||||
options.populateQuotaInfo = true;
|
|
||||||
options.initialHeaders = options.initialHeaders || {};
|
|
||||||
options.initialHeaders[HttpHeaders.populatePartitionStatistics] = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await client()
|
|
||||||
.database(collection.databaseId)
|
|
||||||
.container(collection.id())
|
|
||||||
.read(options);
|
|
||||||
const quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers);
|
|
||||||
const resource = response.resource as ContainerDefinition & Resource & ResourceWithStatistics;
|
|
||||||
quota["usageSizeInKB"] = resource.statistics.reduce(
|
|
||||||
(previousValue: number, currentValue: DataModels.Statistic) => previousValue + currentValue.sizeInKB,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
quota["numPartitions"] = resource.statistics.length;
|
|
||||||
quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617)
|
|
||||||
|
|
||||||
return quota;
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, "ReadCollectionQuotaInfo", `Error while querying quota info for container ${collection.id}`);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
clearMessage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,51 +1,28 @@
|
|||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { HttpHeaders } from "../Constants";
|
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import { client } from "../CosmosClient";
|
|
||||||
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||||
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { readOffers } from "./readOffers";
|
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export const readDatabaseOffer = async (
|
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
||||||
params: DataModels.ReadDatabaseOfferParams
|
|
||||||
): Promise<DataModels.OfferWithHeaders> => {
|
|
||||||
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
||||||
let offerId = params.offerId;
|
|
||||||
if (!offerId) {
|
|
||||||
offerId = await (window.authType === AuthType.AAD &&
|
|
||||||
!userContext.useSDKOperations &&
|
|
||||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
|
||||||
? getDatabaseOfferIdWithARM(params.databaseId)
|
|
||||||
: getDatabaseOfferIdWithSDK(params.databaseResourceId));
|
|
||||||
if (!offerId) {
|
|
||||||
clearMessage();
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: RequestOptions = {
|
|
||||||
initialHeaders: {
|
|
||||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client()
|
if (
|
||||||
.offer(offerId)
|
window.authType === AuthType.AAD &&
|
||||||
.read(options);
|
!userContext.useSDKOperations &&
|
||||||
return (
|
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||||
response && {
|
) {
|
||||||
...response.resource,
|
return await readDatabaseOfferWithARM(params.databaseId);
|
||||||
headers: response.headers
|
}
|
||||||
}
|
|
||||||
);
|
return await readOfferWithSDK(params.offerId, params.databaseResourceId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
|
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -54,13 +31,13 @@ export const readDatabaseOffer = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
|
const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||||
let rpResponse;
|
|
||||||
const subscriptionId = userContext.subscriptionId;
|
const subscriptionId = userContext.subscriptionId;
|
||||||
const resourceGroup = userContext.resourceGroup;
|
const resourceGroup = userContext.resourceGroup;
|
||||||
const accountName = userContext.databaseAccount.name;
|
const accountName = userContext.databaseAccount.name;
|
||||||
const defaultExperience = userContext.defaultExperience;
|
const defaultExperience = userContext.defaultExperience;
|
||||||
|
|
||||||
|
let rpResponse;
|
||||||
try {
|
try {
|
||||||
switch (defaultExperience) {
|
switch (defaultExperience) {
|
||||||
case DefaultAccountExperienceType.DocumentDB:
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
@@ -78,18 +55,41 @@ const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> =>
|
|||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return rpResponse?.name;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code !== "NotFound") {
|
if (error.code !== "NotFound") {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {
|
const resource = rpResponse?.properties?.resource;
|
||||||
const offers = await readOffers();
|
if (resource) {
|
||||||
const offer = offers.find(offer => offer.resource === databaseResourceId);
|
const offerId: string = rpResponse.name;
|
||||||
return offer?.id;
|
const minimumThroughput: number =
|
||||||
|
typeof resource.minimumThroughput === "string"
|
||||||
|
? parseInt(resource.minimumThroughput)
|
||||||
|
: resource.minimumThroughput;
|
||||||
|
const autoscaleSettings = resource.autoscaleSettings;
|
||||||
|
|
||||||
|
if (autoscaleSettings) {
|
||||||
|
return {
|
||||||
|
id: offerId,
|
||||||
|
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
||||||
|
manualThroughput: undefined,
|
||||||
|
minimumThroughput,
|
||||||
|
offerReplacePending: resource.offerReplacePending === "true"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: offerId,
|
||||||
|
autoscaleMaxThroughput: undefined,
|
||||||
|
manualThroughput: resource.throughput,
|
||||||
|
minimumThroughput,
|
||||||
|
offerReplacePending: resource.offerReplacePending === "true"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
29
src/Common/dataAccess/readOfferWithSDK.ts
Normal file
29
src/Common/dataAccess/readOfferWithSDK.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { HttpHeaders } from "../Constants";
|
||||||
|
import { Offer } from "../../Contracts/DataModels";
|
||||||
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||||
|
import { readOffers } from "./readOffers";
|
||||||
|
|
||||||
|
export const readOfferWithSDK = async (offerId: string, resourceId: string): Promise<Offer> => {
|
||||||
|
if (!offerId) {
|
||||||
|
const offers = await readOffers();
|
||||||
|
const offer = offers.find(offer => offer.resource === resourceId);
|
||||||
|
|
||||||
|
if (!offer) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
offerId = offer.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: RequestOptions = {
|
||||||
|
initialHeaders: {
|
||||||
|
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const response = await client()
|
||||||
|
.offer(offerId)
|
||||||
|
.read(options);
|
||||||
|
|
||||||
|
return parseSDKOfferResponse(response);
|
||||||
|
};
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Offer } from "../../Contracts/DataModels";
|
import { SDKOfferDefinition } from "../../Contracts/DataModels";
|
||||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
|
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
|
||||||
|
|
||||||
export const readOffers = async (): Promise<Offer[]> => {
|
export const readOffers = async (): Promise<SDKOfferDefinition[]> => {
|
||||||
const clearMessage = logConsoleProgress(`Querying offers`);
|
const clearMessage = logConsoleProgress(`Querying offers`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { HttpHeaders } from "../Constants";
|
import { HttpHeaders } from "../Constants";
|
||||||
import { Offer, UpdateOfferParams } from "../../Contracts/DataModels";
|
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||||
import { OfferDefinition } from "@azure/cosmos";
|
import { OfferDefinition } from "@azure/cosmos";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
|
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
import { handleError } from "../ErrorHandlingUtils";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||||
import { readCollectionOffer } from "./readCollectionOffer";
|
import { readCollectionOffer } from "./readCollectionOffer";
|
||||||
import { readDatabaseOffer } from "./readDatabaseOffer";
|
import { readDatabaseOffer } from "./readDatabaseOffer";
|
||||||
import {
|
import {
|
||||||
@@ -373,21 +374,21 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
|
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
|
||||||
const currentOffer = params.currentOffer;
|
const sdkOfferDefinition = params.currentOffer.offerDefinition;
|
||||||
const newOffer: Offer = {
|
const newOffer: SDKOfferDefinition = {
|
||||||
content: {
|
content: {
|
||||||
offerThroughput: undefined,
|
offerThroughput: undefined,
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
offerIsRUPerMinuteThroughputEnabled: false
|
||||||
},
|
},
|
||||||
_etag: undefined,
|
_etag: undefined,
|
||||||
_ts: undefined,
|
_ts: undefined,
|
||||||
_rid: currentOffer._rid,
|
_rid: sdkOfferDefinition._rid,
|
||||||
_self: currentOffer._self,
|
_self: sdkOfferDefinition._self,
|
||||||
id: currentOffer.id,
|
id: sdkOfferDefinition.id,
|
||||||
offerResourceId: currentOffer.offerResourceId,
|
offerResourceId: sdkOfferDefinition.offerResourceId,
|
||||||
offerVersion: currentOffer.offerVersion,
|
offerVersion: sdkOfferDefinition.offerVersion,
|
||||||
offerType: currentOffer.offerType,
|
offerType: sdkOfferDefinition.offerType,
|
||||||
resource: currentOffer.resource
|
resource: sdkOfferDefinition.resource
|
||||||
};
|
};
|
||||||
|
|
||||||
if (params.autopilotThroughput) {
|
if (params.autopilotThroughput) {
|
||||||
@@ -415,5 +416,6 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> =>
|
|||||||
.offer(params.currentOffer.id)
|
.offer(params.currentOffer.id)
|
||||||
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
|
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
|
||||||
.replace((newOffer as unknown) as OfferDefinition, options);
|
.replace((newOffer as unknown) as OfferDefinition, options);
|
||||||
return sdkResponse?.resource;
|
|
||||||
|
return parseSDKOfferResponse(sdkResponse);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { updateOfferThroughputBeyondLimit } from "./updateOfferThroughputBeyondLimit";
|
|
||||||
|
|
||||||
describe("updateOfferThroughputBeyondLimit", () => {
|
|
||||||
it("should call fetch", async () => {
|
|
||||||
window.fetch = jest.fn(() => {
|
|
||||||
return {
|
|
||||||
ok: true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
window.dataExplorer = {
|
|
||||||
logConsoleData: jest.fn(),
|
|
||||||
deleteInProgressConsoleDataWithId: jest.fn()
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} as any;
|
|
||||||
await updateOfferThroughputBeyondLimit({
|
|
||||||
subscriptionId: "foo",
|
|
||||||
resourceGroup: "foo",
|
|
||||||
databaseAccountName: "foo",
|
|
||||||
databaseName: "foo",
|
|
||||||
throughput: 1000000000,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
|
||||||
});
|
|
||||||
expect(window.fetch).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { Platform, configContext } from "../../ConfigContext";
|
|
||||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
|
||||||
import { AutoPilotOfferSettings } from "../../Contracts/DataModels";
|
|
||||||
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { HttpHeaders } from "../Constants";
|
|
||||||
import { handleError } from "../ErrorHandlingUtils";
|
|
||||||
|
|
||||||
interface UpdateOfferThroughputRequest {
|
|
||||||
subscriptionId: string;
|
|
||||||
resourceGroup: string;
|
|
||||||
databaseAccountName: string;
|
|
||||||
databaseName: string;
|
|
||||||
collectionName?: string;
|
|
||||||
throughput: number;
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: boolean;
|
|
||||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThroughputRequest): Promise<void> {
|
|
||||||
if (configContext.platform !== Platform.Portal) {
|
|
||||||
throw new Error("Updating throughput beyond specified limit is not supported on this platform");
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceDescriptionInfo = request.collectionName
|
|
||||||
? `database ${request.databaseName} and container ${request.collectionName}`
|
|
||||||
: `database ${request.databaseName}`;
|
|
||||||
|
|
||||||
const clearMessage = logConsoleProgress(
|
|
||||||
`Requesting increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const url = `${configContext.BACKEND_ENDPOINT}/api/offerthroughputrequest/updatebeyondspecifiedlimit`;
|
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(request),
|
|
||||||
headers: { [authorizationHeader.header]: authorizationHeader.token, [HttpHeaders.contentType]: "application/json" }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
logConsoleInfo(
|
|
||||||
`Successfully requested an increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
|
||||||
);
|
|
||||||
clearMessage();
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = await response.json();
|
|
||||||
handleError(
|
|
||||||
error,
|
|
||||||
"updateOfferThroughputBeyondLimit",
|
|
||||||
`Failed to request an increase in throughput for ${request.throughput}`
|
|
||||||
);
|
|
||||||
clearMessage();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
@@ -26,6 +26,7 @@ interface ConfigContext {
|
|||||||
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
||||||
hostedExplorerURL: string;
|
hostedExplorerURL: string;
|
||||||
armAPIVersion?: string;
|
armAPIVersion?: string;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration
|
||||||
|
|||||||
@@ -208,12 +208,21 @@ export interface QueryMetrics {
|
|||||||
vmExecutionTime: any;
|
vmExecutionTime: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Offer extends Resource {
|
export interface Offer {
|
||||||
|
id: string;
|
||||||
|
autoscaleMaxThroughput: number;
|
||||||
|
manualThroughput: number;
|
||||||
|
minimumThroughput: number;
|
||||||
|
offerDefinition?: SDKOfferDefinition;
|
||||||
|
offerReplacePending: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SDKOfferDefinition extends Resource {
|
||||||
offerVersion?: string;
|
offerVersion?: string;
|
||||||
offerType?: string;
|
offerType?: string;
|
||||||
content?: {
|
content?: {
|
||||||
offerThroughput: number;
|
offerThroughput: number;
|
||||||
offerIsRUPerMinuteThroughputEnabled: boolean;
|
offerIsRUPerMinuteThroughputEnabled?: boolean;
|
||||||
collectionThroughputInfo?: OfferThroughputInfo;
|
collectionThroughputInfo?: OfferThroughputInfo;
|
||||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
offerAutopilotSettings?: AutoPilotOfferSettings;
|
||||||
};
|
};
|
||||||
@@ -221,22 +230,6 @@ export interface Offer extends Resource {
|
|||||||
offerResourceId?: string;
|
offerResourceId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OfferWithHeaders extends Offer {
|
|
||||||
headers: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CollectionQuotaInfo {
|
|
||||||
storedProcedures: number;
|
|
||||||
triggers: number;
|
|
||||||
functions: number;
|
|
||||||
documentsSize: number;
|
|
||||||
collectionSize: number;
|
|
||||||
documentsCount: number;
|
|
||||||
usageSizeInKB: number;
|
|
||||||
numPartitions: number;
|
|
||||||
uniqueKeyPolicy?: UniqueKeyPolicy; // TODO: This should ideally not be a part of the collection quota. Remove after refactoring. (#119617)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OfferThroughputInfo {
|
export interface OfferThroughputInfo {
|
||||||
minimumRUForCollection: number;
|
minimumRUForCollection: number;
|
||||||
numPhysicalPartitions: number;
|
numPhysicalPartitions: number;
|
||||||
@@ -255,7 +248,6 @@ export interface CreateDatabaseAndCollectionRequest {
|
|||||||
collectionId: string;
|
collectionId: string;
|
||||||
offerThroughput: number;
|
offerThroughput: number;
|
||||||
databaseLevelThroughput: boolean;
|
databaseLevelThroughput: boolean;
|
||||||
rupmEnabled?: boolean;
|
|
||||||
partitionKey?: PartitionKey;
|
partitionKey?: PartitionKey;
|
||||||
indexingPolicy?: IndexingPolicy;
|
indexingPolicy?: IndexingPolicy;
|
||||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export enum MessageTypes {
|
|||||||
GetArcadiaToken,
|
GetArcadiaToken,
|
||||||
CreateWorkspace,
|
CreateWorkspace,
|
||||||
CreateSparkPool,
|
CreateSparkPool,
|
||||||
RefreshDatabaseAccount
|
RefreshDatabaseAccount,
|
||||||
|
InitTestExplorer
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Versions, ActionContracts, Diagnostics };
|
export { Versions, ActionContracts, Diagnostics };
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export interface Collection extends CollectionBase {
|
|||||||
requestSchema?: () => void;
|
requestSchema?: () => void;
|
||||||
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||||
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||||
quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
|
usageSizeInKB: ko.Observable<number>;
|
||||||
offer: ko.Observable<DataModels.Offer>;
|
offer: ko.Observable<DataModels.Offer>;
|
||||||
conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||||
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||||
|
|||||||
@@ -44,10 +44,6 @@ describe("Component Registerer", () => {
|
|||||||
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
|
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should register settings-tab component", () => {
|
|
||||||
expect(ko.components.isRegistered("settings-tab")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should register settings-tab-v2 component", () => {
|
it("should register settings-tab-v2 component", () => {
|
||||||
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
|
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,12 +26,11 @@ ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponent
|
|||||||
ko.components.register("tabs-manager", TabsManagerKOComponent());
|
ko.components.register("tabs-manager", TabsManagerKOComponent());
|
||||||
|
|
||||||
// Collection Tabs
|
// Collection Tabs
|
||||||
ko.components.register("documents-tab", new TabComponents.MongoDocumentsTabV2());
|
ko.components.register("documents-tab", new TabComponents.DocumentsTab());
|
||||||
ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab());
|
ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab());
|
||||||
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
|
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
|
||||||
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
|
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
|
||||||
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
|
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
|
||||||
ko.components.register("settings-tab", new TabComponents.SettingsTab());
|
|
||||||
ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2());
|
ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2());
|
||||||
ko.components.register("query-tab", new TabComponents.QueryTab());
|
ko.components.register("query-tab", new TabComponents.QueryTab());
|
||||||
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
|
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
|
||||||
|
|||||||
@@ -44,12 +44,10 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
|||||||
onChange?: (_?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
|
onChange?: (_?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
|
||||||
}[] = [
|
}[] = [
|
||||||
{ key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" },
|
{ key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" },
|
||||||
{ key: "feature.enablerupm", label: "Enable RUPM", value: "true" },
|
|
||||||
{ key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" },
|
{ key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" },
|
||||||
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
||||||
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
||||||
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
|
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
|
||||||
{ key: "feature.enablecodeofconduct", label: "Enable Code Of Conduct Acknowledgement", value: "true" },
|
|
||||||
{
|
{
|
||||||
key: "feature.enableLinkInjection",
|
key: "feature.enableLinkInjection",
|
||||||
label: "Enable Injecting Notebook Viewer Link into the first cell",
|
label: "Enable Injecting Notebook Viewer Link into the first cell",
|
||||||
|
|||||||
@@ -131,12 +131,6 @@ exports[`Feature panel renders all flags 1`] = `
|
|||||||
label="Enable change feed policy"
|
label="Enable change feed policy"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
/>
|
/>
|
||||||
<StyledCheckboxBase
|
|
||||||
checked={false}
|
|
||||||
key="feature.enablerupm"
|
|
||||||
label="Enable RUPM"
|
|
||||||
onChange={[Function]}
|
|
||||||
/>
|
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
key="feature.dataexplorerexecutesproc"
|
key="feature.dataexplorerexecutesproc"
|
||||||
@@ -163,14 +157,14 @@ exports[`Feature panel renders all flags 1`] = `
|
|||||||
/>
|
/>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
key="feature.enablecodeofconduct"
|
key="feature.enableLinkInjection"
|
||||||
label="Enable Code Of Conduct Acknowledgement"
|
label="Enable Injecting Notebook Viewer Link into the first cell"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
/>
|
/>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
key="feature.enableLinkInjection"
|
key="feature.canexceedmaximumvalue"
|
||||||
label="Enable Injecting Notebook Viewer Link into the first cell"
|
label="Can exceed max value"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -178,12 +172,6 @@ exports[`Feature panel renders all flags 1`] = `
|
|||||||
className="checkboxRow"
|
className="checkboxRow"
|
||||||
horizontalAlign="space-between"
|
horizontalAlign="space-between"
|
||||||
>
|
>
|
||||||
<StyledCheckboxBase
|
|
||||||
checked={false}
|
|
||||||
key="feature.canexceedmaximumvalue"
|
|
||||||
label="Can exceed max value"
|
|
||||||
onChange={[Function]}
|
|
||||||
/>
|
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
key="feature.enablefixedcollectionwithsharedthroughput"
|
key="feature.enablefixedcollectionwithsharedthroughput"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Dropdown,
|
Dropdown,
|
||||||
FocusZone,
|
FocusZone,
|
||||||
|
FontIcon,
|
||||||
FontWeights,
|
FontWeights,
|
||||||
IDropdownOption,
|
IDropdownOption,
|
||||||
IPageSpecification,
|
IPageSpecification,
|
||||||
@@ -16,7 +17,7 @@ import {
|
|||||||
Text
|
Text
|
||||||
} from "office-ui-fabric-react";
|
} from "office-ui-fabric-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
|
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient";
|
||||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||||
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
||||||
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
|
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
|
||||||
@@ -136,7 +137,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||||
|
|
||||||
if (this.props.container?.isGalleryPublishEnabled()) {
|
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||||
tabs.push(
|
tabs.push(
|
||||||
@@ -146,7 +147,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
this.state.isCodeOfConductAccepted
|
this.state.isCodeOfConductAccepted
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||||
|
|
||||||
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
|
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
|
||||||
// Displaying code of conduct component on gallery load should not be the default behavior.
|
// Displaying code of conduct component on gallery load should not be the default behavior.
|
||||||
@@ -183,6 +184,27 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isEmptyData = (data: IGalleryItem[]): boolean => {
|
||||||
|
return !data || data.length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
private createEmptyTabContent = (iconName: string, line1: string, line2: string): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}>
|
||||||
|
<FontIcon iconName={iconName} style={{ fontSize: 100, color: "lightgray", marginTop: 20 }} />
|
||||||
|
<Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{line1}</Text>
|
||||||
|
<Text>{line2}</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private createSamplesTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||||
|
return {
|
||||||
|
tab,
|
||||||
|
content: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
private createPublicGalleryTab(
|
private createPublicGalleryTab(
|
||||||
tab: GalleryTab,
|
tab: GalleryTab,
|
||||||
data: IGalleryItem[],
|
data: IGalleryItem[],
|
||||||
@@ -194,17 +216,29 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
private createFavoritesTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||||
return {
|
return {
|
||||||
tab,
|
tab,
|
||||||
content: this.createSearchBarHeader(this.createCardsTabContent(data))
|
content: this.isEmptyData(data)
|
||||||
|
? this.createEmptyTabContent(
|
||||||
|
"ContactHeart",
|
||||||
|
"You have not liked anything",
|
||||||
|
"Like any notebook from Official Samples or Public gallery"
|
||||||
|
)
|
||||||
|
: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||||
return {
|
return {
|
||||||
tab,
|
tab,
|
||||||
content: this.createPublishedNotebooksTabContent(data)
|
content: this.isEmptyData(data)
|
||||||
|
? this.createEmptyTabContent(
|
||||||
|
"Contact",
|
||||||
|
"You have not published anything",
|
||||||
|
"Publish your sample notebooks to share your published work with others"
|
||||||
|
)
|
||||||
|
: this.createPublishedNotebooksTabContent(data)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -364,9 +398,9 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||||
if (!offline) {
|
if (!offline) {
|
||||||
try {
|
try {
|
||||||
let response: IJunoResponse<IPublicGalleryData> | IJunoResponse<IGalleryItem[]>;
|
let response: IJunoResponse<IGalleryItem[]> | IJunoResponse<IPublicGalleryData>;
|
||||||
if (this.props.container.isCodeOfConductEnabled()) {
|
if (this.props.container) {
|
||||||
response = await this.props.junoClient.fetchPublicNotebooks();
|
response = await this.props.junoClient.getPublicGalleryData();
|
||||||
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
|
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
|
||||||
this.publicNotebooks = response.data?.notebooksData;
|
this.publicNotebooks = response.data?.notebooksData;
|
||||||
} else {
|
} else {
|
||||||
@@ -568,7 +602,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
|
|
||||||
private deleteItem = async (data: IGalleryItem): Promise<void> => {
|
private deleteItem = async (data: IGalleryItem): Promise<void> => {
|
||||||
GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, item => {
|
GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, item => {
|
||||||
this.publishedNotebooks = this.publishedNotebooks.filter(notebook => item.id !== notebook.id);
|
this.publishedNotebooks = this.publishedNotebooks?.filter(notebook => item.id !== notebook.id);
|
||||||
this.refreshSelectedTab(item);
|
this.refreshSelectedTab(item);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -89,12 +89,12 @@ describe("SettingsComponent", () => {
|
|||||||
it("auto pilot helper functions pass on correct value", () => {
|
it("auto pilot helper functions pass on correct value", () => {
|
||||||
const newCollection = { ...collection };
|
const newCollection = { ...collection };
|
||||||
newCollection.offer = ko.observable<DataModels.Offer>({
|
newCollection.offer = ko.observable<DataModels.Offer>({
|
||||||
content: {
|
autoscaleMaxThroughput: 10000,
|
||||||
offerAutopilotSettings: {
|
manualThroughput: undefined,
|
||||||
maxThroughput: 10000
|
minimumThroughput: 400,
|
||||||
}
|
id: "test",
|
||||||
}
|
offerReplacePending: false
|
||||||
} as DataModels.Offer);
|
});
|
||||||
|
|
||||||
const props = { ...baseProps };
|
const props = { ...baseProps };
|
||||||
props.settingsTab.collection = newCollection;
|
props.settingsTab.collection = newCollection;
|
||||||
@@ -187,21 +187,6 @@ describe("SettingsComponent", () => {
|
|||||||
expect(settingsComponentInstance.hasConflictResolution()).toEqual(true);
|
expect(settingsComponentInstance.hasConflictResolution()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("isOfferReplacePending", () => {
|
|
||||||
let settingsComponentInstance = new SettingsComponent(baseProps);
|
|
||||||
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(undefined);
|
|
||||||
|
|
||||||
const newCollection = { ...collection };
|
|
||||||
newCollection.offer = ko.observable({
|
|
||||||
headers: { "x-ms-offer-replace-pending": true }
|
|
||||||
} as DataModels.OfferWithHeaders);
|
|
||||||
const props = { ...baseProps };
|
|
||||||
props.settingsTab.collection = newCollection;
|
|
||||||
|
|
||||||
settingsComponentInstance = new SettingsComponent(props);
|
|
||||||
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("save calls updateCollection, updateMongoDBCollectionThroughRP and updateOffer", async () => {
|
it("save calls updateCollection, updateMongoDBCollectionThroughRP and updateOffer", async () => {
|
||||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||||
wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true, isMongoIndexingPolicySaveable: true });
|
wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true, isMongoIndexingPolicySaveable: true });
|
||||||
|
|||||||
@@ -2,28 +2,23 @@ import * as React from "react";
|
|||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as SharedConstants from "../../../Shared/Constants";
|
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import DiscardIcon from "../../../../images/discard.svg";
|
import DiscardIcon from "../../../../images/discard.svg";
|
||||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||||
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||||
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { userContext } from "../../../UserContext";
|
|
||||||
import { updateOfferThroughputBeyondLimit } from "../../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
|
||||||
import SettingsTab from "../../Tabs/SettingsTabV2";
|
import SettingsTab from "../../Tabs/SettingsTabV2";
|
||||||
import { throughputUnit } from "./SettingsRenderUtils";
|
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||||
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
||||||
import {
|
import {
|
||||||
MongoIndexingPolicyComponent,
|
MongoIndexingPolicyComponent,
|
||||||
MongoIndexingPolicyComponentProps
|
MongoIndexingPolicyComponentProps
|
||||||
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
||||||
import {
|
import {
|
||||||
getMaxRUs,
|
|
||||||
hasDatabaseSharedThroughput,
|
hasDatabaseSharedThroughput,
|
||||||
GeospatialConfigType,
|
GeospatialConfigType,
|
||||||
TtlType,
|
TtlType,
|
||||||
@@ -49,6 +44,7 @@ import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/genera
|
|||||||
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
|
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
|
||||||
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
|
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
|
||||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||||
|
import { isEmpty } from "underscore";
|
||||||
|
|
||||||
interface SettingsV2TabInfo {
|
interface SettingsV2TabInfo {
|
||||||
tab: SettingsV2TabTypes;
|
tab: SettingsV2TabTypes;
|
||||||
@@ -227,7 +223,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
|
|
||||||
public loadMongoIndexes = async (): Promise<void> => {
|
public loadMongoIndexes = async (): Promise<void> => {
|
||||||
if (
|
if (
|
||||||
this.container.isMongoIndexEditorEnabled() &&
|
|
||||||
this.container.isPreferredApiMongoDB() &&
|
this.container.isPreferredApiMongoDB() &&
|
||||||
this.container.isEnableMongoCapabilityPresent() &&
|
this.container.isEnableMongoCapabilityPresent() &&
|
||||||
this.container.databaseAccount()
|
this.container.databaseAccount()
|
||||||
@@ -275,19 +270,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
};
|
};
|
||||||
|
|
||||||
private setAutoPilotStates = (): void => {
|
private setAutoPilotStates = (): void => {
|
||||||
const offer = this.collection?.offer && this.collection.offer();
|
const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput;
|
||||||
const offerAutopilotSettings = offer?.content?.offerAutopilotSettings;
|
|
||||||
|
|
||||||
if (
|
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||||
offerAutopilotSettings &&
|
|
||||||
offerAutopilotSettings.maxThroughput &&
|
|
||||||
AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)
|
|
||||||
) {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isAutoPilotSelected: true,
|
isAutoPilotSelected: true,
|
||||||
wasAutopilotOriginallySet: true,
|
wasAutopilotOriginallySet: true,
|
||||||
autoPilotThroughput: offerAutopilotSettings.maxThroughput,
|
autoPilotThroughput: autoscaleMaxThroughput,
|
||||||
autoPilotThroughputBaseline: offerAutopilotSettings.maxThroughput
|
autoPilotThroughputBaseline: autoscaleMaxThroughput
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -305,12 +295,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
!!this.collection.conflictResolutionPolicy();
|
!!this.collection.conflictResolutionPolicy();
|
||||||
|
|
||||||
public isOfferReplacePending = (): boolean => {
|
public isOfferReplacePending = (): boolean => {
|
||||||
const offer = this.collection?.offer && this.collection.offer();
|
return this.collection?.offer()?.offerReplacePending;
|
||||||
return (
|
|
||||||
offer &&
|
|
||||||
Object.keys(offer).find(value => value === "headers") &&
|
|
||||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public onSaveClick = async (): Promise<void> => {
|
public onSaveClick = async (): Promise<void> => {
|
||||||
@@ -448,103 +433,34 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.isScaleSaveable) {
|
if (this.state.isScaleSaveable) {
|
||||||
const newThroughput = this.state.throughput;
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
const newOffer: DataModels.Offer = { ...this.collection.offer() };
|
databaseId: this.collection.databaseId,
|
||||||
const originalThroughputValue: number = this.state.throughput;
|
collectionId: this.collection.id(),
|
||||||
|
currentOffer: this.collection.offer(),
|
||||||
if (newOffer.content) {
|
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||||
newOffer.content.offerThroughput = newThroughput;
|
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput
|
||||||
} else {
|
};
|
||||||
newOffer.content = {
|
if (this.hasProvisioningTypeChanged()) {
|
||||||
offerThroughput: newThroughput,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
|
||||||
|
|
||||||
if (this.state.isAutoPilotSelected) {
|
|
||||||
newOffer.content.offerAutopilotSettings = {
|
|
||||||
maxThroughput: this.state.autoPilotThroughput
|
|
||||||
};
|
|
||||||
|
|
||||||
// user has changed from provisioned --> autoscale
|
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
|
||||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
|
|
||||||
delete newOffer.content.offerAutopilotSettings;
|
|
||||||
} else {
|
|
||||||
delete newOffer.content.offerThroughput;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
isAutoPilotSelected: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// user has changed from autoscale --> provisioned
|
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
|
||||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
|
|
||||||
} else {
|
|
||||||
delete newOffer.content.offerAutopilotSettings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
getMaxRUs(this.collection, this.container) <=
|
|
||||||
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
this.container
|
|
||||||
) {
|
|
||||||
const requestPayload = {
|
|
||||||
subscriptionId: userContext.subscriptionId,
|
|
||||||
databaseAccountName: userContext.databaseAccount.name,
|
|
||||||
resourceGroup: userContext.resourceGroup,
|
|
||||||
databaseName: this.collection.databaseId,
|
|
||||||
collectionName: this.collection.id(),
|
|
||||||
throughput: newThroughput,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
|
||||||
};
|
|
||||||
|
|
||||||
await updateOfferThroughputBeyondLimit(requestPayload);
|
|
||||||
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
|
||||||
this.setState({
|
|
||||||
isScaleSaveable: false,
|
|
||||||
isScaleDiscardable: false,
|
|
||||||
throughput: originalThroughputValue,
|
|
||||||
throughputBaseline: originalThroughputValue,
|
|
||||||
initialNotification: {
|
|
||||||
description: `Throughput update for ${newThroughput} ${throughputUnit}`
|
|
||||||
} as DataModels.Notification
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
|
||||||
databaseId: this.collection.databaseId,
|
|
||||||
collectionId: this.collection.id(),
|
|
||||||
currentOffer: this.collection.offer(),
|
|
||||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
|
||||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : newThroughput
|
|
||||||
};
|
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
|
||||||
if (this.state.isAutoPilotSelected) {
|
|
||||||
updateOfferParams.migrateToAutoPilot = true;
|
|
||||||
} else {
|
|
||||||
updateOfferParams.migrateToManual = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
|
||||||
this.collection.offer(updatedOffer);
|
|
||||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
|
||||||
if (this.state.isAutoPilotSelected) {
|
if (this.state.isAutoPilotSelected) {
|
||||||
this.setState({
|
updateOfferParams.migrateToAutoPilot = true;
|
||||||
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput,
|
|
||||||
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
updateOfferParams.migrateToManual = true;
|
||||||
throughput: updatedOffer.content.offerThroughput,
|
|
||||||
throughputBaseline: updatedOffer.content.offerThroughput
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||||
|
this.collection.offer(updatedOffer);
|
||||||
|
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||||
|
if (this.state.isAutoPilotSelected) {
|
||||||
|
this.setState({
|
||||||
|
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
|
||||||
|
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
throughput: updatedOffer.manualThroughput,
|
||||||
|
throughputBaseline: updatedOffer.manualThroughput
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
this.setBaseline();
|
this.setBaseline();
|
||||||
@@ -809,7 +725,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const offerThroughput = this.collection?.offer && this.collection.offer()?.content?.offerThroughput;
|
const offerThroughput = this.collection.offer()?.manualThroughput;
|
||||||
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
||||||
? ChangeFeedPolicyState.On
|
? ChangeFeedPolicyState.On
|
||||||
: ChangeFeedPolicyState.Off;
|
: ChangeFeedPolicyState.Off;
|
||||||
@@ -1000,15 +916,18 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
|
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
|
||||||
});
|
});
|
||||||
} else if (
|
} else if (this.container.isPreferredApiMongoDB()) {
|
||||||
this.container.isMongoIndexEditorEnabled() &&
|
if (isEmpty(this.container.features())) {
|
||||||
this.container.isPreferredApiMongoDB() &&
|
tabs.push({
|
||||||
this.container.isEnableMongoCapabilityPresent()
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
) {
|
content: mongoIndexingPolicyAADError
|
||||||
tabs.push({
|
});
|
||||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
} else if (this.container.isEnableMongoCapabilityPresent()) {
|
||||||
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
tabs.push({
|
||||||
});
|
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||||
|
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hasConflictResolution()) {
|
if (this.hasConflictResolution()) {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
|||||||
{getAutoPilotV3SpendElement(1000, true)}
|
{getAutoPilotV3SpendElement(1000, true)}
|
||||||
{getAutoPilotV3SpendElement(undefined, true)}
|
{getAutoPilotV3SpendElement(undefined, true)}
|
||||||
|
|
||||||
{getEstimatedSpendElement(1000, "mooncake", 2, false, true)}
|
{getEstimatedSpendElement(1000, "mooncake", 2, false)}
|
||||||
|
|
||||||
{getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)}
|
{getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
|||||||
{updateThroughputDelayedApplyWarningMessage}
|
{updateThroughputDelayedApplyWarningMessage}
|
||||||
|
|
||||||
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||||
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection")}
|
||||||
{getThroughputApplyLongDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
{getThroughputApplyLongDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||||
|
|
||||||
{getToolTipContainer(<span>Sample Text</span>)}
|
{getToolTipContainer(<span>Sample Text</span>)}
|
||||||
|
|||||||
@@ -199,10 +199,9 @@ export const getEstimatedSpendElement = (
|
|||||||
throughput: number,
|
throughput: number,
|
||||||
serverId: string,
|
serverId: string,
|
||||||
regions: number,
|
regions: number,
|
||||||
multimaster: boolean,
|
multimaster: boolean
|
||||||
rupmEnabled: boolean
|
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, rupmEnabled, throughput, regions, multimaster);
|
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster);
|
||||||
const dailyPrice: number = hourlyPrice * 24;
|
const dailyPrice: number = hourlyPrice * 24;
|
||||||
const monthlyPrice: number = hourlyPrice * hoursInAMonth;
|
const monthlyPrice: number = hourlyPrice * hoursInAMonth;
|
||||||
const currency: string = getPriceCurrency(serverId);
|
const currency: string = getPriceCurrency(serverId);
|
||||||
@@ -319,14 +318,13 @@ export const getThroughputApplyShortDelayMessage = (
|
|||||||
throughput: number,
|
throughput: number,
|
||||||
throughputUnit: string,
|
throughputUnit: string,
|
||||||
databaseName: string,
|
databaseName: string,
|
||||||
collectionName: string,
|
collectionName: string
|
||||||
targetThroughput: number
|
|
||||||
): JSX.Element => (
|
): JSX.Element => (
|
||||||
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
|
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
|
||||||
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
||||||
<br />
|
<br />
|
||||||
Database: {databaseName}, Container: {collectionName}{" "}
|
Database: {databaseName}, Container: {collectionName}{" "}
|
||||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, targetThroughput)}
|
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
transparentDetailsRowStyles,
|
transparentDetailsRowStyles,
|
||||||
createAndAddMongoIndexStackProps,
|
createAndAddMongoIndexStackProps,
|
||||||
separatorStyles,
|
separatorStyles,
|
||||||
mongoIndexingPolicyAADError,
|
|
||||||
indexingPolicynUnsavedWarningMessage,
|
indexingPolicynUnsavedWarningMessage,
|
||||||
infoAndToolTipTextStyle
|
infoAndToolTipTextStyle
|
||||||
} from "../../SettingsRenderUtils";
|
} from "../../SettingsRenderUtils";
|
||||||
@@ -40,7 +39,6 @@ import {
|
|||||||
} from "../../SettingsUtils";
|
} from "../../SettingsUtils";
|
||||||
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
||||||
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
|
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
import { AuthType } from "../../../../../AuthType";
|
|
||||||
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||||
|
|
||||||
export interface MongoIndexingPolicyComponentProps {
|
export interface MongoIndexingPolicyComponentProps {
|
||||||
@@ -321,7 +319,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return window.authType !== AuthType.AAD ? mongoIndexingPolicyAADError : <Spinner size={SpinnerSize.large} />;
|
return <Spinner size={SpinnerSize.large} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ describe("ScaleComponent", () => {
|
|||||||
} as DataModels.Notification
|
} as DataModels.Notification
|
||||||
};
|
};
|
||||||
|
|
||||||
it("renders with correct intiial notification", () => {
|
it("renders with correct initial notification", () => {
|
||||||
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
||||||
@@ -54,16 +54,13 @@ describe("ScaleComponent", () => {
|
|||||||
|
|
||||||
const newCollection = { ...collection };
|
const newCollection = { ...collection };
|
||||||
const maxThroughput = 5000;
|
const maxThroughput = 5000;
|
||||||
const targetMaxThroughput = 50000;
|
|
||||||
newCollection.offer = ko.observable({
|
newCollection.offer = ko.observable({
|
||||||
content: {
|
manualThroughput: undefined,
|
||||||
offerAutopilotSettings: {
|
autoscaleMaxThroughput: maxThroughput,
|
||||||
maxThroughput: maxThroughput,
|
minimumThroughput: 400,
|
||||||
targetMaxThroughput: targetMaxThroughput
|
id: "offer",
|
||||||
}
|
offerReplacePending: true
|
||||||
},
|
});
|
||||||
headers: { "x-ms-offer-replace-pending": true }
|
|
||||||
} as DataModels.OfferWithHeaders);
|
|
||||||
const newProps = {
|
const newProps = {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
initialNotification: undefined as DataModels.Notification,
|
initialNotification: undefined as DataModels.Notification,
|
||||||
@@ -73,7 +70,6 @@ describe("ScaleComponent", () => {
|
|||||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
||||||
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
||||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
|
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
|
||||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(targetMaxThroughput);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("autoScale disabled", () => {
|
it("autoScale disabled", () => {
|
||||||
@@ -109,11 +105,6 @@ describe("ScaleComponent", () => {
|
|||||||
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
|
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getMaxRUThroughputInputLimit", () => {
|
|
||||||
const scaleComponent = new ScaleComponent(baseProps);
|
|
||||||
expect(scaleComponent.getMaxRUThroughputInputLimit()).toEqual(40000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getThroughputTitle", () => {
|
it("getThroughputTitle", () => {
|
||||||
let scaleComponent = new ScaleComponent(baseProps);
|
let scaleComponent = new ScaleComponent(baseProps);
|
||||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
||||||
@@ -138,14 +129,8 @@ describe("ScaleComponent", () => {
|
|||||||
|
|
||||||
it("getThroughputWarningMessage", () => {
|
it("getThroughputWarningMessage", () => {
|
||||||
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
|
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
|
||||||
const throughputBeyondMaxRus = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million - 1000;
|
|
||||||
|
|
||||||
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
|
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
|
||||||
let scaleComponent = new ScaleComponent(newProps);
|
const scaleComponent = new ScaleComponent(newProps);
|
||||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
|
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
|
||||||
|
|
||||||
newProps.throughput = throughputBeyondMaxRus;
|
|
||||||
scaleComponent = new ScaleComponent(newProps);
|
|
||||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputDelayedApplyWarningMessage");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ import {
|
|||||||
throughputUnit,
|
throughputUnit,
|
||||||
getThroughputApplyLongDelayMessage,
|
getThroughputApplyLongDelayMessage,
|
||||||
getThroughputApplyShortDelayMessage,
|
getThroughputApplyShortDelayMessage,
|
||||||
updateThroughputBeyondLimitWarningMessage,
|
updateThroughputBeyondLimitWarningMessage
|
||||||
updateThroughputDelayedApplyWarningMessage
|
|
||||||
} from "../SettingsRenderUtils";
|
} from "../SettingsRenderUtils";
|
||||||
import { getMaxRUs, getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
|
import { hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||||
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
||||||
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||||
import { configContext, Platform } from "../../../../ConfigContext";
|
import { configContext, Platform } from "../../../../ConfigContext";
|
||||||
@@ -62,11 +61,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private getStorageCapacityTitle = (): JSX.Element => {
|
private getStorageCapacityTitle = (): JSX.Element => {
|
||||||
// Mongo container with system partition key still treat as "Fixed"
|
const capacity: string = this.props.isFixedContainer ? "Fixed" : "Unlimited";
|
||||||
const isFixed =
|
|
||||||
!this.props.collection.partitionKey ||
|
|
||||||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey);
|
|
||||||
const capacity: string = isFixed ? "Fixed" : "Unlimited";
|
|
||||||
return (
|
return (
|
||||||
<Stack {...titleAndInputStackProps}>
|
<Stack {...titleAndInputStackProps}>
|
||||||
<Label>Storage capacity</Label>
|
<Label>Storage capacity</Label>
|
||||||
@@ -75,12 +70,26 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public getMaxRUThroughputInputLimit = (): number => {
|
public getMaxRUs = (): number => {
|
||||||
if (configContext.platform === Platform.Hosted && this.props.collection.partitionKey) {
|
if (this.props.container?.isTryCosmosDBSubscription()) {
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
return Constants.TryCosmosExperience.maxRU;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getMaxRUs(this.props.collection, this.props.container);
|
if (this.props.isFixedContainer) {
|
||||||
|
return SharedConstants.CollectionCreation.MaxRUPerPartition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||||
|
};
|
||||||
|
|
||||||
|
public getMinRUs = (): number => {
|
||||||
|
if (this.props.container?.isTryCosmosDBSubscription()) {
|
||||||
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.props.collection.offer()?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public getThroughputTitle = (): string => {
|
public getThroughputTitle = (): string => {
|
||||||
@@ -88,11 +97,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
return AutoPilotUtils.getAutoPilotHeaderText();
|
return AutoPilotUtils.getAutoPilotHeaderText();
|
||||||
}
|
}
|
||||||
|
|
||||||
const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString();
|
const minThroughput: string = this.getMinRUs().toLocaleString();
|
||||||
const maxThroughput: string =
|
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : this.getMaxRUs().toLocaleString();
|
||||||
this.canThroughputExceedMaximumValue() && !this.props.isFixedContainer
|
|
||||||
? "unlimited"
|
|
||||||
: getMaxRUs(this.props.collection, this.props.container).toLocaleString();
|
|
||||||
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,26 +115,15 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
return this.getLongDelayMessage();
|
return this.getLongDelayMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
const offer = this.props.collection?.offer && this.props.collection.offer();
|
const offer = this.props.collection?.offer();
|
||||||
if (
|
if (offer?.offerReplacePending) {
|
||||||
offer &&
|
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
||||||
Object.keys(offer).find(value => {
|
|
||||||
return value === "headers";
|
|
||||||
}) &&
|
|
||||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
|
||||||
) {
|
|
||||||
const throughput = offer?.content?.offerAutopilotSettings?.maxThroughput;
|
|
||||||
|
|
||||||
const targetThroughput =
|
|
||||||
offer.content?.offerAutopilotSettings?.targetMaxThroughput || offer?.content?.offerThroughput;
|
|
||||||
|
|
||||||
return getThroughputApplyShortDelayMessage(
|
return getThroughputApplyShortDelayMessage(
|
||||||
this.props.isAutoPilotSelected,
|
this.props.isAutoPilotSelected,
|
||||||
throughput,
|
throughput,
|
||||||
throughputUnit,
|
throughputUnit,
|
||||||
this.props.collection.databaseId,
|
this.props.collection.databaseId,
|
||||||
this.props.collection.id(),
|
this.props.collection.id()
|
||||||
targetThroughput
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,21 +133,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
public getThroughputWarningMessage = (): JSX.Element => {
|
public getThroughputWarningMessage = (): JSX.Element => {
|
||||||
const throughputExceedsBackendLimits: boolean =
|
const throughputExceedsBackendLimits: boolean =
|
||||||
this.canThroughputExceedMaximumValue() &&
|
this.canThroughputExceedMaximumValue() &&
|
||||||
getMaxRUs(this.props.collection, this.props.container) <=
|
|
||||||
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||||
|
|
||||||
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
||||||
return updateThroughputBeyondLimitWarningMessage;
|
return updateThroughputBeyondLimitWarningMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const throughputExceedsMaxValue: boolean =
|
|
||||||
!this.isEmulator && this.props.throughput > getMaxRUs(this.props.collection, this.props.container);
|
|
||||||
|
|
||||||
if (throughputExceedsMaxValue && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
|
||||||
return updateThroughputDelayedApplyWarningMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -179,12 +165,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
private getThroughputInputComponent = (): JSX.Element => (
|
private getThroughputInputComponent = (): JSX.Element => (
|
||||||
<ThroughputInputAutoPilotV3Component
|
<ThroughputInputAutoPilotV3Component
|
||||||
databaseAccount={this.props.container.databaseAccount()}
|
databaseAccount={this.props.container.databaseAccount()}
|
||||||
serverId={this.props.container.serverId()}
|
serverId={configContext.serverId}
|
||||||
throughput={this.props.throughput}
|
throughput={this.props.throughput}
|
||||||
throughputBaseline={this.props.throughputBaseline}
|
throughputBaseline={this.props.throughputBaseline}
|
||||||
onThroughputChange={this.props.onThroughputChange}
|
onThroughputChange={this.props.onThroughputChange}
|
||||||
minimum={getMinRUs(this.props.collection, this.props.container)}
|
minimum={this.getMinRUs()}
|
||||||
maximum={this.getMaxRUThroughputInputLimit()}
|
maximum={this.getMaxRUs()}
|
||||||
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
||||||
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
||||||
label={this.getThroughputTitle()}
|
label={this.getThroughputTitle()}
|
||||||
@@ -200,7 +186,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
||||||
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
||||||
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
||||||
usageSizeInKB={this.props.collection.quotaInfo().usageSizeInKB}
|
usageSizeInKB={this.props.collection.usageSizeInKB()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -179,8 +179,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : offerThroughput,
|
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : offerThroughput,
|
||||||
serverId,
|
serverId,
|
||||||
regions,
|
regions,
|
||||||
multimaster,
|
multimaster
|
||||||
false
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
estimatedSpend = getEstimatedAutoscaleSpendElement(
|
estimatedSpend = getEstimatedAutoscaleSpendElement(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||||
<Stack
|
<Stack
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
@@ -48,7 +48,7 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
|||||||
label="Throughput (6,000 - unlimited RU/s)"
|
label="Throughput (6,000 - unlimited RU/s)"
|
||||||
maxAutoPilotThroughput={4000}
|
maxAutoPilotThroughput={4000}
|
||||||
maxAutoPilotThroughputBaseline={4000}
|
maxAutoPilotThroughputBaseline={4000}
|
||||||
maximum={40000}
|
maximum={1000000}
|
||||||
minimum={6000}
|
minimum={6000}
|
||||||
onAutoPilotSelected={[Function]}
|
onAutoPilotSelected={[Function]}
|
||||||
onMaxAutoPilotThroughputChange={[Function]}
|
onMaxAutoPilotThroughputChange={[Function]}
|
||||||
@@ -58,6 +58,7 @@ exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
|||||||
spendAckChecked={false}
|
spendAckChecked={false}
|
||||||
throughput={1000}
|
throughput={1000}
|
||||||
throughputBaseline={1000}
|
throughputBaseline={1000}
|
||||||
|
usageSizeInKB={100}
|
||||||
wasAutopilotOriginallySet={true}
|
wasAutopilotOriginallySet={true}
|
||||||
/>
|
/>
|
||||||
<Stack
|
<Stack
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { collection, container } from "./TestUtils";
|
import { collection } from "./TestUtils";
|
||||||
import {
|
import {
|
||||||
getMaxRUs,
|
|
||||||
getMinRUs,
|
|
||||||
getMongoIndexType,
|
getMongoIndexType,
|
||||||
getMongoNotification,
|
getMongoNotification,
|
||||||
getSanitizedInputValue,
|
getSanitizedInputValue,
|
||||||
@@ -23,16 +21,6 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
|||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
|
|
||||||
describe("SettingsUtils", () => {
|
describe("SettingsUtils", () => {
|
||||||
it("getMaxRUs", () => {
|
|
||||||
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
|
|
||||||
expect(getMaxRUs(collection, container)).toEqual(40000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getMinRUs", () => {
|
|
||||||
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
|
|
||||||
expect(getMinRUs(collection, container)).toEqual(6000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("hasDatabaseSharedThroughput", () => {
|
it("hasDatabaseSharedThroughput", () => {
|
||||||
expect(hasDatabaseSharedThroughput(collection)).toEqual(undefined);
|
expect(hasDatabaseSharedThroughput(collection)).toEqual(undefined);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import * as SharedConstants from "../../../Shared/Constants";
|
|
||||||
import * as PricingUtils from "../../../Utils/PricingUtils";
|
|
||||||
|
|
||||||
import Explorer from "../../Explorer";
|
|
||||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
|
|
||||||
const zeroValue = 0;
|
const zeroValue = 0;
|
||||||
@@ -71,57 +67,6 @@ export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection):
|
|||||||
return database?.isDatabaseShared() && !collection.offer();
|
return database?.isDatabaseShared() && !collection.offer();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMaxRUs = (collection: ViewModels.Collection, container: Explorer): number => {
|
|
||||||
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription() || false;
|
|
||||||
if (isTryCosmosDBSubscription) {
|
|
||||||
return Constants.TryCosmosExperience.maxRU;
|
|
||||||
}
|
|
||||||
|
|
||||||
const numPartitionsFromOffer: number =
|
|
||||||
collection?.offer && collection.offer()?.content?.collectionThroughputInfo?.numPhysicalPartitions;
|
|
||||||
|
|
||||||
const numPartitionsFromQuotaInfo: number = collection?.quotaInfo()?.numPartitions;
|
|
||||||
|
|
||||||
const numPartitions = numPartitionsFromOffer ?? numPartitionsFromQuotaInfo ?? 1;
|
|
||||||
|
|
||||||
return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMinRUs = (collection: ViewModels.Collection, container: Explorer): number => {
|
|
||||||
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription();
|
|
||||||
if (isTryCosmosDBSubscription) {
|
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
|
||||||
}
|
|
||||||
|
|
||||||
const offerContent = collection?.offer && collection.offer()?.content;
|
|
||||||
|
|
||||||
if (offerContent?.offerAutopilotSettings) {
|
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
|
||||||
}
|
|
||||||
|
|
||||||
const collectionThroughputInfo: DataModels.OfferThroughputInfo = offerContent?.collectionThroughputInfo;
|
|
||||||
|
|
||||||
if (collectionThroughputInfo?.minimumRUForCollection > 0) {
|
|
||||||
return collectionThroughputInfo.minimumRUForCollection;
|
|
||||||
}
|
|
||||||
|
|
||||||
const numPartitions = collectionThroughputInfo?.numPhysicalPartitions ?? collection.quotaInfo()?.numPartitions;
|
|
||||||
|
|
||||||
if (!numPartitions || numPartitions === 1) {
|
|
||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseRU = SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
|
||||||
|
|
||||||
const quotaInKb = collection.quotaInfo().collectionSize;
|
|
||||||
const quotaInGb = PricingUtils.usageInGB(quotaInKb);
|
|
||||||
|
|
||||||
const perPartitionGBQuota: number = Math.max(10, quotaInGb / numPartitions);
|
|
||||||
const baseRUbyPartitions: number = ((numPartitions * perPartitionGBQuota) / 10) * 100;
|
|
||||||
|
|
||||||
return Math.max(baseRU, baseRUbyPartitions);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => {
|
export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => {
|
||||||
// Backend can contain different casing as it does case-insensitive comparisson
|
// Backend can contain different casing as it does case-insensitive comparisson
|
||||||
if (!modeFromBackend) {
|
if (!modeFromBackend) {
|
||||||
|
|||||||
@@ -18,17 +18,14 @@ export const collection = ({
|
|||||||
excludedPaths: []
|
excludedPaths: []
|
||||||
}),
|
}),
|
||||||
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
|
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
|
||||||
quotaInfo: ko.observable<DataModels.CollectionQuotaInfo>({} as DataModels.CollectionQuotaInfo),
|
usageSizeInKB: ko.observable(100),
|
||||||
offer: ko.observable<DataModels.Offer>({
|
offer: ko.observable<DataModels.Offer>({
|
||||||
content: {
|
autoscaleMaxThroughput: undefined,
|
||||||
offerThroughput: 10000,
|
manualThroughput: 10000,
|
||||||
offerIsRUPerMinuteThroughputEnabled: false,
|
minimumThroughput: 6000,
|
||||||
collectionThroughputInfo: {
|
id: "offer",
|
||||||
minimumRUForCollection: 6000,
|
offerReplacePending: false
|
||||||
numPhysicalPartitions: 4
|
}),
|
||||||
} as DataModels.OfferThroughputInfo
|
|
||||||
}
|
|
||||||
} as DataModels.Offer),
|
|
||||||
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
|
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
|
||||||
{} as DataModels.ConflictResolutionPolicy
|
{} as DataModels.ConflictResolutionPolicy
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -133,8 +133,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyVisible": [Function],
|
"partitionKeyVisible": [Function],
|
||||||
"requestUnitsUsageCost": [Function],
|
"requestUnitsUsageCost": [Function],
|
||||||
"ruToolTipText": [Function],
|
"ruToolTipText": [Function],
|
||||||
"rupm": [Function],
|
|
||||||
"rupmVisible": [Function],
|
|
||||||
"sharedAutoPilotThroughput": [Function],
|
"sharedAutoPilotThroughput": [Function],
|
||||||
"sharedThroughputRangeText": [Function],
|
"sharedThroughputRangeText": [Function],
|
||||||
"shouldCreateMongoWildcardIndex": [Function],
|
"shouldCreateMongoWildcardIndex": [Function],
|
||||||
@@ -622,8 +620,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyVisible": [Function],
|
"partitionKeyVisible": [Function],
|
||||||
"requestUnitsUsageCost": [Function],
|
"requestUnitsUsageCost": [Function],
|
||||||
"ruToolTipText": [Function],
|
"ruToolTipText": [Function],
|
||||||
"rupm": [Function],
|
|
||||||
"rupmVisible": [Function],
|
|
||||||
"sharedAutoPilotThroughput": [Function],
|
"sharedAutoPilotThroughput": [Function],
|
||||||
"sharedThroughputRangeText": [Function],
|
"sharedThroughputRangeText": [Function],
|
||||||
"shouldCreateMongoWildcardIndex": [Function],
|
"shouldCreateMongoWildcardIndex": [Function],
|
||||||
@@ -735,7 +731,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"visible": [Function],
|
"visible": [Function],
|
||||||
},
|
},
|
||||||
"arcadiaToken": [Function],
|
"arcadiaToken": [Function],
|
||||||
"armEndpoint": [Function],
|
|
||||||
"browseQueriesPane": BrowseQueriesPane {
|
"browseQueriesPane": BrowseQueriesPane {
|
||||||
"canSaveQueries": [Function],
|
"canSaveQueries": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
@@ -947,7 +942,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"hasWriteAccess": [Function],
|
"hasWriteAccess": [Function],
|
||||||
"isAccountReady": [Function],
|
"isAccountReady": [Function],
|
||||||
"isAuthWithResourceToken": [Function],
|
"isAuthWithResourceToken": [Function],
|
||||||
"isCodeOfConductEnabled": [Function],
|
|
||||||
"isCopyNotebookPaneEnabled": [Function],
|
"isCopyNotebookPaneEnabled": [Function],
|
||||||
"isEnableMongoCapabilityPresent": [Function],
|
"isEnableMongoCapabilityPresent": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
@@ -956,7 +950,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isHostedDataExplorerEnabled": [Function],
|
"isHostedDataExplorerEnabled": [Function],
|
||||||
"isLeftPaneExpanded": [Function],
|
"isLeftPaneExpanded": [Function],
|
||||||
"isLinkInjectionEnabled": [Function],
|
"isLinkInjectionEnabled": [Function],
|
||||||
"isMongoIndexEditorEnabled": [Function],
|
|
||||||
"isNotebookEnabled": [Function],
|
"isNotebookEnabled": [Function],
|
||||||
"isNotebooksEnabledForAccount": [Function],
|
"isNotebooksEnabledForAccount": [Function],
|
||||||
"isNotificationConsoleExpanded": [Function],
|
"isNotificationConsoleExpanded": [Function],
|
||||||
@@ -973,7 +966,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isRightPanelV2Enabled": [Function],
|
"isRightPanelV2Enabled": [Function],
|
||||||
"isSchemaEnabled": [Function],
|
"isSchemaEnabled": [Function],
|
||||||
"isServerlessEnabled": [Function],
|
"isServerlessEnabled": [Function],
|
||||||
"isSettingsV2Enabled": [Function],
|
|
||||||
"isSparkEnabled": [Function],
|
"isSparkEnabled": [Function],
|
||||||
"isSparkEnabledForAccount": [Function],
|
"isSparkEnabledForAccount": [Function],
|
||||||
"isSynapseLinkUpdating": [Function],
|
"isSynapseLinkUpdating": [Function],
|
||||||
@@ -1030,7 +1022,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"onSwitchToConnectionString": [Function],
|
"onSwitchToConnectionString": [Function],
|
||||||
"onToggleKeyDown": [Function],
|
"onToggleKeyDown": [Function],
|
||||||
"parentFrameDataExplorerVersion": [Function],
|
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
@@ -1056,7 +1047,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"titleLabel": "Select Columns",
|
"titleLabel": "Select Columns",
|
||||||
"visible": [Function],
|
"visible": [Function],
|
||||||
},
|
},
|
||||||
"quotaId": [Function],
|
|
||||||
"refreshDatabaseAccount": [Function],
|
"refreshDatabaseAccount": [Function],
|
||||||
"refreshNotebookList": [Function],
|
"refreshNotebookList": [Function],
|
||||||
"refreshTreeTitle": [Function],
|
"refreshTreeTitle": [Function],
|
||||||
@@ -1302,9 +1292,9 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"version": 2,
|
"version": 2,
|
||||||
},
|
},
|
||||||
"partitionKeyProperty": "partitionKey",
|
"partitionKeyProperty": "partitionKey",
|
||||||
"quotaInfo": [Function],
|
|
||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": Object {},
|
"uniqueKeyPolicy": Object {},
|
||||||
|
"usageSizeInKB": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
container={
|
container={
|
||||||
@@ -1414,8 +1404,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyVisible": [Function],
|
"partitionKeyVisible": [Function],
|
||||||
"requestUnitsUsageCost": [Function],
|
"requestUnitsUsageCost": [Function],
|
||||||
"ruToolTipText": [Function],
|
"ruToolTipText": [Function],
|
||||||
"rupm": [Function],
|
|
||||||
"rupmVisible": [Function],
|
|
||||||
"sharedAutoPilotThroughput": [Function],
|
"sharedAutoPilotThroughput": [Function],
|
||||||
"sharedThroughputRangeText": [Function],
|
"sharedThroughputRangeText": [Function],
|
||||||
"shouldCreateMongoWildcardIndex": [Function],
|
"shouldCreateMongoWildcardIndex": [Function],
|
||||||
@@ -1903,8 +1891,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyVisible": [Function],
|
"partitionKeyVisible": [Function],
|
||||||
"requestUnitsUsageCost": [Function],
|
"requestUnitsUsageCost": [Function],
|
||||||
"ruToolTipText": [Function],
|
"ruToolTipText": [Function],
|
||||||
"rupm": [Function],
|
|
||||||
"rupmVisible": [Function],
|
|
||||||
"sharedAutoPilotThroughput": [Function],
|
"sharedAutoPilotThroughput": [Function],
|
||||||
"sharedThroughputRangeText": [Function],
|
"sharedThroughputRangeText": [Function],
|
||||||
"shouldCreateMongoWildcardIndex": [Function],
|
"shouldCreateMongoWildcardIndex": [Function],
|
||||||
@@ -2016,7 +2002,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"visible": [Function],
|
"visible": [Function],
|
||||||
},
|
},
|
||||||
"arcadiaToken": [Function],
|
"arcadiaToken": [Function],
|
||||||
"armEndpoint": [Function],
|
|
||||||
"browseQueriesPane": BrowseQueriesPane {
|
"browseQueriesPane": BrowseQueriesPane {
|
||||||
"canSaveQueries": [Function],
|
"canSaveQueries": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
@@ -2228,7 +2213,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"hasWriteAccess": [Function],
|
"hasWriteAccess": [Function],
|
||||||
"isAccountReady": [Function],
|
"isAccountReady": [Function],
|
||||||
"isAuthWithResourceToken": [Function],
|
"isAuthWithResourceToken": [Function],
|
||||||
"isCodeOfConductEnabled": [Function],
|
|
||||||
"isCopyNotebookPaneEnabled": [Function],
|
"isCopyNotebookPaneEnabled": [Function],
|
||||||
"isEnableMongoCapabilityPresent": [Function],
|
"isEnableMongoCapabilityPresent": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
@@ -2237,7 +2221,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isHostedDataExplorerEnabled": [Function],
|
"isHostedDataExplorerEnabled": [Function],
|
||||||
"isLeftPaneExpanded": [Function],
|
"isLeftPaneExpanded": [Function],
|
||||||
"isLinkInjectionEnabled": [Function],
|
"isLinkInjectionEnabled": [Function],
|
||||||
"isMongoIndexEditorEnabled": [Function],
|
|
||||||
"isNotebookEnabled": [Function],
|
"isNotebookEnabled": [Function],
|
||||||
"isNotebooksEnabledForAccount": [Function],
|
"isNotebooksEnabledForAccount": [Function],
|
||||||
"isNotificationConsoleExpanded": [Function],
|
"isNotificationConsoleExpanded": [Function],
|
||||||
@@ -2254,7 +2237,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isRightPanelV2Enabled": [Function],
|
"isRightPanelV2Enabled": [Function],
|
||||||
"isSchemaEnabled": [Function],
|
"isSchemaEnabled": [Function],
|
||||||
"isServerlessEnabled": [Function],
|
"isServerlessEnabled": [Function],
|
||||||
"isSettingsV2Enabled": [Function],
|
|
||||||
"isSparkEnabled": [Function],
|
"isSparkEnabled": [Function],
|
||||||
"isSparkEnabledForAccount": [Function],
|
"isSparkEnabledForAccount": [Function],
|
||||||
"isSynapseLinkUpdating": [Function],
|
"isSynapseLinkUpdating": [Function],
|
||||||
@@ -2311,7 +2293,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"onSwitchToConnectionString": [Function],
|
"onSwitchToConnectionString": [Function],
|
||||||
"onToggleKeyDown": [Function],
|
"onToggleKeyDown": [Function],
|
||||||
"parentFrameDataExplorerVersion": [Function],
|
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
@@ -2337,7 +2318,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"titleLabel": "Select Columns",
|
"titleLabel": "Select Columns",
|
||||||
"visible": [Function],
|
"visible": [Function],
|
||||||
},
|
},
|
||||||
"quotaId": [Function],
|
|
||||||
"refreshDatabaseAccount": [Function],
|
"refreshDatabaseAccount": [Function],
|
||||||
"refreshNotebookList": [Function],
|
"refreshNotebookList": [Function],
|
||||||
"refreshTreeTitle": [Function],
|
"refreshTreeTitle": [Function],
|
||||||
@@ -2708,8 +2688,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyVisible": [Function],
|
"partitionKeyVisible": [Function],
|
||||||
"requestUnitsUsageCost": [Function],
|
"requestUnitsUsageCost": [Function],
|
||||||
"ruToolTipText": [Function],
|
"ruToolTipText": [Function],
|
||||||
"rupm": [Function],
|
|
||||||
"rupmVisible": [Function],
|
|
||||||
"sharedAutoPilotThroughput": [Function],
|
"sharedAutoPilotThroughput": [Function],
|
||||||
"sharedThroughputRangeText": [Function],
|
"sharedThroughputRangeText": [Function],
|
||||||
"shouldCreateMongoWildcardIndex": [Function],
|
"shouldCreateMongoWildcardIndex": [Function],
|
||||||
@@ -3197,8 +3175,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyVisible": [Function],
|
"partitionKeyVisible": [Function],
|
||||||
"requestUnitsUsageCost": [Function],
|
"requestUnitsUsageCost": [Function],
|
||||||
"ruToolTipText": [Function],
|
"ruToolTipText": [Function],
|
||||||
"rupm": [Function],
|
|
||||||
"rupmVisible": [Function],
|
|
||||||
"sharedAutoPilotThroughput": [Function],
|
"sharedAutoPilotThroughput": [Function],
|
||||||
"sharedThroughputRangeText": [Function],
|
"sharedThroughputRangeText": [Function],
|
||||||
"shouldCreateMongoWildcardIndex": [Function],
|
"shouldCreateMongoWildcardIndex": [Function],
|
||||||
@@ -3310,7 +3286,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"visible": [Function],
|
"visible": [Function],
|
||||||
},
|
},
|
||||||
"arcadiaToken": [Function],
|
"arcadiaToken": [Function],
|
||||||
"armEndpoint": [Function],
|
|
||||||
"browseQueriesPane": BrowseQueriesPane {
|
"browseQueriesPane": BrowseQueriesPane {
|
||||||
"canSaveQueries": [Function],
|
"canSaveQueries": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
@@ -3522,7 +3497,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"hasWriteAccess": [Function],
|
"hasWriteAccess": [Function],
|
||||||
"isAccountReady": [Function],
|
"isAccountReady": [Function],
|
||||||
"isAuthWithResourceToken": [Function],
|
"isAuthWithResourceToken": [Function],
|
||||||
"isCodeOfConductEnabled": [Function],
|
|
||||||
"isCopyNotebookPaneEnabled": [Function],
|
"isCopyNotebookPaneEnabled": [Function],
|
||||||
"isEnableMongoCapabilityPresent": [Function],
|
"isEnableMongoCapabilityPresent": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
@@ -3531,7 +3505,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isHostedDataExplorerEnabled": [Function],
|
"isHostedDataExplorerEnabled": [Function],
|
||||||
"isLeftPaneExpanded": [Function],
|
"isLeftPaneExpanded": [Function],
|
||||||
"isLinkInjectionEnabled": [Function],
|
"isLinkInjectionEnabled": [Function],
|
||||||
"isMongoIndexEditorEnabled": [Function],
|
|
||||||
"isNotebookEnabled": [Function],
|
"isNotebookEnabled": [Function],
|
||||||
"isNotebooksEnabledForAccount": [Function],
|
"isNotebooksEnabledForAccount": [Function],
|
||||||
"isNotificationConsoleExpanded": [Function],
|
"isNotificationConsoleExpanded": [Function],
|
||||||
@@ -3548,7 +3521,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isRightPanelV2Enabled": [Function],
|
"isRightPanelV2Enabled": [Function],
|
||||||
"isSchemaEnabled": [Function],
|
"isSchemaEnabled": [Function],
|
||||||
"isServerlessEnabled": [Function],
|
"isServerlessEnabled": [Function],
|
||||||
"isSettingsV2Enabled": [Function],
|
|
||||||
"isSparkEnabled": [Function],
|
"isSparkEnabled": [Function],
|
||||||
"isSparkEnabledForAccount": [Function],
|
"isSparkEnabledForAccount": [Function],
|
||||||
"isSynapseLinkUpdating": [Function],
|
"isSynapseLinkUpdating": [Function],
|
||||||
@@ -3605,7 +3577,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"onSwitchToConnectionString": [Function],
|
"onSwitchToConnectionString": [Function],
|
||||||
"onToggleKeyDown": [Function],
|
"onToggleKeyDown": [Function],
|
||||||
"parentFrameDataExplorerVersion": [Function],
|
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
@@ -3631,7 +3602,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"titleLabel": "Select Columns",
|
"titleLabel": "Select Columns",
|
||||||
"visible": [Function],
|
"visible": [Function],
|
||||||
},
|
},
|
||||||
"quotaId": [Function],
|
|
||||||
"refreshDatabaseAccount": [Function],
|
"refreshDatabaseAccount": [Function],
|
||||||
"refreshNotebookList": [Function],
|
"refreshNotebookList": [Function],
|
||||||
"refreshTreeTitle": [Function],
|
"refreshTreeTitle": [Function],
|
||||||
@@ -3877,9 +3847,9 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"version": 2,
|
"version": 2,
|
||||||
},
|
},
|
||||||
"partitionKeyProperty": "partitionKey",
|
"partitionKeyProperty": "partitionKey",
|
||||||
"quotaInfo": [Function],
|
|
||||||
"readSettings": [Function],
|
"readSettings": [Function],
|
||||||
"uniqueKeyPolicy": Object {},
|
"uniqueKeyPolicy": Object {},
|
||||||
|
"usageSizeInKB": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
container={
|
container={
|
||||||
@@ -3989,8 +3959,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyVisible": [Function],
|
"partitionKeyVisible": [Function],
|
||||||
"requestUnitsUsageCost": [Function],
|
"requestUnitsUsageCost": [Function],
|
||||||
"ruToolTipText": [Function],
|
"ruToolTipText": [Function],
|
||||||
"rupm": [Function],
|
|
||||||
"rupmVisible": [Function],
|
|
||||||
"sharedAutoPilotThroughput": [Function],
|
"sharedAutoPilotThroughput": [Function],
|
||||||
"sharedThroughputRangeText": [Function],
|
"sharedThroughputRangeText": [Function],
|
||||||
"shouldCreateMongoWildcardIndex": [Function],
|
"shouldCreateMongoWildcardIndex": [Function],
|
||||||
@@ -4478,8 +4446,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"partitionKeyVisible": [Function],
|
"partitionKeyVisible": [Function],
|
||||||
"requestUnitsUsageCost": [Function],
|
"requestUnitsUsageCost": [Function],
|
||||||
"ruToolTipText": [Function],
|
"ruToolTipText": [Function],
|
||||||
"rupm": [Function],
|
|
||||||
"rupmVisible": [Function],
|
|
||||||
"sharedAutoPilotThroughput": [Function],
|
"sharedAutoPilotThroughput": [Function],
|
||||||
"sharedThroughputRangeText": [Function],
|
"sharedThroughputRangeText": [Function],
|
||||||
"shouldCreateMongoWildcardIndex": [Function],
|
"shouldCreateMongoWildcardIndex": [Function],
|
||||||
@@ -4591,7 +4557,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"visible": [Function],
|
"visible": [Function],
|
||||||
},
|
},
|
||||||
"arcadiaToken": [Function],
|
"arcadiaToken": [Function],
|
||||||
"armEndpoint": [Function],
|
|
||||||
"browseQueriesPane": BrowseQueriesPane {
|
"browseQueriesPane": BrowseQueriesPane {
|
||||||
"canSaveQueries": [Function],
|
"canSaveQueries": [Function],
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
@@ -4803,7 +4768,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"hasWriteAccess": [Function],
|
"hasWriteAccess": [Function],
|
||||||
"isAccountReady": [Function],
|
"isAccountReady": [Function],
|
||||||
"isAuthWithResourceToken": [Function],
|
"isAuthWithResourceToken": [Function],
|
||||||
"isCodeOfConductEnabled": [Function],
|
|
||||||
"isCopyNotebookPaneEnabled": [Function],
|
"isCopyNotebookPaneEnabled": [Function],
|
||||||
"isEnableMongoCapabilityPresent": [Function],
|
"isEnableMongoCapabilityPresent": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
@@ -4812,7 +4776,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isHostedDataExplorerEnabled": [Function],
|
"isHostedDataExplorerEnabled": [Function],
|
||||||
"isLeftPaneExpanded": [Function],
|
"isLeftPaneExpanded": [Function],
|
||||||
"isLinkInjectionEnabled": [Function],
|
"isLinkInjectionEnabled": [Function],
|
||||||
"isMongoIndexEditorEnabled": [Function],
|
|
||||||
"isNotebookEnabled": [Function],
|
"isNotebookEnabled": [Function],
|
||||||
"isNotebooksEnabledForAccount": [Function],
|
"isNotebooksEnabledForAccount": [Function],
|
||||||
"isNotificationConsoleExpanded": [Function],
|
"isNotificationConsoleExpanded": [Function],
|
||||||
@@ -4829,7 +4792,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isRightPanelV2Enabled": [Function],
|
"isRightPanelV2Enabled": [Function],
|
||||||
"isSchemaEnabled": [Function],
|
"isSchemaEnabled": [Function],
|
||||||
"isServerlessEnabled": [Function],
|
"isServerlessEnabled": [Function],
|
||||||
"isSettingsV2Enabled": [Function],
|
|
||||||
"isSparkEnabled": [Function],
|
"isSparkEnabled": [Function],
|
||||||
"isSparkEnabledForAccount": [Function],
|
"isSparkEnabledForAccount": [Function],
|
||||||
"isSynapseLinkUpdating": [Function],
|
"isSynapseLinkUpdating": [Function],
|
||||||
@@ -4886,7 +4848,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"onSwitchToConnectionString": [Function],
|
"onSwitchToConnectionString": [Function],
|
||||||
"onToggleKeyDown": [Function],
|
"onToggleKeyDown": [Function],
|
||||||
"parentFrameDataExplorerVersion": [Function],
|
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
@@ -4912,7 +4873,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"titleLabel": "Select Columns",
|
"titleLabel": "Select Columns",
|
||||||
"visible": [Function],
|
"visible": [Function],
|
||||||
},
|
},
|
||||||
"quotaId": [Function],
|
|
||||||
"refreshDatabaseAccount": [Function],
|
"refreshDatabaseAccount": [Function],
|
||||||
"refreshNotebookList": [Function],
|
"refreshNotebookList": [Function],
|
||||||
"refreshTreeTitle": [Function],
|
"refreshTreeTitle": [Function],
|
||||||
|
|||||||
@@ -69,15 +69,15 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
|
|
||||||
<b>
|
<b>
|
||||||
¥
|
¥
|
||||||
1.29
|
1.02
|
||||||
hourly
|
hourly
|
||||||
/
|
/
|
||||||
¥
|
¥
|
||||||
31.06
|
24.48
|
||||||
daily
|
daily
|
||||||
/
|
/
|
||||||
¥
|
¥
|
||||||
944.60
|
744.60
|
||||||
monthly
|
monthly
|
||||||
|
|
||||||
</b>
|
</b>
|
||||||
@@ -227,7 +227,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
, Container:
|
, Container:
|
||||||
sampleCollection
|
sampleCollection
|
||||||
|
|
||||||
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
|
, Current manual throughput: 1000 RU/s
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
id="throughputApplyLongDelayMessage"
|
id="throughputApplyLongDelayMessage"
|
||||||
|
|||||||
@@ -126,6 +126,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-bind="visible: !isAutoPilotSelected()">
|
<div data-bind="visible: !isAutoPilotSelected()">
|
||||||
|
<p>
|
||||||
|
<span
|
||||||
|
>Estimate your required throughput with
|
||||||
|
<a target="_blank" href="https://cosmos.azure.com/capacitycalculator/">capacity calculator</a></span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
<div data-bind="setTemplateReady: true">
|
<div data-bind="setTemplateReady: true">
|
||||||
<input
|
<input
|
||||||
data-bind="
|
data-bind="
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPa
|
|||||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||||
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
||||||
import EnvironmentUtility from "../Common/EnvironmentUtility";
|
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
|
||||||
import GraphStylingPane from "./Panes/GraphStylingPane";
|
import GraphStylingPane from "./Panes/GraphStylingPane";
|
||||||
import hasher from "hasher";
|
import hasher from "hasher";
|
||||||
import NewVertexPane from "./Panes/NewVertexPane";
|
import NewVertexPane from "./Panes/NewVertexPane";
|
||||||
@@ -121,7 +121,6 @@ export default class Explorer {
|
|||||||
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
|
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
|
||||||
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
|
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
|
||||||
public subscriptionType: ko.Observable<SubscriptionType>;
|
public subscriptionType: ko.Observable<SubscriptionType>;
|
||||||
public quotaId: ko.Observable<string>;
|
|
||||||
public defaultExperience: ko.Observable<string>;
|
public defaultExperience: ko.Observable<string>;
|
||||||
public isPreferredApiDocumentDB: ko.Computed<boolean>;
|
public isPreferredApiDocumentDB: ko.Computed<boolean>;
|
||||||
public isPreferredApiCassandra: ko.Computed<boolean>;
|
public isPreferredApiCassandra: ko.Computed<boolean>;
|
||||||
@@ -134,13 +133,10 @@ export default class Explorer {
|
|||||||
public isAccountReady: ko.Observable<boolean>;
|
public isAccountReady: ko.Observable<boolean>;
|
||||||
public canSaveQueries: ko.Computed<boolean>;
|
public canSaveQueries: ko.Computed<boolean>;
|
||||||
public features: ko.Observable<any>;
|
public features: ko.Observable<any>;
|
||||||
public serverId: ko.Observable<string>;
|
|
||||||
public armEndpoint: ko.Observable<string>;
|
|
||||||
public isTryCosmosDBSubscription: ko.Observable<boolean>;
|
public isTryCosmosDBSubscription: ko.Observable<boolean>;
|
||||||
public queriesClient: QueriesClient;
|
public queriesClient: QueriesClient;
|
||||||
public tableDataClient: TableDataClient;
|
public tableDataClient: TableDataClient;
|
||||||
public splitter: Splitter;
|
public splitter: Splitter;
|
||||||
public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>("");
|
|
||||||
public mostRecentActivity: MostRecentActivity.MostRecentActivity;
|
public mostRecentActivity: MostRecentActivity.MostRecentActivity;
|
||||||
|
|
||||||
// Notification Console
|
// Notification Console
|
||||||
@@ -204,10 +200,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
// features
|
// features
|
||||||
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||||
public isCodeOfConductEnabled: ko.Computed<boolean>;
|
|
||||||
public isLinkInjectionEnabled: ko.Computed<boolean>;
|
public isLinkInjectionEnabled: ko.Computed<boolean>;
|
||||||
public isSettingsV2Enabled: ko.Observable<boolean>;
|
|
||||||
public isMongoIndexEditorEnabled: ko.Observable<boolean>;
|
|
||||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||||
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||||
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
|
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
|
||||||
@@ -281,7 +274,6 @@ export default class Explorer {
|
|||||||
|
|
||||||
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
|
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
|
||||||
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
|
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
|
||||||
this.quotaId = ko.observable<string>("");
|
|
||||||
let firstInitialization = true;
|
let firstInitialization = true;
|
||||||
this.isRefreshingExplorer = ko.observable<boolean>(true);
|
this.isRefreshingExplorer = ko.observable<boolean>(true);
|
||||||
this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => {
|
this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => {
|
||||||
@@ -321,9 +313,9 @@ export default class Explorer {
|
|||||||
if (isAccountReady) {
|
if (isAccountReady) {
|
||||||
this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true);
|
this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true);
|
||||||
RouteHandler.getInstance().initHandler();
|
RouteHandler.getInstance().initHandler();
|
||||||
this.notebookWorkspaceManager = new NotebookWorkspaceManager(this.armEndpoint());
|
this.notebookWorkspaceManager = new NotebookWorkspaceManager();
|
||||||
this.arcadiaWorkspaces = ko.observableArray();
|
this.arcadiaWorkspaces = ko.observableArray();
|
||||||
this._arcadiaManager = new ArcadiaResourceManager(this.armEndpoint());
|
this._arcadiaManager = new ArcadiaResourceManager();
|
||||||
this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then(isRegistered =>
|
this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then(isRegistered =>
|
||||||
this.hasStorageAnalyticsAfecFeature(isRegistered)
|
this.hasStorageAnalyticsAfecFeature(isRegistered)
|
||||||
);
|
);
|
||||||
@@ -372,8 +364,6 @@ export default class Explorer {
|
|||||||
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
|
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
|
||||||
|
|
||||||
this.features = ko.observable();
|
this.features = ko.observable();
|
||||||
this.serverId = ko.observable<string>();
|
|
||||||
this.armEndpoint = ko.observable<string>(undefined);
|
|
||||||
this.queriesClient = new QueriesClient(this);
|
this.queriesClient = new QueriesClient(this);
|
||||||
this.isTryCosmosDBSubscription = ko.observable<boolean>(false);
|
this.isTryCosmosDBSubscription = ko.observable<boolean>(false);
|
||||||
|
|
||||||
@@ -406,14 +396,9 @@ export default class Explorer {
|
|||||||
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
|
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
|
||||||
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
||||||
);
|
);
|
||||||
this.isCodeOfConductEnabled = ko.computed<boolean>(() =>
|
|
||||||
this.isFeatureEnabled(Constants.Features.enableCodeOfConduct)
|
|
||||||
);
|
|
||||||
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
|
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
|
||||||
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
||||||
);
|
);
|
||||||
this.isSettingsV2Enabled = ko.observable(false);
|
|
||||||
this.isMongoIndexEditorEnabled = ko.observable(false);
|
|
||||||
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
||||||
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||||
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
|
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||||
@@ -1020,9 +1005,7 @@ export default class Explorer {
|
|||||||
this.isSynapseLinkUpdating(true);
|
this.isSynapseLinkUpdating(true);
|
||||||
this._closeSynapseLinkModalDialog();
|
this._closeSynapseLinkModalDialog();
|
||||||
|
|
||||||
const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(
|
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(this.databaseAccount().id);
|
||||||
this.databaseAccount().id
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync(
|
const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync(
|
||||||
@@ -1734,6 +1717,7 @@ export default class Explorer {
|
|||||||
case MessageTypes.SendNotification:
|
case MessageTypes.SendNotification:
|
||||||
case MessageTypes.ClearNotification:
|
case MessageTypes.ClearNotification:
|
||||||
case MessageTypes.LoadingStatus:
|
case MessageTypes.LoadingStatus:
|
||||||
|
case MessageTypes.InitTestExplorer:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1761,61 +1745,59 @@ export default class Explorer {
|
|||||||
inputs.extensionEndpoint = configContext.PROXY_PATH;
|
inputs.extensionEndpoint = configContext.PROXY_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initPromise: Q.Promise<void> = inputs ? this.initDataExplorerWithFrameInputs(inputs) : Q();
|
this.initDataExplorerWithFrameInputs(inputs);
|
||||||
|
|
||||||
initPromise.then(() => {
|
const openAction: ActionContracts.DataExplorerAction = message.openAction;
|
||||||
const openAction: ActionContracts.DataExplorerAction = message.openAction;
|
if (!!openAction) {
|
||||||
if (!!openAction) {
|
if (this.isRefreshingExplorer()) {
|
||||||
if (this.isRefreshingExplorer()) {
|
const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => {
|
||||||
const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => {
|
|
||||||
handleOpenAction(openAction, this.nonSystemDatabases(), this);
|
|
||||||
subscription.dispose();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
handleOpenAction(openAction, this.nonSystemDatabases(), this);
|
handleOpenAction(openAction, this.nonSystemDatabases(), this);
|
||||||
}
|
subscription.dispose();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
handleOpenAction(openAction, this.nonSystemDatabases(), this);
|
||||||
}
|
}
|
||||||
if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
|
}
|
||||||
handleCachedDataMessage(message);
|
if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
|
||||||
return;
|
handleCachedDataMessage(message);
|
||||||
}
|
return;
|
||||||
if (message.type) {
|
}
|
||||||
switch (message.type) {
|
if (message.type) {
|
||||||
case MessageTypes.UpdateLocationHash:
|
switch (message.type) {
|
||||||
if (!message.locationHash) {
|
case MessageTypes.UpdateLocationHash:
|
||||||
break;
|
if (!message.locationHash) {
|
||||||
}
|
break;
|
||||||
hasher.replaceHash(message.locationHash);
|
}
|
||||||
RouteHandler.getInstance().parseHash(message.locationHash);
|
hasher.replaceHash(message.locationHash);
|
||||||
break;
|
RouteHandler.getInstance().parseHash(message.locationHash);
|
||||||
case MessageTypes.SendNotification:
|
break;
|
||||||
if (!message.message) {
|
case MessageTypes.SendNotification:
|
||||||
break;
|
if (!message.message) {
|
||||||
}
|
break;
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
}
|
||||||
message.consoleDataType || ConsoleDataType.Info,
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
message.message,
|
message.consoleDataType || ConsoleDataType.Info,
|
||||||
message.id
|
message.message,
|
||||||
);
|
message.id
|
||||||
break;
|
);
|
||||||
case MessageTypes.ClearNotification:
|
break;
|
||||||
if (!message.id) {
|
case MessageTypes.ClearNotification:
|
||||||
break;
|
if (!message.id) {
|
||||||
}
|
break;
|
||||||
NotificationConsoleUtils.clearInProgressMessageWithId(message.id);
|
}
|
||||||
break;
|
NotificationConsoleUtils.clearInProgressMessageWithId(message.id);
|
||||||
case MessageTypes.LoadingStatus:
|
break;
|
||||||
if (!message.text) {
|
case MessageTypes.LoadingStatus:
|
||||||
break;
|
if (!message.text) {
|
||||||
}
|
break;
|
||||||
this._setLoadingStatusText(message.text, message.title);
|
}
|
||||||
break;
|
this._setLoadingStatusText(message.text, message.title);
|
||||||
}
|
break;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.splashScreenAdapter.forceRender();
|
this.splashScreenAdapter.forceRender();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findSelectedDatabase(): ViewModels.Database {
|
public findSelectedDatabase(): ViewModels.Database {
|
||||||
@@ -1855,8 +1837,14 @@ export default class Explorer {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): Q.Promise<void> {
|
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): void {
|
||||||
if (inputs != null) {
|
if (inputs != null) {
|
||||||
|
// In development mode, save the iframe message from the portal in session storage.
|
||||||
|
// This allows webpack hot reload to funciton properly
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
const authorizationToken = inputs.authorizationToken || "";
|
const authorizationToken = inputs.authorizationToken || "";
|
||||||
const masterKey = inputs.masterKey || "";
|
const masterKey = inputs.masterKey || "";
|
||||||
const databaseAccount = inputs.databaseAccount || null;
|
const databaseAccount = inputs.databaseAccount || null;
|
||||||
@@ -1864,26 +1852,19 @@ export default class Explorer {
|
|||||||
this.collectionCreationDefaults = inputs.defaultCollectionThroughput;
|
this.collectionCreationDefaults = inputs.defaultCollectionThroughput;
|
||||||
}
|
}
|
||||||
this.features(inputs.features);
|
this.features(inputs.features);
|
||||||
this.serverId(inputs.serverId);
|
|
||||||
this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || configContext.ARM_ENDPOINT));
|
|
||||||
this.databaseAccount(databaseAccount);
|
this.databaseAccount(databaseAccount);
|
||||||
this.subscriptionType(inputs.subscriptionType);
|
this.subscriptionType(inputs.subscriptionType);
|
||||||
this.quotaId(inputs.quotaId);
|
|
||||||
this.hasWriteAccess(inputs.hasWriteAccess);
|
this.hasWriteAccess(inputs.hasWriteAccess);
|
||||||
this.flight(inputs.addCollectionDefaultFlight);
|
this.flight(inputs.addCollectionDefaultFlight);
|
||||||
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription);
|
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription);
|
||||||
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken);
|
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken);
|
||||||
this.setFeatureFlagsFromFlights(inputs.flights);
|
this.setFeatureFlagsFromFlights(inputs.flights);
|
||||||
|
|
||||||
if (!!inputs.dataExplorerVersion) {
|
|
||||||
this.parentFrameDataExplorerVersion(inputs.dataExplorerVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._importExplorerConfigComplete = true;
|
this._importExplorerConfigComplete = true;
|
||||||
|
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
BACKEND_ENDPOINT: inputs.extensionEndpoint || "",
|
BACKEND_ENDPOINT: inputs.extensionEndpoint || "",
|
||||||
ARM_ENDPOINT: this.armEndpoint()
|
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
||||||
|
serverId: inputs.serverId
|
||||||
});
|
});
|
||||||
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
@@ -1892,7 +1873,8 @@ export default class Explorer {
|
|||||||
databaseAccount,
|
databaseAccount,
|
||||||
resourceGroup: inputs.resourceGroup,
|
resourceGroup: inputs.resourceGroup,
|
||||||
subscriptionId: inputs.subscriptionId,
|
subscriptionId: inputs.subscriptionId,
|
||||||
subscriptionType: inputs.subscriptionType
|
subscriptionType: inputs.subscriptionType,
|
||||||
|
quotaId: inputs.quotaId
|
||||||
});
|
});
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.LoadDatabaseAccount,
|
Action.LoadDatabaseAccount,
|
||||||
@@ -1906,21 +1888,12 @@ export default class Explorer {
|
|||||||
|
|
||||||
this.isAccountReady(true);
|
this.isAccountReady(true);
|
||||||
}
|
}
|
||||||
return Q();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public setFeatureFlagsFromFlights(flights: readonly string[]): void {
|
public setFeatureFlagsFromFlights(flights: readonly string[]): void {
|
||||||
if (!flights) {
|
if (!flights) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flights.indexOf(Constants.Flights.SettingsV2) !== -1) {
|
|
||||||
this.isSettingsV2Enabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flights.indexOf(Constants.Flights.MongoIndexEditor) !== -1) {
|
|
||||||
this.isMongoIndexEditorEnabled(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findSelectedCollection(): ViewModels.Collection {
|
public findSelectedCollection(): ViewModels.Collection {
|
||||||
@@ -1978,9 +1951,9 @@ export default class Explorer {
|
|||||||
|
|
||||||
public isRunningOnNationalCloud(): boolean {
|
public isRunningOnNationalCloud(): boolean {
|
||||||
return (
|
return (
|
||||||
this.serverId() === Constants.ServerIds.blackforest ||
|
userContext === Constants.ServerIds.blackforest ||
|
||||||
this.serverId() === Constants.ServerIds.fairfax ||
|
userContext === Constants.ServerIds.fairfax ||
|
||||||
this.serverId() === Constants.ServerIds.mooncake
|
userContext === Constants.ServerIds.mooncake
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2287,7 +2260,6 @@ export default class Explorer {
|
|||||||
name,
|
name,
|
||||||
content,
|
content,
|
||||||
parentDomElement,
|
parentDomElement,
|
||||||
this.isCodeOfConductEnabled(),
|
|
||||||
this.isLinkInjectionEnabled()
|
this.isLinkInjectionEnabled()
|
||||||
);
|
);
|
||||||
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
||||||
@@ -2379,11 +2351,13 @@ export default class Explorer {
|
|||||||
this.tabsManager.activateTab(notebookTab);
|
this.tabsManager.activateTab(notebookTab);
|
||||||
} else {
|
} else {
|
||||||
const options: NotebookTabOptions = {
|
const options: NotebookTabOptions = {
|
||||||
|
account: userContext.databaseAccount,
|
||||||
tabKind: ViewModels.CollectionTabKind.NotebookV2,
|
tabKind: ViewModels.CollectionTabKind.NotebookV2,
|
||||||
node: null,
|
node: null,
|
||||||
title: notebookContentItem.name,
|
title: notebookContentItem.name,
|
||||||
tabPath: notebookContentItem.path,
|
tabPath: notebookContentItem.path,
|
||||||
collection: null,
|
collection: null,
|
||||||
|
masterKey: userContext.masterKey || "",
|
||||||
hashLocation: "notebooks",
|
hashLocation: "notebooks",
|
||||||
isActive: ko.observable(false),
|
isActive: ko.observable(false),
|
||||||
isTabsContentExpanded: ko.observable(true),
|
isTabsContentExpanded: ko.observable(true),
|
||||||
@@ -2576,7 +2550,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
public _refreshSparkEnabledStateForAccount = async (): Promise<void> => {
|
public _refreshSparkEnabledStateForAccount = async (): Promise<void> => {
|
||||||
const subscriptionId = userContext.subscriptionId;
|
const subscriptionId = userContext.subscriptionId;
|
||||||
const armEndpoint = this.armEndpoint();
|
const armEndpoint = configContext.ARM_ENDPOINT;
|
||||||
const authType = window.authType as AuthType;
|
const authType = window.authType as AuthType;
|
||||||
if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
|
if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
|
||||||
// explorer is not aware of the database account yet
|
// explorer is not aware of the database account yet
|
||||||
@@ -2585,7 +2559,7 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${Constants.AfecFeatures.Spark}`;
|
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${Constants.AfecFeatures.Spark}`;
|
||||||
const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(featureUri);
|
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri);
|
||||||
try {
|
try {
|
||||||
const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync(
|
const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync(
|
||||||
featureUri,
|
featureUri,
|
||||||
@@ -2605,7 +2579,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => {
|
public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => {
|
||||||
const subscriptionId = userContext.subscriptionId;
|
const subscriptionId = userContext.subscriptionId;
|
||||||
const armEndpoint = this.armEndpoint();
|
const armEndpoint = configContext.ARM_ENDPOINT;
|
||||||
const authType = window.authType as AuthType;
|
const authType = window.authType as AuthType;
|
||||||
if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
|
if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
|
||||||
// explorer is not aware of the database account yet
|
// explorer is not aware of the database account yet
|
||||||
@@ -2613,7 +2587,7 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${featureName}`;
|
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${featureName}`;
|
||||||
const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(featureUri);
|
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri);
|
||||||
try {
|
try {
|
||||||
const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync(
|
const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync(
|
||||||
featureUri,
|
featureUri,
|
||||||
|
|||||||
@@ -99,7 +99,22 @@
|
|||||||
.notificationConsoleControls {
|
.notificationConsoleControls {
|
||||||
padding: @MediumSpace;
|
padding: @MediumSpace;
|
||||||
margin-left:@DefaultSpace;
|
margin-left:@DefaultSpace;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.ms-Dropdown-container {
|
||||||
|
display: flex;
|
||||||
|
.ms-Dropdown-title {
|
||||||
|
height: 25px;
|
||||||
|
line-height: 25px;
|
||||||
|
}
|
||||||
|
.ms-Dropdown {
|
||||||
|
min-width: 110px;
|
||||||
|
margin-left: 10px;
|
||||||
|
height: 25px;
|
||||||
|
line-height: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
#consoleFilterLabel {
|
#consoleFilterLabel {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
@@ -107,6 +122,7 @@
|
|||||||
.consoleSplitter {
|
.consoleSplitter {
|
||||||
border-left: 1px solid @BaseMedium;
|
border-left: 1px solid @BaseMedium;
|
||||||
margin: @MediumSpace;
|
margin: @MediumSpace;
|
||||||
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clearNotificationsButton {
|
.clearNotificationsButton {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
|
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
|
||||||
import AnimateHeight from "react-animate-height";
|
import AnimateHeight from "react-animate-height";
|
||||||
|
import { Dropdown, IDropdownOption } from "office-ui-fabric-react";
|
||||||
import LoadingIcon from "../../../../images/loading.svg";
|
import LoadingIcon from "../../../../images/loading.svg";
|
||||||
import ErrorBlackIcon from "../../../../images/error_black.svg";
|
import ErrorBlackIcon from "../../../../images/error_black.svg";
|
||||||
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
|
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
|
||||||
@@ -53,7 +53,12 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
NotificationConsoleComponentState
|
NotificationConsoleComponentState
|
||||||
> {
|
> {
|
||||||
private static readonly transitionDurationMs = 200;
|
private static readonly transitionDurationMs = 200;
|
||||||
private static readonly FilterOptions = ["All", "In Progress", "Info", "Error"];
|
private static readonly FilterOptions = [
|
||||||
|
{ key: "All", text: "All" },
|
||||||
|
{ key: "In Progress", text: "In progress" },
|
||||||
|
{ key: "Info", text: "Info" },
|
||||||
|
{ key: "Error", text: "Error" }
|
||||||
|
];
|
||||||
private headerTimeoutId: number;
|
private headerTimeoutId: number;
|
||||||
private prevHeaderStatus: string;
|
private prevHeaderStatus: string;
|
||||||
private consoleHeaderElement: HTMLElement;
|
private consoleHeaderElement: HTMLElement;
|
||||||
@@ -62,7 +67,7 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
headerStatus: "",
|
headerStatus: "",
|
||||||
selectedFilter: NotificationConsoleComponent.FilterOptions[0],
|
selectedFilter: NotificationConsoleComponent.FilterOptions[0].key || "",
|
||||||
isExpanded: props.isConsoleExpanded
|
isExpanded: props.isConsoleExpanded
|
||||||
};
|
};
|
||||||
this.prevHeaderStatus = null;
|
this.prevHeaderStatus = null;
|
||||||
@@ -150,20 +155,15 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
>
|
>
|
||||||
<div className="notificationConsoleContents">
|
<div className="notificationConsoleContents">
|
||||||
<div className="notificationConsoleControls">
|
<div className="notificationConsoleControls">
|
||||||
<label id="consoleFilterLabel">Filter</label>
|
<Dropdown
|
||||||
<select
|
label="Filter:"
|
||||||
aria-labelledby="consoleFilterLabel"
|
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-label={this.state.selectedFilter}
|
selectedKey={this.state.selectedFilter}
|
||||||
value={this.state.selectedFilter}
|
options={NotificationConsoleComponent.FilterOptions}
|
||||||
onChange={this.onFilterSelected.bind(this)}
|
onChange={this.onFilterSelected.bind(this)}
|
||||||
>
|
aria-labelledby="consoleFilterLabel"
|
||||||
{NotificationConsoleComponent.FilterOptions.map((value: string) => (
|
aria-label={this.state.selectedFilter}
|
||||||
<option value={value} key={value}>
|
/>
|
||||||
{value}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<span className="consoleSplitter" />
|
<span className="consoleSplitter" />
|
||||||
<span
|
<span
|
||||||
className="clearNotificationsButton"
|
className="clearNotificationsButton"
|
||||||
@@ -220,8 +220,8 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private onFilterSelected(event: React.ChangeEvent<HTMLSelectElement>): void {
|
private onFilterSelected(event: React.ChangeEvent<HTMLSelectElement>, option: IDropdownOption): void {
|
||||||
this.setState({ selectedFilter: event.target.value });
|
this.setState({ selectedFilter: String(option.key) });
|
||||||
}
|
}
|
||||||
|
|
||||||
private getFilteredConsoleData(): ConsoleData[] {
|
private getFilteredConsoleData(): ConsoleData[] {
|
||||||
|
|||||||
@@ -110,43 +110,34 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
|
|||||||
<div
|
<div
|
||||||
className="notificationConsoleControls"
|
className="notificationConsoleControls"
|
||||||
>
|
>
|
||||||
<label
|
<StyledWithResponsiveMode
|
||||||
id="consoleFilterLabel"
|
|
||||||
>
|
|
||||||
Filter
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
aria-label="All"
|
aria-label="All"
|
||||||
aria-labelledby="consoleFilterLabel"
|
aria-labelledby="consoleFilterLabel"
|
||||||
|
label="Filter:"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
options={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"key": "All",
|
||||||
|
"text": "All",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "In Progress",
|
||||||
|
"text": "In progress",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "Info",
|
||||||
|
"text": "Info",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "Error",
|
||||||
|
"text": "Error",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
role="combobox"
|
role="combobox"
|
||||||
value="All"
|
selectedKey="All"
|
||||||
>
|
/>
|
||||||
<option
|
|
||||||
key="All"
|
|
||||||
value="All"
|
|
||||||
>
|
|
||||||
All
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
key="In Progress"
|
|
||||||
value="In Progress"
|
|
||||||
>
|
|
||||||
In Progress
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
key="Info"
|
|
||||||
value="Info"
|
|
||||||
>
|
|
||||||
Info
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
key="Error"
|
|
||||||
value="Error"
|
|
||||||
>
|
|
||||||
Error
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<span
|
<span
|
||||||
className="consoleSplitter"
|
className="consoleSplitter"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
.mongoQueryComponent {
|
|
||||||
margin-left: 10px;
|
|
||||||
|
|
||||||
input {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
padding: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
label:before {
|
|
||||||
top: 2px;
|
|
||||||
left: 2px;
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queryInput {
|
|
||||||
border: 1px solid black;
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
import MonacoEditor from "@nteract/monaco-editor";
|
|
||||||
import { PrimaryButton } from "office-ui-fabric-react";
|
|
||||||
import { ChoiceGroup, IChoiceGroupOption } from "office-ui-fabric-react/lib/ChoiceGroup";
|
|
||||||
import Outputs from "@nteract/stateful-components/lib/outputs";
|
|
||||||
import { KernelOutputError, StreamText } from "@nteract/outputs";
|
|
||||||
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
|
|
||||||
import { actions, selectors, AppState, ContentRef, KernelRef } from "@nteract/core";
|
|
||||||
import loadTransform from "../NotebookComponent/loadTransform";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import Immutable from "immutable";
|
|
||||||
|
|
||||||
import "./MongoQueryComponent.less";
|
|
||||||
interface MongoQueryComponentPureProps {
|
|
||||||
contentRef: ContentRef;
|
|
||||||
kernelRef: KernelRef;
|
|
||||||
databaseId: string;
|
|
||||||
collectionId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MongoQueryComponentDispatchProps {
|
|
||||||
runCell: (contentRef: ContentRef, cellId: string) => void;
|
|
||||||
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void;
|
|
||||||
onChange: (text: string, id: string, contentRef: ContentRef) => void;
|
|
||||||
save: (contentRef: ContentRef) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutputType = "rich" | "json";
|
|
||||||
|
|
||||||
interface MongoQueryComponentState {
|
|
||||||
outputType: OutputType;
|
|
||||||
selectedId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: IChoiceGroupOption[] = [
|
|
||||||
{ key: "rich", text: "Rich Output" },
|
|
||||||
{ key: "json", text: "Json Output" }
|
|
||||||
];
|
|
||||||
|
|
||||||
interface MongoKernelJsonOutput {
|
|
||||||
results: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MongoDocument {
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type MongoQueryComponentProps = MongoQueryComponentPureProps & StateProps & MongoQueryComponentDispatchProps;
|
|
||||||
export class MongoQueryComponent extends React.Component<MongoQueryComponentProps, MongoQueryComponentState> {
|
|
||||||
constructor(props: MongoQueryComponentProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
outputType: "json",
|
|
||||||
selectedId: undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
loadTransform(this.props);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onExecute = () => {
|
|
||||||
this.props.runCell(this.props.contentRef, this.props.firstCellId);
|
|
||||||
this.props.save(this.props.contentRef);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param databaseId
|
|
||||||
* @param collectionId
|
|
||||||
* @param query e.g. { "lastName": { $in: ["Andersen"] } }
|
|
||||||
*/
|
|
||||||
private createFilterQuery(databaseId: string, collectionId: string, query: string): string {
|
|
||||||
const newCommand = `{ "command": "filter", "database": "${databaseId}", "collection": "${collectionId}", "filter": ${JSON.stringify(query)}, "outputType": "${this.state.outputType}" }`;
|
|
||||||
return newCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onOutputTypeChange = (e: React.FormEvent<HTMLElement | HTMLInputElement>, option: IChoiceGroupOption): void => {
|
|
||||||
const outputType = option.key as OutputType;
|
|
||||||
this.setState({ outputType }, () => this.onInputChange(this.props.inputValue));
|
|
||||||
};
|
|
||||||
|
|
||||||
private onInputChange = (text: string) => {
|
|
||||||
this.props.onChange(this.createFilterQuery(this.props.databaseId, this.props.collectionId, text),
|
|
||||||
this.props.firstCellId, this.props.contentRef);
|
|
||||||
};
|
|
||||||
|
|
||||||
render(): JSX.Element {
|
|
||||||
const { firstCellId: id, contentRef, outputDocuments } = this.props;
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mongoQueryComponent">
|
|
||||||
<div className="queryInput">
|
|
||||||
<MonacoEditor id={this.props.firstCellId} contentRef={this.props.contentRef} theme={""}
|
|
||||||
language="json" onChange={this.onInputChange}
|
|
||||||
value={this.props.inputValue} />
|
|
||||||
</div>
|
|
||||||
<PrimaryButton text="Apply" onClick={this.onExecute} disabled={!this.props.firstCellId} />
|
|
||||||
<ChoiceGroup
|
|
||||||
selectedKey={this.state.outputType}
|
|
||||||
options={options}
|
|
||||||
onChange={this.onOutputTypeChange}
|
|
||||||
label="Output Type"
|
|
||||||
styles={{ input: { marginTop: 0 }, root: { marginTop: 0 } }}
|
|
||||||
/>
|
|
||||||
<hr />
|
|
||||||
<div style={ { display: "flex" } }>
|
|
||||||
<ul>
|
|
||||||
{outputDocuments && outputDocuments.map(d => (
|
|
||||||
<li key={d.id}>
|
|
||||||
<a onClick={() => this.setState({ selectedId: id })}>{d.id}</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<div style={{ width: "100%" }} >
|
|
||||||
<MonacoEditor id={""} contentRef={""} theme={""} language="json" onChange={() => {}}
|
|
||||||
value={JSON.stringify(outputDocuments.find(doc => doc.id ===this.state.selectedId)) ?? ""} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<Outputs id={id} contentRef={contentRef}>
|
|
||||||
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
|
|
||||||
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
|
|
||||||
<KernelOutputError />
|
|
||||||
<StreamText />
|
|
||||||
</Outputs>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
firstCellId: string;
|
|
||||||
inputValue: string;
|
|
||||||
outputDocuments: MongoDocument[];
|
|
||||||
}
|
|
||||||
interface InitialProps {
|
|
||||||
contentRef: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redux
|
|
||||||
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
|
|
||||||
const { contentRef } = initialProps;
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
|
||||||
let firstCellId;
|
|
||||||
let inputValue = "";
|
|
||||||
let outputDocuments = [];
|
|
||||||
const content = selectors.content(state, { contentRef });
|
|
||||||
if (content?.type === "notebook") {
|
|
||||||
const cellOrder = selectors.notebook.cellOrder(content.model);
|
|
||||||
if (cellOrder.size > 0) {
|
|
||||||
firstCellId = cellOrder.first() as string;
|
|
||||||
const cell = selectors.notebook.cellById(content.model, { id: firstCellId });
|
|
||||||
|
|
||||||
// Parse to extract filter and output type
|
|
||||||
const cellValue = cell.get("source", "");
|
|
||||||
if (cellValue) {
|
|
||||||
try {
|
|
||||||
const filterValue = JSON.parse(cellValue).filter;
|
|
||||||
if (filterValue) {
|
|
||||||
inputValue = filterValue;
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
console.error("Could not parse", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const outputs = cell.get("outputs", Immutable.List());
|
|
||||||
// Extract "application/json" mime-type
|
|
||||||
let jsonOutput: MongoKernelJsonOutput;
|
|
||||||
for (const output of outputs) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(output.data, "application/json")) {
|
|
||||||
jsonOutput = output.data["application/json"];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outputDocuments = jsonOutput?.results ?? [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
firstCellId,
|
|
||||||
inputValue,
|
|
||||||
outputDocuments
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: MongoQueryComponentProps) => {
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => {
|
|
||||||
return {
|
|
||||||
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
|
|
||||||
return dispatch(
|
|
||||||
actions.addTransform({
|
|
||||||
mediaType: transform.MIMETYPE,
|
|
||||||
component: transform
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
runCell: (contentRef: ContentRef, cellId: string) => {
|
|
||||||
return dispatch(
|
|
||||||
actions.executeCell({
|
|
||||||
contentRef,
|
|
||||||
id: cellId
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onChange: (text: string, id: string, contentRef: ContentRef) => {
|
|
||||||
dispatch(actions.updateCellSource({ id, contentRef, value: text }));
|
|
||||||
},
|
|
||||||
save: (contentRef: ContentRef) => {
|
|
||||||
dispatch(actions.save({ contentRef }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return mapDispatchToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, makeMapDispatchToProps)(MongoQueryComponent);
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
|
||||||
import {
|
|
||||||
NotebookComponentBootstrapper,
|
|
||||||
NotebookComponentBootstrapperOptions
|
|
||||||
} from "../NotebookComponent/NotebookComponentBootstrapper";
|
|
||||||
import MongoQueryComponent from "../MongoQueryComponent/MongoQueryComponent";
|
|
||||||
import { actions, createContentRef, createKernelRef, KernelRef } from "@nteract/core";
|
|
||||||
import { Provider } from "react-redux";
|
|
||||||
|
|
||||||
export class MongoQueryComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
|
|
||||||
public parameters: unknown;
|
|
||||||
private kernelRef: KernelRef;
|
|
||||||
|
|
||||||
constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) {
|
|
||||||
super(options);
|
|
||||||
|
|
||||||
if (!this.contentRef) {
|
|
||||||
this.contentRef = createContentRef();
|
|
||||||
this.kernelRef = createKernelRef();
|
|
||||||
|
|
||||||
// Request fetching notebook content
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.fetchContent({
|
|
||||||
filepath: "mongo.ipynb",
|
|
||||||
params: {},
|
|
||||||
kernelRef: this.kernelRef,
|
|
||||||
contentRef: this.contentRef
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
|
||||||
const props = {
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
kernelRef: this.kernelRef,
|
|
||||||
databaseId: this.databaseId,
|
|
||||||
collectionId: this.collectionId
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Provider store={this.getStore()}>
|
|
||||||
<MongoQueryComponent {...props} />;
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -128,17 +128,9 @@ export default class NotebookManager {
|
|||||||
name: string,
|
name: string,
|
||||||
content: string | ImmutableNotebook,
|
content: string | ImmutableNotebook,
|
||||||
parentDomElement: HTMLElement,
|
parentDomElement: HTMLElement,
|
||||||
isCodeOfConductEnabled: boolean,
|
|
||||||
isLinkInjectionEnabled: boolean
|
isLinkInjectionEnabled: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.publishNotebookPaneAdapter.open(
|
await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement, isLinkInjectionEnabled);
|
||||||
name,
|
|
||||||
getFullName(),
|
|
||||||
content,
|
|
||||||
parentDomElement,
|
|
||||||
isCodeOfConductEnabled,
|
|
||||||
isLinkInjectionEnabled
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public openCopyNotebookPane(name: string, content: string): void {
|
public openCopyNotebookPane(name: string, content: string): void {
|
||||||
|
|||||||
@@ -243,38 +243,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Unlimited Button Content - Start -->
|
<!-- Unlimited Button Content - Start -->
|
||||||
<div class="tabcontent" data-bind="visible: isUnlimitedStorageSelected() || databaseHasSharedOffer()">
|
<div class="tabcontent" data-bind="visible: isUnlimitedStorageSelected() || databaseHasSharedOffer()">
|
||||||
<div data-bind="visible: rupmVisible">
|
|
||||||
<div class="tabs">
|
|
||||||
<p>
|
|
||||||
<span class="mandatoryStar">*</span>
|
|
||||||
<span class="addCollectionLabel">RU/m</span>
|
|
||||||
<span class="infoTooltip" role="tooltip" tabindex="0">
|
|
||||||
<img class="infoImg" src="/info-bubble.svg" alt="More information">
|
|
||||||
<span class="tooltiptext throughputRuInfo">
|
|
||||||
For each 100 Request Units per second (RU/s) provisioned, 1,000 Request Units
|
|
||||||
per
|
|
||||||
minute
|
|
||||||
(RU/m) can be provisioned. E.g.: for a container with 5,000 RU/s provisioned
|
|
||||||
with
|
|
||||||
RU/m
|
|
||||||
enabled, the RU/m budget will be 50,000 RU/m.
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<div tabindex="0" data-bind="event: { keydown: onRupmOptionsKeyDown }" aria-label="RU/m">
|
|
||||||
<div class="tab">
|
|
||||||
<input type="radio" id="rupmOn2" name="rupmcoll2" value="on" class="radio"
|
|
||||||
data-bind="checked: rupm">
|
|
||||||
<label for="rupmOn2">ON</label>
|
|
||||||
</div>
|
|
||||||
<div class="tab">
|
|
||||||
<input type="radio" id="rupmOff2" name="rupmcoll2" value="off" class="radio"
|
|
||||||
data-bind="checked: rupm">
|
|
||||||
<label for="rupmOff2">OFF</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div data-bind="visible: partitionKeyVisible">
|
<div data-bind="visible: partitionKeyVisible">
|
||||||
<p>
|
<p>
|
||||||
<span class="mandatoryStar">*</span>
|
<span class="mandatoryStar">*</span>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
|
|||||||
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
|
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
|
||||||
import { createCollection } from "../../Common/dataAccess/createCollection";
|
import { createCollection } from "../../Common/dataAccess/createCollection";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
|
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
|
||||||
isPreferredApiTable: ko.Computed<boolean>;
|
isPreferredApiTable: ko.Computed<boolean>;
|
||||||
@@ -42,8 +43,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
public partitionKeyVisible: ko.Computed<boolean>;
|
public partitionKeyVisible: ko.Computed<boolean>;
|
||||||
public partitionKeyPattern: ko.Computed<string>;
|
public partitionKeyPattern: ko.Computed<string>;
|
||||||
public partitionKeyTitle: ko.Computed<string>;
|
public partitionKeyTitle: ko.Computed<string>;
|
||||||
public rupm: ko.Observable<string>;
|
|
||||||
public rupmVisible: ko.Observable<boolean>;
|
|
||||||
public storage: ko.Observable<string>;
|
public storage: ko.Observable<string>;
|
||||||
public throughputSinglePartition: ViewModels.Editable<number>;
|
public throughputSinglePartition: ViewModels.Editable<number>;
|
||||||
public throughputMultiPartition: ViewModels.Editable<number>;
|
public throughputMultiPartition: ViewModels.Editable<number>;
|
||||||
@@ -143,12 +142,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
});
|
});
|
||||||
this.rupm = ko.observable<string>(Constants.RUPMStates.off);
|
|
||||||
this.rupmVisible = ko.observable<boolean>(false);
|
|
||||||
const featureSubcription = this.container.features.subscribe(() => {
|
|
||||||
this.rupmVisible(this.container.isFeatureEnabled(Constants.Features.enableRupm));
|
|
||||||
featureSubcription.dispose();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
|
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
|
||||||
|
|
||||||
@@ -193,7 +186,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverId: string = this.container.serverId();
|
const serverId = configContext.serverId;
|
||||||
const regions =
|
const regions =
|
||||||
(account &&
|
(account &&
|
||||||
account.properties &&
|
account.properties &&
|
||||||
@@ -201,33 +194,24 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
account.properties.readLocations.length) ||
|
account.properties.readLocations.length) ||
|
||||||
1;
|
1;
|
||||||
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
|
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
|
||||||
const rupmEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
|
|
||||||
|
|
||||||
let throughputSpendAckText: string;
|
let throughputSpendAckText: string;
|
||||||
let estimatedSpend: string;
|
let estimatedSpend: string;
|
||||||
if (!this.isSharedAutoPilotSelected()) {
|
if (!this.isSharedAutoPilotSelected()) {
|
||||||
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
|
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||||
offerThroughput,
|
offerThroughput,
|
||||||
serverId,
|
configContext.serverId,
|
||||||
regions,
|
regions,
|
||||||
multimaster,
|
multimaster,
|
||||||
rupmEnabled,
|
|
||||||
this.isSharedAutoPilotSelected()
|
this.isSharedAutoPilotSelected()
|
||||||
);
|
);
|
||||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
|
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster);
|
||||||
offerThroughput,
|
|
||||||
serverId,
|
|
||||||
regions,
|
|
||||||
multimaster,
|
|
||||||
rupmEnabled
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
|
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||||
this.sharedAutoPilotThroughput(),
|
this.sharedAutoPilotThroughput(),
|
||||||
serverId,
|
serverId,
|
||||||
regions,
|
regions,
|
||||||
multimaster,
|
multimaster,
|
||||||
rupmEnabled,
|
|
||||||
this.isSharedAutoPilotSelected()
|
this.isSharedAutoPilotSelected()
|
||||||
);
|
);
|
||||||
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
|
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
|
||||||
@@ -256,7 +240,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverId: string = this.container.serverId();
|
const serverId: string = configContext.serverId;
|
||||||
const regions =
|
const regions =
|
||||||
(account &&
|
(account &&
|
||||||
account.properties &&
|
account.properties &&
|
||||||
@@ -264,7 +248,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
account.properties.readLocations.length) ||
|
account.properties.readLocations.length) ||
|
||||||
1;
|
1;
|
||||||
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
|
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
|
||||||
const rupmEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
|
|
||||||
|
|
||||||
let throughputSpendAckText: string;
|
let throughputSpendAckText: string;
|
||||||
let estimatedSpend: string;
|
let estimatedSpend: string;
|
||||||
@@ -274,15 +257,13 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
serverId,
|
serverId,
|
||||||
regions,
|
regions,
|
||||||
multimaster,
|
multimaster,
|
||||||
rupmEnabled,
|
|
||||||
this.isAutoPilotSelected()
|
this.isAutoPilotSelected()
|
||||||
);
|
);
|
||||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
|
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
|
||||||
this.throughputMultiPartition(),
|
this.throughputMultiPartition(),
|
||||||
serverId,
|
serverId,
|
||||||
regions,
|
regions,
|
||||||
multimaster,
|
multimaster
|
||||||
rupmEnabled
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
|
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||||
@@ -290,7 +271,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
serverId,
|
serverId,
|
||||||
regions,
|
regions,
|
||||||
multimaster,
|
multimaster,
|
||||||
rupmEnabled,
|
|
||||||
this.isAutoPilotSelected()
|
this.isAutoPilotSelected()
|
||||||
);
|
);
|
||||||
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
|
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
|
||||||
@@ -502,7 +482,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.upsellMessage = ko.pureComputed<string>(() => {
|
this.upsellMessage = ko.pureComputed<string>(() => {
|
||||||
return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount());
|
return PricingUtils.getUpsellMessage(configContext.serverId, this.isFreeTierAccount());
|
||||||
});
|
});
|
||||||
|
|
||||||
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
|
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
|
||||||
@@ -686,11 +666,10 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
storage: this.storage(),
|
storage: this.storage(),
|
||||||
offerThroughput: this._getThroughput(),
|
offerThroughput: this._getThroughput(),
|
||||||
partitionKey: this.partitionKey(),
|
partitionKey: this.partitionKey(),
|
||||||
databaseId: this.databaseId(),
|
databaseId: this.databaseId()
|
||||||
rupm: this.rupm()
|
|
||||||
}),
|
}),
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: userContext.quotaId,
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||||
throughput: this._getThroughput(),
|
throughput: this._getThroughput(),
|
||||||
@@ -788,12 +767,11 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
id: this.collectionId(),
|
id: this.collectionId(),
|
||||||
storage: this.storage(),
|
storage: this.storage(),
|
||||||
partitionKey,
|
partitionKey,
|
||||||
rupm: this.rupm(),
|
|
||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||||
}),
|
}),
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: userContext.quotaId,
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||||
throughput: offerThroughput,
|
throughput: offerThroughput,
|
||||||
@@ -863,12 +841,11 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
id: this.collectionId(),
|
id: this.collectionId(),
|
||||||
storage: this.storage(),
|
storage: this.storage(),
|
||||||
partitionKey,
|
partitionKey,
|
||||||
rupm: this.rupm(),
|
|
||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||||
}),
|
}),
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: userContext.quotaId,
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||||
throughput: offerThroughput,
|
throughput: offerThroughput,
|
||||||
@@ -898,12 +875,11 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
id: this.collectionId(),
|
id: this.collectionId(),
|
||||||
storage: this.storage(),
|
storage: this.storage(),
|
||||||
partitionKey,
|
partitionKey,
|
||||||
rupm: this.rupm(),
|
|
||||||
uniqueKeyPolicy,
|
uniqueKeyPolicy,
|
||||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||||
},
|
},
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: userContext.quotaId,
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||||
throughput: offerThroughput,
|
throughput: offerThroughput,
|
||||||
@@ -981,20 +957,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public onRupmOptionsKeyDown(source: any, event: KeyboardEvent): boolean {
|
|
||||||
if (event.key === "ArrowRight") {
|
|
||||||
this.rupm("off");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === "ArrowLeft") {
|
|
||||||
this.rupm("on");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public onEnableSynapseLinkButtonClicked() {
|
public onEnableSynapseLinkButtonClicked() {
|
||||||
this.container.openEnableSynapseLinkDialog();
|
this.container.openEnableSynapseLinkDialog();
|
||||||
}
|
}
|
||||||
@@ -1018,16 +980,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const throughput = this._getThroughput();
|
const throughput = this._getThroughput();
|
||||||
const maxThroughputWithRUPM =
|
|
||||||
SharedConstants.CollectionCreation.MaxRUPMPerPartition * this._calculateNumberOfPartitions();
|
|
||||||
|
|
||||||
if (this.rupm() === Constants.RUPMStates.on && throughput > maxThroughputWithRUPM) {
|
|
||||||
this.formErrors(
|
|
||||||
`The maximum supported provisioned throughput with RU/m enabled is ${maxThroughputWithRUPM} RU/s. Please turn off RU/m to incease thoughput above ${maxThroughputWithRUPM} RU/s.`
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) {
|
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) {
|
||||||
this.formErrors(`Please acknowledge the estimated daily spend.`);
|
this.formErrors(`Please acknowledge the estimated daily spend.`);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { createDatabase } from "../../Common/dataAccess/createDatabase";
|
|||||||
import { configContext, Platform } from "../../ConfigContext";
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export default class AddDatabasePane extends ContextualPaneBase {
|
export default class AddDatabasePane extends ContextualPaneBase {
|
||||||
public defaultExperience: ko.Computed<string>;
|
public defaultExperience: ko.Computed<string>;
|
||||||
@@ -121,7 +122,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverId = this.container.serverId();
|
const serverId = configContext.serverId;
|
||||||
const regions =
|
const regions =
|
||||||
(account &&
|
(account &&
|
||||||
account.properties &&
|
account.properties &&
|
||||||
@@ -133,19 +134,12 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
let estimatedSpendAcknowledge: string;
|
let estimatedSpendAcknowledge: string;
|
||||||
let estimatedSpend: string;
|
let estimatedSpend: string;
|
||||||
if (!this.isAutoPilotSelected()) {
|
if (!this.isAutoPilotSelected()) {
|
||||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
|
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster);
|
||||||
offerThroughput,
|
|
||||||
serverId,
|
|
||||||
regions,
|
|
||||||
multimaster,
|
|
||||||
false /*rupmEnabled*/
|
|
||||||
);
|
|
||||||
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
|
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||||
offerThroughput,
|
offerThroughput,
|
||||||
serverId,
|
serverId,
|
||||||
regions,
|
regions,
|
||||||
multimaster,
|
multimaster,
|
||||||
false /*rupmEnabled*/,
|
|
||||||
this.isAutoPilotSelected()
|
this.isAutoPilotSelected()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -160,7 +154,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
serverId,
|
serverId,
|
||||||
regions,
|
regions,
|
||||||
multimaster,
|
multimaster,
|
||||||
false /*rupmEnabled*/,
|
|
||||||
this.isAutoPilotSelected()
|
this.isAutoPilotSelected()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -227,7 +220,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.upsellMessage = ko.pureComputed<string>(() => {
|
this.upsellMessage = ko.pureComputed<string>(() => {
|
||||||
return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount());
|
return PricingUtils.getUpsellMessage(configContext.serverId, this.isFreeTierAccount());
|
||||||
});
|
});
|
||||||
|
|
||||||
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
|
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
|
||||||
@@ -258,7 +251,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
databaseAccountName: this.container.databaseAccount().name,
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
defaultExperience: this.container.defaultExperience(),
|
defaultExperience: this.container.defaultExperience(),
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: userContext.quotaId,
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
throughput: this.throughput(),
|
throughput: this.throughput(),
|
||||||
flight: this.container.flight()
|
flight: this.container.flight()
|
||||||
@@ -286,7 +279,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
}),
|
}),
|
||||||
offerThroughput,
|
offerThroughput,
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: userContext.quotaId,
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
flight: this.container.flight()
|
flight: this.container.flight()
|
||||||
},
|
},
|
||||||
@@ -350,7 +343,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
}),
|
}),
|
||||||
offerThroughput: offerThroughput,
|
offerThroughput: offerThroughput,
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: userContext.quotaId,
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
flight: this.container.flight()
|
flight: this.container.flight()
|
||||||
},
|
},
|
||||||
@@ -374,7 +367,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
|||||||
}),
|
}),
|
||||||
offerThroughput: offerThroughput,
|
offerThroughput: offerThroughput,
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: userContext.quotaId,
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
flight: this.container.flight()
|
flight: this.container.flight()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { HashMap } from "../../Common/HashMap";
|
|||||||
import { configContext, Platform } from "../../ConfigContext";
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||||
public createTableQuery: ko.Observable<string>;
|
public createTableQuery: ko.Observable<string>;
|
||||||
@@ -126,7 +127,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverId = this.container.serverId();
|
const serverId = configContext.serverId;
|
||||||
const regions =
|
const regions =
|
||||||
(account &&
|
(account &&
|
||||||
account.properties &&
|
account.properties &&
|
||||||
@@ -138,19 +139,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
let estimatedSpend: string;
|
let estimatedSpend: string;
|
||||||
let estimatedDedicatedSpendAcknowledge: string;
|
let estimatedDedicatedSpendAcknowledge: string;
|
||||||
if (!this.isAutoPilotSelected()) {
|
if (!this.isAutoPilotSelected()) {
|
||||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
|
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster);
|
||||||
offerThroughput,
|
|
||||||
serverId,
|
|
||||||
regions,
|
|
||||||
multimaster,
|
|
||||||
false /*rupmEnabled*/
|
|
||||||
);
|
|
||||||
estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
|
estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||||
offerThroughput,
|
offerThroughput,
|
||||||
serverId,
|
serverId,
|
||||||
regions,
|
regions,
|
||||||
multimaster,
|
multimaster,
|
||||||
false /*rupmEnabled*/,
|
|
||||||
this.isAutoPilotSelected()
|
this.isAutoPilotSelected()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -165,7 +159,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
serverId,
|
serverId,
|
||||||
regions,
|
regions,
|
||||||
multimaster,
|
multimaster,
|
||||||
false /*rupmEnabled*/,
|
|
||||||
this.isAutoPilotSelected()
|
this.isAutoPilotSelected()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -179,7 +172,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverId = this.container.serverId();
|
const serverId = configContext.serverId;
|
||||||
const regions =
|
const regions =
|
||||||
(account &&
|
(account &&
|
||||||
account.properties &&
|
account.properties &&
|
||||||
@@ -190,19 +183,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
let estimatedSpend: string;
|
let estimatedSpend: string;
|
||||||
let estimatedSharedSpendAcknowledge: string;
|
let estimatedSharedSpendAcknowledge: string;
|
||||||
if (!this.isSharedAutoPilotSelected()) {
|
if (!this.isSharedAutoPilotSelected()) {
|
||||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
|
estimatedSpend = PricingUtils.getEstimatedSpendHtml(this.keyspaceThroughput(), serverId, regions, multimaster);
|
||||||
this.keyspaceThroughput(),
|
|
||||||
serverId,
|
|
||||||
regions,
|
|
||||||
multimaster,
|
|
||||||
false /*rupmEnabled*/
|
|
||||||
);
|
|
||||||
estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
|
estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||||
this.keyspaceThroughput(),
|
this.keyspaceThroughput(),
|
||||||
serverId,
|
serverId,
|
||||||
regions,
|
regions,
|
||||||
multimaster,
|
multimaster,
|
||||||
false /*rupmEnabled*/,
|
|
||||||
this.isSharedAutoPilotSelected()
|
this.isSharedAutoPilotSelected()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -217,7 +203,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
serverId,
|
serverId,
|
||||||
regions,
|
regions,
|
||||||
multimaster,
|
multimaster,
|
||||||
false /*rupmEnabled*/,
|
|
||||||
this.isSharedAutoPilotSelected()
|
this.isSharedAutoPilotSelected()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -312,11 +297,10 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
|
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
|
||||||
offerThroughput: this.throughput(),
|
offerThroughput: this.throughput(),
|
||||||
partitionKey: "",
|
partitionKey: "",
|
||||||
databaseId: this.keyspaceId(),
|
databaseId: this.keyspaceId()
|
||||||
rupm: false
|
|
||||||
}),
|
}),
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: userContext.quotaId,
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
throughput: this.throughput(),
|
throughput: this.throughput(),
|
||||||
@@ -366,12 +350,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
offerThroughput: this.throughput(),
|
offerThroughput: this.throughput(),
|
||||||
partitionKey: "",
|
partitionKey: "",
|
||||||
databaseId: this.keyspaceId(),
|
databaseId: this.keyspaceId(),
|
||||||
rupm: false,
|
|
||||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||||
}),
|
}),
|
||||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: userContext.quotaId,
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
throughput: this.throughput(),
|
throughput: this.throughput(),
|
||||||
@@ -413,12 +396,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
offerThroughput: this.throughput(),
|
offerThroughput: this.throughput(),
|
||||||
partitionKey: "",
|
partitionKey: "",
|
||||||
databaseId: this.keyspaceId(),
|
databaseId: this.keyspaceId(),
|
||||||
rupm: false,
|
|
||||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||||
}),
|
}),
|
||||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: userContext.quotaId,
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
throughput: this.throughput(),
|
throughput: this.throughput(),
|
||||||
@@ -444,12 +426,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
|||||||
offerThroughput: this.throughput(),
|
offerThroughput: this.throughput(),
|
||||||
partitionKey: "",
|
partitionKey: "",
|
||||||
databaseId: this.keyspaceId(),
|
databaseId: this.keyspaceId(),
|
||||||
rupm: false,
|
|
||||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||||
},
|
},
|
||||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||||
subscriptionQuotaId: this.container.quotaId(),
|
subscriptionQuotaId: userContext.quotaId,
|
||||||
defaultsCheck: {
|
defaultsCheck: {
|
||||||
storage: "u",
|
storage: "u",
|
||||||
throughput: this.throughput(),
|
throughput: this.throughput(),
|
||||||
|
|||||||
@@ -98,26 +98,21 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
author: string,
|
author: string,
|
||||||
notebookContent: string | ImmutableNotebook,
|
notebookContent: string | ImmutableNotebook,
|
||||||
parentDomElement: HTMLElement,
|
parentDomElement: HTMLElement,
|
||||||
isCodeOfConductEnabled: boolean,
|
|
||||||
isLinkInjectionEnabled: boolean
|
isLinkInjectionEnabled: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (isCodeOfConductEnabled) {
|
try {
|
||||||
try {
|
const response = await this.junoClient.isCodeOfConductAccepted();
|
||||||
const response = await this.junoClient.isCodeOfConductAccepted();
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
|
||||||
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isCodeOfConductAccepted = response.data;
|
|
||||||
} catch (error) {
|
|
||||||
handleError(
|
|
||||||
error,
|
|
||||||
"PublishNotebookPaneAdapter/isCodeOfConductAccepted",
|
|
||||||
"Failed to check if code of conduct was accepted"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.isCodeOfConductAccepted = true;
|
this.isCodeOfConductAccepted = response.data;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(
|
||||||
|
error,
|
||||||
|
"PublishNotebookPaneAdapter/isCodeOfConductAccepted",
|
||||||
|
"Failed to check if code of conduct was accepted"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
|||||||
@@ -50,13 +50,24 @@
|
|||||||
id="fileImportLinkNotebook"
|
id="fileImportLinkNotebook"
|
||||||
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
|
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
|
||||||
>
|
>
|
||||||
<img class="fileImportImg" src="/folder_16x16.svg" alt="upload files" title="Upload files" />
|
<img
|
||||||
|
id="importFileButton"
|
||||||
|
class="fileImportImg"
|
||||||
|
src="/folder_16x16.svg"
|
||||||
|
alt="upload files"
|
||||||
|
title="Upload files"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="paneFooter">
|
<div class="paneFooter">
|
||||||
<div class="leftpanel-okbut">
|
<div class="leftpanel-okbut">
|
||||||
<input type="submit" data-bind="attr: { value: submitButtonLabel }" class="btncreatecoll1" />
|
<input
|
||||||
|
id="uploadFileButton"
|
||||||
|
type="submit"
|
||||||
|
data-bind="attr: { value: submitButtonLabel }"
|
||||||
|
class="btncreatecoll1"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Upload File inputs - End -->
|
<!-- Upload File inputs - End -->
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
padding: 32px 16px;
|
padding: 32px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: @BaseLight;
|
background-color: @BaseLight;
|
||||||
border: 1px solid #E5E5E5;
|
border: 1px solid #949494;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ import { RequestOptions } from "@azure/cosmos/dist-esm";
|
|||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { updateOffer } from "../../Common/dataAccess/updateOffer";
|
import { updateOffer } from "../../Common/dataAccess/updateOffer";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { userContext } from "../../UserContext";
|
|
||||||
import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
|
||||||
import { configContext, Platform } from "../../ConfigContext";
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
|
||||||
@@ -35,11 +33,6 @@ const currentThroughput: (isAutoscale: boolean, throughput: number) => string =
|
|||||||
? `Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} RU/s`
|
? `Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} RU/s`
|
||||||
: `Current manual throughput: ${throughput} RU/s`;
|
: `Current manual throughput: ${throughput} RU/s`;
|
||||||
|
|
||||||
const throughputApplyDelayedMessage = (isAutoscale: boolean, throughput: number, databaseName: string) =>
|
|
||||||
`The request to increase the throughput has successfully been submitted.
|
|
||||||
This operation will take 1-3 business days to complete. View the latest status in Notifications.<br />
|
|
||||||
Database: ${databaseName}, ${currentThroughput(isAutoscale, throughput)}`;
|
|
||||||
|
|
||||||
const throughputApplyShortDelayMessage = (isAutoscale: boolean, throughput: number, databaseName: string) =>
|
const throughputApplyShortDelayMessage = (isAutoscale: boolean, throughput: number, databaseName: string) =>
|
||||||
`A request to increase the throughput is currently in progress.
|
`A request to increase the throughput is currently in progress.
|
||||||
This operation will take some time to complete.<br />
|
This operation will take some time to complete.<br />
|
||||||
@@ -66,8 +59,8 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
public displayedError: ko.Observable<string>;
|
public displayedError: ko.Observable<string>;
|
||||||
public isTemplateReady: ko.Observable<boolean>;
|
public isTemplateReady: ko.Observable<boolean>;
|
||||||
public minRUAnotationVisible: ko.Computed<boolean>;
|
public minRUAnotationVisible: ko.Computed<boolean>;
|
||||||
public minRUs: ko.Computed<number>;
|
public minRUs: ko.Observable<number>;
|
||||||
public maxRUs: ko.Computed<number>;
|
public maxRUs: ko.Observable<number>;
|
||||||
public maxRUsText: ko.PureComputed<string>;
|
public maxRUsText: ko.PureComputed<string>;
|
||||||
public maxRUThroughputInputLimit: ko.Computed<number>;
|
public maxRUThroughputInputLimit: ko.Computed<number>;
|
||||||
public notificationStatusInfo: ko.Observable<string>;
|
public notificationStatusInfo: ko.Observable<string>;
|
||||||
@@ -92,7 +85,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
|
|
||||||
private _hasProvisioningTypeChanged: ko.Computed<boolean>;
|
private _hasProvisioningTypeChanged: ko.Computed<boolean>;
|
||||||
private _wasAutopilotOriginallySet: ko.Observable<boolean>;
|
private _wasAutopilotOriginallySet: ko.Observable<boolean>;
|
||||||
private _offerReplacePending: ko.Computed<boolean>;
|
private _offerReplacePending: ko.Observable<boolean>;
|
||||||
private container: Explorer;
|
private container: Explorer;
|
||||||
|
|
||||||
constructor(options: ViewModels.TabOptions) {
|
constructor(options: ViewModels.TabOptions) {
|
||||||
@@ -111,15 +104,14 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
this._wasAutopilotOriginallySet = ko.observable(false);
|
this._wasAutopilotOriginallySet = ko.observable(false);
|
||||||
this.isAutoPilotSelected = editable.observable(false);
|
this.isAutoPilotSelected = editable.observable(false);
|
||||||
this.autoPilotThroughput = editable.observable<number>();
|
this.autoPilotThroughput = editable.observable<number>();
|
||||||
const offer = this.database && this.database.offer && this.database.offer();
|
|
||||||
const offerAutopilotSettings = offer && offer.content && offer.content.offerAutopilotSettings;
|
|
||||||
this.userCanChangeProvisioningTypes = ko.observable(true);
|
this.userCanChangeProvisioningTypes = ko.observable(true);
|
||||||
|
|
||||||
if (offerAutopilotSettings && offerAutopilotSettings.maxThroughput) {
|
const autoscaleMaxThroughput = this.database?.offer()?.autoscaleMaxThroughput;
|
||||||
if (AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)) {
|
if (autoscaleMaxThroughput) {
|
||||||
|
if (AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||||
this._wasAutopilotOriginallySet(true);
|
this._wasAutopilotOriginallySet(true);
|
||||||
this.isAutoPilotSelected(true);
|
this.isAutoPilotSelected(true);
|
||||||
this.autoPilotThroughput(offerAutopilotSettings.maxThroughput);
|
this.autoPilotThroughput(autoscaleMaxThroughput);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +139,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverId = this.container.serverId();
|
const serverId = configContext.serverId;
|
||||||
const regions =
|
const regions =
|
||||||
(account &&
|
(account &&
|
||||||
account.properties &&
|
account.properties &&
|
||||||
@@ -163,8 +155,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
this.overrideWithAutoPilotSettings() ? this.autoPilotThroughput() : this.throughput(),
|
this.overrideWithAutoPilotSettings() ? this.autoPilotThroughput() : this.throughput(),
|
||||||
serverId,
|
serverId,
|
||||||
regions,
|
regions,
|
||||||
multimaster,
|
multimaster
|
||||||
false /*rupmEnabled*/
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
|
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
|
||||||
@@ -205,45 +196,15 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
return this._hasProvisioningTypeChanged() && !this._wasAutopilotOriginallySet();
|
return this._hasProvisioningTypeChanged() && !this._wasAutopilotOriginallySet();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.minRUs = ko.computed<number>(() => {
|
this.minRUs = ko.observable<number>(
|
||||||
const offerContent =
|
this.database.offer()?.minimumThroughput || this.container.collectionCreationDefaults.throughput.unlimitedmin
|
||||||
this.database && this.database.offer && this.database.offer() && this.database.offer().content;
|
);
|
||||||
|
|
||||||
// TODO: backend is returning 1,000,000 as min throughput which seems wrong
|
|
||||||
// Setting to min throughput to not block and let the backend pass or fail
|
|
||||||
if (offerContent && offerContent.offerAutopilotSettings) {
|
|
||||||
return 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
const collectionThroughputInfo: DataModels.OfferThroughputInfo =
|
|
||||||
offerContent && offerContent.collectionThroughputInfo;
|
|
||||||
|
|
||||||
if (collectionThroughputInfo && !!collectionThroughputInfo.minimumRUForCollection) {
|
|
||||||
return collectionThroughputInfo.minimumRUForCollection;
|
|
||||||
}
|
|
||||||
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
|
|
||||||
return throughputDefaults.unlimitedmin;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.minRUAnotationVisible = ko.computed<boolean>(() => {
|
this.minRUAnotationVisible = ko.computed<boolean>(() => {
|
||||||
return PricingUtils.isLargerThanDefaultMinRU(this.minRUs());
|
return PricingUtils.isLargerThanDefaultMinRU(this.minRUs());
|
||||||
});
|
});
|
||||||
|
|
||||||
this.maxRUs = ko.computed<number>(() => {
|
this.maxRUs = ko.observable<number>(this.container.collectionCreationDefaults.throughput.unlimitedmax);
|
||||||
const collectionThroughputInfo: DataModels.OfferThroughputInfo =
|
|
||||||
this.database &&
|
|
||||||
this.database.offer &&
|
|
||||||
this.database.offer() &&
|
|
||||||
this.database.offer().content &&
|
|
||||||
this.database.offer().content.collectionThroughputInfo;
|
|
||||||
const numPartitions = collectionThroughputInfo && collectionThroughputInfo.numPhysicalPartitions;
|
|
||||||
if (!!numPartitions) {
|
|
||||||
return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
|
|
||||||
return throughputDefaults.unlimitedmax;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.maxRUThroughputInputLimit = ko.pureComputed<number>(() => {
|
this.maxRUThroughputInputLimit = ko.pureComputed<number>(() => {
|
||||||
if (configContext.platform === Platform.Hosted) {
|
if (configContext.platform === Platform.Hosted) {
|
||||||
@@ -269,37 +230,21 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
return this.throughputTitle() + this.requestUnitsUsageCost();
|
return this.throughputTitle() + this.requestUnitsUsageCost();
|
||||||
});
|
});
|
||||||
this.pendingNotification = ko.observable<DataModels.Notification>();
|
this.pendingNotification = ko.observable<DataModels.Notification>();
|
||||||
this._offerReplacePending = ko.pureComputed<boolean>(() => {
|
this._offerReplacePending = ko.observable<boolean>(!!this.database.offer()?.offerReplacePending);
|
||||||
const offer = this.database && this.database.offer && this.database.offer();
|
|
||||||
return (
|
|
||||||
offer &&
|
|
||||||
offer.hasOwnProperty("headers") &&
|
|
||||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
this.notificationStatusInfo = ko.observable<string>("");
|
this.notificationStatusInfo = ko.observable<string>("");
|
||||||
this.shouldShowNotificationStatusPrompt = ko.computed<boolean>(() => this.notificationStatusInfo().length > 0);
|
this.shouldShowNotificationStatusPrompt = ko.computed<boolean>(() => this.notificationStatusInfo().length > 0);
|
||||||
this.warningMessage = ko.computed<string>(() => {
|
this.warningMessage = ko.computed<string>(() => {
|
||||||
const offer = this.database && this.database.offer && this.database.offer();
|
|
||||||
|
|
||||||
if (this.overrideWithProvisionedThroughputSettings()) {
|
if (this.overrideWithProvisionedThroughputSettings()) {
|
||||||
return AutoPilotUtils.manualToAutoscaleDisclaimer;
|
return AutoPilotUtils.manualToAutoscaleDisclaimer;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
const offer = this.database.offer();
|
||||||
offer &&
|
if (offer?.offerReplacePending) {
|
||||||
offer.hasOwnProperty("headers") &&
|
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
||||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
|
||||||
) {
|
|
||||||
const throughput = offer.content.offerAutopilotSettings
|
|
||||||
? offer.content.offerAutopilotSettings.maxThroughput
|
|
||||||
: offer.content.offerThroughput;
|
|
||||||
|
|
||||||
return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id());
|
return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
this.canThroughputExceedMaximumValue()
|
this.canThroughputExceedMaximumValue()
|
||||||
) {
|
) {
|
||||||
@@ -432,60 +377,26 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
const headerOptions: RequestOptions = { initialHeaders: {} };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.isAutoPilotSelected()) {
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
databaseId: this.database.id(),
|
||||||
databaseId: this.database.id(),
|
currentOffer: this.database.offer(),
|
||||||
currentOffer: this.database.offer(),
|
autopilotThroughput: this.isAutoPilotSelected() ? this.autoPilotThroughput() : undefined,
|
||||||
autopilotThroughput: this.autoPilotThroughput(),
|
manualThroughput: this.isAutoPilotSelected() ? undefined : this.throughput()
|
||||||
manualThroughput: undefined,
|
};
|
||||||
migrateToAutoPilot: this._hasProvisioningTypeChanged()
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
if (this._hasProvisioningTypeChanged()) {
|
||||||
this.database.offer(updatedOffer);
|
if (this.isAutoPilotSelected()) {
|
||||||
this.database.offer.valueHasMutated();
|
updateOfferParams.migrateToAutoPilot = true;
|
||||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
} else {
|
||||||
} else {
|
updateOfferParams.migrateToManual = true;
|
||||||
if (this.throughput.editableIsDirty() || this.isAutoPilotSelected.editableIsDirty()) {
|
|
||||||
const originalThroughputValue = this.throughput.getEditableOriginalValue();
|
|
||||||
const newThroughput = this.throughput();
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.canThroughputExceedMaximumValue() &&
|
|
||||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
|
||||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
|
|
||||||
) {
|
|
||||||
const requestPayload = {
|
|
||||||
subscriptionId: userContext.subscriptionId,
|
|
||||||
databaseAccountName: userContext.databaseAccount.name,
|
|
||||||
resourceGroup: userContext.resourceGroup,
|
|
||||||
databaseName: this.database.id(),
|
|
||||||
throughput: newThroughput,
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
|
||||||
};
|
|
||||||
await updateOfferThroughputBeyondLimit(requestPayload);
|
|
||||||
this.database.offer().content.offerThroughput = originalThroughputValue;
|
|
||||||
this.throughput(originalThroughputValue);
|
|
||||||
this.notificationStatusInfo(
|
|
||||||
throughputApplyDelayedMessage(this.isAutoPilotSelected(), newThroughput, this.database.id())
|
|
||||||
);
|
|
||||||
this.throughput.valueHasMutated(); // force component re-render
|
|
||||||
} else {
|
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
|
||||||
databaseId: this.database.id(),
|
|
||||||
currentOffer: this.database.offer(),
|
|
||||||
autopilotThroughput: undefined,
|
|
||||||
manualThroughput: newThroughput,
|
|
||||||
migrateToManual: this._hasProvisioningTypeChanged()
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedOffer = await updateOffer(updateOfferParams);
|
|
||||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
|
||||||
this.database.offer(updatedOffer);
|
|
||||||
this.database.offer.valueHasMutated();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||||
|
this.database.offer(updatedOffer);
|
||||||
|
this.database.offer.valueHasMutated();
|
||||||
|
this._setBaseline();
|
||||||
|
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
this.isExecutionError(true);
|
this.isExecutionError(true);
|
||||||
@@ -527,15 +438,10 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
|
|
||||||
private _setBaseline() {
|
private _setBaseline() {
|
||||||
const offer = this.database && this.database.offer && this.database.offer();
|
const offer = this.database && this.database.offer && this.database.offer();
|
||||||
const offerThroughput = offer.content && offer.content.offerThroughput;
|
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(offer.autoscaleMaxThroughput));
|
||||||
const offerAutopilotSettings = offer && offer.content && offer.content.offerAutopilotSettings;
|
this.autoPilotThroughput.setBaseline(offer.autoscaleMaxThroughput);
|
||||||
|
this.throughput.setBaseline(offer.manualThroughput);
|
||||||
this.throughput.setBaseline(offerThroughput);
|
|
||||||
this.userCanChangeProvisioningTypes(true);
|
this.userCanChangeProvisioningTypes(true);
|
||||||
|
|
||||||
const maxThroughputForAutoPilot = offerAutopilotSettings && offerAutopilotSettings.maxThroughput;
|
|
||||||
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(maxThroughputForAutoPilot));
|
|
||||||
this.autoPilotThroughput.setBaseline(maxThroughputForAutoPilot || AutoPilotUtils.minAutoPilotThroughput);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<div data-bind="react:mongoQueryComponentAdapter" style="height: 100%"></div>
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import * as Q from "q";
|
|
||||||
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
|
|
||||||
import { MongoQueryComponentAdapter } from "../Notebook/MongoQueryComponent/MongoQueryComponentAdapter";
|
|
||||||
|
|
||||||
export default class MongoDocumentsTabV2 extends NotebookTabBase {
|
|
||||||
private mongoQueryComponentAdapter: MongoQueryComponentAdapter;
|
|
||||||
|
|
||||||
constructor(options: NotebookTabBaseOptions) {
|
|
||||||
super(options);
|
|
||||||
this.mongoQueryComponentAdapter = new MongoQueryComponentAdapter({
|
|
||||||
contentRef: undefined,
|
|
||||||
notebookClient: NotebookTabBase.clientManager
|
|
||||||
}, options.collection?.databaseId, options.collection?.id());
|
|
||||||
}
|
|
||||||
|
|
||||||
public onCloseTabButtonClick(): Q.Promise<void> {
|
|
||||||
super.onCloseTabButtonClick();
|
|
||||||
|
|
||||||
// const cleanup = () => {
|
|
||||||
// this.notebookComponentAdapter.notebookShutdown();
|
|
||||||
// this.isActive(false);
|
|
||||||
// super.onCloseTabButtonClick();
|
|
||||||
// };
|
|
||||||
|
|
||||||
// if (this.notebookComponentAdapter.isContentDirty()) {
|
|
||||||
// this.container.showOkCancelModalDialog(
|
|
||||||
// "Close without saving?",
|
|
||||||
// `File has unsaved changes, close without saving?`,
|
|
||||||
// "Close",
|
|
||||||
// cleanup,
|
|
||||||
// "Cancel",
|
|
||||||
// undefined
|
|
||||||
// );
|
|
||||||
// return Q.resolve(null);
|
|
||||||
// } else {
|
|
||||||
// cleanup();
|
|
||||||
// return Q.resolve(null);
|
|
||||||
// }
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected buildCommandBarOptions(): void {
|
|
||||||
this.updateNavbarWithTabsButtons();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,7 +33,7 @@ export default class MongoShellTab extends TabsBase {
|
|||||||
this._runtimeEndpoint = configContext.platform === Platform.Hosted ? configContext.BACKEND_ENDPOINT : "";
|
this._runtimeEndpoint = configContext.platform === Platform.Hosted ? configContext.BACKEND_ENDPOINT : "";
|
||||||
const extensionEndpoint: string = configContext.BACKEND_ENDPOINT || this._runtimeEndpoint || "";
|
const extensionEndpoint: string = configContext.BACKEND_ENDPOINT || this._runtimeEndpoint || "";
|
||||||
let baseUrl = "/content/mongoshell/dist/";
|
let baseUrl = "/content/mongoshell/dist/";
|
||||||
if (this._container.serverId() === "localhost") {
|
if (configContext.serverId === "localhost") {
|
||||||
baseUrl = "/content/mongoshell/";
|
baseUrl = "/content/mongoshell/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
import TabsBase from "./TabsBase";
|
|
||||||
|
|
||||||
import { NotebookClientV2 } from "../Notebook/NotebookClientV2";
|
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import Explorer from "../Explorer";
|
|
||||||
import { ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import { Areas } from "../../Common/Constants";
|
|
||||||
|
|
||||||
export interface NotebookTabBaseOptions extends ViewModels.TabOptions {
|
|
||||||
container: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Every notebook-based tab inherits from this class. It holds the static reference to a notebook client (singleton)
|
|
||||||
*/
|
|
||||||
export default class NotebookTabBase extends TabsBase {
|
|
||||||
protected static clientManager: NotebookClientV2;
|
|
||||||
protected container: Explorer;
|
|
||||||
|
|
||||||
constructor(options: NotebookTabBaseOptions) {
|
|
||||||
super(options);
|
|
||||||
|
|
||||||
this.container = options.container;
|
|
||||||
|
|
||||||
if (!NotebookTabBase.clientManager) {
|
|
||||||
NotebookTabBase.clientManager = new NotebookClientV2({
|
|
||||||
connectionInfo: this.container.notebookServerInfo(),
|
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
|
||||||
defaultExperience: this.container.defaultExperience(),
|
|
||||||
contentProvider: this.container.notebookManager?.notebookContentProvider
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Override base behavior
|
|
||||||
*/
|
|
||||||
protected getContainer(): Explorer {
|
|
||||||
return this.container;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected traceTelemetry(actionType: number): void {
|
|
||||||
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
|
||||||
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
|
||||||
defaultExperience: this.container.defaultExperience && this.container.defaultExperience(),
|
|
||||||
dataExplorerArea: Areas.Notebook
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,9 @@ import * as _ from "underscore";
|
|||||||
import * as Q from "q";
|
import * as Q from "q";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
|
|
||||||
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import TabsBase from "./TabsBase";
|
||||||
|
|
||||||
import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg";
|
import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg";
|
||||||
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
|
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
|
||||||
@@ -15,25 +17,31 @@ import SaveIcon from "../../../images/save-cosmos.svg";
|
|||||||
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
|
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
|
||||||
import InterruptKernelIcon from "../../../images/notebook/Notebook-stop.svg";
|
import InterruptKernelIcon from "../../../images/notebook/Notebook-stop.svg";
|
||||||
import KillKernelIcon from "../../../images/notebook/Notebook-stop.svg";
|
import KillKernelIcon from "../../../images/notebook/Notebook-stop.svg";
|
||||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { ArmApiVersions } from "../../Common/Constants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { Areas, ArmApiVersions } from "../../Common/Constants";
|
||||||
import { CommandBarComponentButtonFactory } from "../Menus/CommandBar/CommandBarComponentButtonFactory";
|
import { CommandBarComponentButtonFactory } from "../Menus/CommandBar/CommandBarComponentButtonFactory";
|
||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
|
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
|
||||||
import { NotebookConfigurationUtils } from "../../Utils/NotebookConfigurationUtils";
|
import { NotebookConfigurationUtils } from "../../Utils/NotebookConfigurationUtils";
|
||||||
import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2";
|
import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2";
|
||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
|
import Explorer from "../Explorer";
|
||||||
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { toJS, stringifyNotebook } from "@nteract/commutable";
|
import { toJS, stringifyNotebook } from "@nteract/commutable";
|
||||||
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
|
|
||||||
|
|
||||||
export interface NotebookTabOptions extends NotebookTabBaseOptions {
|
export interface NotebookTabOptions extends ViewModels.TabOptions {
|
||||||
|
account: DataModels.DatabaseAccount;
|
||||||
|
masterKey: string;
|
||||||
|
container: Explorer;
|
||||||
notebookContentItem: NotebookContentItem;
|
notebookContentItem: NotebookContentItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class NotebookTabV2 extends NotebookTabBase {
|
export default class NotebookTabV2 extends TabsBase {
|
||||||
|
private static clientManager: NotebookClientV2;
|
||||||
|
private container: Explorer;
|
||||||
public notebookPath: ko.Observable<string>;
|
public notebookPath: ko.Observable<string>;
|
||||||
private selectedSparkPool: ko.Observable<string>;
|
private selectedSparkPool: ko.Observable<string>;
|
||||||
private notebookComponentAdapter: NotebookComponentAdapter;
|
private notebookComponentAdapter: NotebookComponentAdapter;
|
||||||
@@ -42,6 +50,16 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
|||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.container = options.container;
|
this.container = options.container;
|
||||||
|
|
||||||
|
if (!NotebookTabV2.clientManager) {
|
||||||
|
NotebookTabV2.clientManager = new NotebookClientV2({
|
||||||
|
connectionInfo: this.container.notebookServerInfo(),
|
||||||
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
contentProvider: this.container.notebookManager?.notebookContentProvider
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.notebookPath = ko.observable(options.notebookContentItem.path);
|
this.notebookPath = ko.observable(options.notebookContentItem.path);
|
||||||
|
|
||||||
this.container.notebookServerInfo.subscribe((newValue: DataModels.NotebookWorkspaceConnectionInfo) => {
|
this.container.notebookServerInfo.subscribe((newValue: DataModels.NotebookWorkspaceConnectionInfo) => {
|
||||||
@@ -51,7 +69,7 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
|||||||
this.notebookComponentAdapter = new NotebookComponentAdapter({
|
this.notebookComponentAdapter = new NotebookComponentAdapter({
|
||||||
contentItem: options.notebookContentItem,
|
contentItem: options.notebookContentItem,
|
||||||
notebooksBasePath: this.container.getNotebookBasePath(),
|
notebooksBasePath: this.container.getNotebookBasePath(),
|
||||||
notebookClient: NotebookTabBase.clientManager,
|
notebookClient: NotebookTabV2.clientManager,
|
||||||
onUpdateKernelInfo: this.onKernelUpdate
|
onUpdateKernelInfo: this.onKernelUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,6 +115,10 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
|||||||
return await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName());
|
return await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getContainer(): Explorer {
|
||||||
|
return this.container;
|
||||||
|
}
|
||||||
|
|
||||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||||
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
||||||
|
|
||||||
@@ -471,4 +493,12 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
|||||||
|
|
||||||
this.container.copyNotebook(notebookContent.name, content);
|
this.container.copyNotebook(notebookContent.name, content);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private traceTelemetry(actionType: number) {
|
||||||
|
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
||||||
|
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
||||||
|
defaultExperience: this.container.defaultExperience && this.container.defaultExperience(),
|
||||||
|
dataExplorerArea: Areas.Notebook
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,723 +0,0 @@
|
|||||||
<div
|
|
||||||
class="tab-pane flexContainer"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
id: tabId
|
|
||||||
},
|
|
||||||
visible: isActive"
|
|
||||||
role="tabpanel"
|
|
||||||
>
|
|
||||||
<div class="warningErrorContainer scaleWarningContainer" data-bind="visible: shouldShowStatusBar">
|
|
||||||
<div>
|
|
||||||
<div class="warningErrorContent" data-bind="visible: shouldShowNotificationStatusPrompt">
|
|
||||||
<span><img src="/info_color.svg" alt="Info"/></span>
|
|
||||||
<span class="warningErrorDetailsLinkContainer" data-bind="html: notificationStatusInfo"></span>
|
|
||||||
</div>
|
|
||||||
<div class="warningErrorContent" data-bind="visible: !shouldShowNotificationStatusPrompt()">
|
|
||||||
<span><img src="/warning.svg" alt="Warning"/></span>
|
|
||||||
<span class="warningErrorDetailsLinkContainer" data-bind="html: warningMessage"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="tabForm scaleSettingScrollable">
|
|
||||||
<!-- ko if: shouldShowKeyspaceSharedThroughputMessage -->
|
|
||||||
<div>This table shared throughput is configured at the keyspace</div>
|
|
||||||
<!-- /ko -->
|
|
||||||
|
|
||||||
<!-- ko ifnot: hasDatabaseSharedThroughput -->
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="scaleDivison"
|
|
||||||
data-bind="click:toggleScale, event: { keypress: onScaleKeyPress }, attr:{ 'aria-expanded': scaleExpanded() ? 'true' : 'false' }"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
aria-label="Scale"
|
|
||||||
aria-controls="scaleRegion"
|
|
||||||
>
|
|
||||||
<span class="themed-images" type="text/html" id="ExpandChevronRightScale" data-bind="visible: !scaleExpanded()">
|
|
||||||
<img
|
|
||||||
class="imgiconwidth ssExpandCollapseIcon ssCollapseIcon "
|
|
||||||
src="/Triangle-right.svg"
|
|
||||||
alt="Show scale properties"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="themed-images" type="text/html" id="ExpandChevronDownScale" data-bind="visible: scaleExpanded">
|
|
||||||
<img class="imgiconwidth ssExpandCollapseIcon " src="/Triangle-down.svg" alt="Hide scale properties" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="scaleSettingTitle">Scale</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ssTextAllignment" data-bind="visible: scaleExpanded" id="scaleRegion">
|
|
||||||
<!-- ko ifnot: isAutoScaleEnabled -->
|
|
||||||
<throughput-input-autopilot-v3
|
|
||||||
params="{
|
|
||||||
testId: testId,
|
|
||||||
class: 'scaleForm dirty',
|
|
||||||
value: throughput,
|
|
||||||
minimum: minRUs,
|
|
||||||
maximum: maxRUThroughputInputLimit,
|
|
||||||
isEnabled: !hasDatabaseSharedThroughput(),
|
|
||||||
canExceedMaximumValue: canThroughputExceedMaximumValue,
|
|
||||||
label: throughputTitle,
|
|
||||||
ariaLabel: throughputAriaLabel,
|
|
||||||
costsVisible: costsVisible,
|
|
||||||
requestUnitsUsageCost: requestUnitsUsageCost,
|
|
||||||
throughputAutoPilotRadioId: throughputAutoPilotRadioId,
|
|
||||||
throughputProvisionedRadioId: throughputProvisionedRadioId,
|
|
||||||
throughputModeRadioName: throughputModeRadioName,
|
|
||||||
showAutoPilot: userCanChangeProvisioningTypes,
|
|
||||||
isAutoPilotSelected: isAutoPilotSelected,
|
|
||||||
maxAutoPilotThroughputSet: autoPilotThroughput,
|
|
||||||
autoPilotUsageCost: autoPilotUsageCost,
|
|
||||||
overrideWithAutoPilotSettings: overrideWithAutoPilotSettings,
|
|
||||||
overrideWithProvisionedThroughputSettings: overrideWithProvisionedThroughputSettings
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
</throughput-input-autopilot-v3>
|
|
||||||
|
|
||||||
<div class="storageCapacityTitle throughputStorageValue" data-bind="html: storageCapacityTitle"></div>
|
|
||||||
<!-- /ko -->
|
|
||||||
|
|
||||||
<div data-bind="visible: rupmVisible">
|
|
||||||
<div class="formTitle">RU/m</div>
|
|
||||||
<div class="tabs" aria-label="RU/m">
|
|
||||||
<div class="tab">
|
|
||||||
<label
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
for: rupmOnId
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
dirty: rupm.editableIsDirty,
|
|
||||||
selectedRadio: rupm() === 'on',
|
|
||||||
unselectedRadio: rupm() !== 'on'
|
|
||||||
}"
|
|
||||||
>On</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="rupm"
|
|
||||||
value="on"
|
|
||||||
class="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
id: rupmOnId
|
|
||||||
},
|
|
||||||
checked: rupm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="tab">
|
|
||||||
<label
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
for: rupmOffId
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
dirty: rupm.editableIsDirty,
|
|
||||||
selectedRadio: rupm() === 'off',
|
|
||||||
unselectedRadio: rupm() !== 'off'
|
|
||||||
}"
|
|
||||||
>Off</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="rupm"
|
|
||||||
value="off"
|
|
||||||
class="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
id: rupmOffId
|
|
||||||
},
|
|
||||||
checked: rupm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TODO: Replace link with call to the Azure Support blade -->
|
|
||||||
<div data-bind="visible: isAutoScaleEnabled">
|
|
||||||
<div class="autoScaleThroughputTitle">Throughput (RU/s)</div>
|
|
||||||
<input
|
|
||||||
class="formReadOnly collid-white"
|
|
||||||
readonly
|
|
||||||
aria-label="Throughput input"
|
|
||||||
data-bind="textInput: throughput"
|
|
||||||
/>
|
|
||||||
<div class="autoScaleDescription">
|
|
||||||
Your account has custom settings that prevents setting throughput at the container level. Please work with
|
|
||||||
your Cosmos DB engineering team point of contact to make changes.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- /ko -->
|
|
||||||
|
|
||||||
<div data-bind="visible: hasConflictResolution">
|
|
||||||
<div
|
|
||||||
class="formTitle"
|
|
||||||
data-bind="click:toggleConflictResolution, event: { keypress: onConflictResolutionKeyPress }, attr:{ 'aria-expanded': conflictResolutionExpanded() ? 'true' : 'false' }"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
aria-label="Conflict Resolution"
|
|
||||||
aria-controls="conflictResolutionRegion"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="themed-images"
|
|
||||||
type="text/html"
|
|
||||||
id="ExpandChevronRightConflictResolution"
|
|
||||||
data-bind="visible: !conflictResolutionExpanded()"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="imgiconwidth ssExpandCollapseIcon ssCollapseIcon"
|
|
||||||
src="/Triangle-right.svg"
|
|
||||||
alt="Show conflict resolution"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="themed-images"
|
|
||||||
type="text/html"
|
|
||||||
id="ExpandChevronDownConflictResolution"
|
|
||||||
data-bind="visible: conflictResolutionExpanded"
|
|
||||||
>
|
|
||||||
<img class="imgiconwidth ssExpandCollapseIcon" src="/Triangle-down.svg" alt="Show conflict resolution" />
|
|
||||||
</span>
|
|
||||||
<span class="scaleSettingTitle">Conflict resolution</span>
|
|
||||||
</div>
|
|
||||||
<div id="conflictResolutionRegion" class="ssTextAllignment" data-bind="visible: conflictResolutionExpanded">
|
|
||||||
<div class="formTitle">Mode</div>
|
|
||||||
<div class="tabs" aria-label="Mode" role="radiogroup">
|
|
||||||
<div class="tab">
|
|
||||||
<label
|
|
||||||
tabindex="0"
|
|
||||||
role="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
for: conflictResolutionPolicyModeLWW,
|
|
||||||
'aria-checked': conflictResolutionPolicyMode() !== 'Custom' ? 'true' : 'false'
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
dirty: conflictResolutionPolicyMode.editableIsDirty,
|
|
||||||
selectedRadio: conflictResolutionPolicyMode() === 'LastWriterWins',
|
|
||||||
unselectedRadio: conflictResolutionPolicyMode() !== 'LastWriterWins'
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
keypress: onConflictResolutionLWWKeyPress
|
|
||||||
}"
|
|
||||||
>Last Write Wins (default)</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="conflictresolution"
|
|
||||||
value="LastWriterWins"
|
|
||||||
class="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
id: conflictResolutionPolicyModeLWW
|
|
||||||
},
|
|
||||||
checked: conflictResolutionPolicyMode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab">
|
|
||||||
<label
|
|
||||||
tabindex="0"
|
|
||||||
role="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
for: conflictResolutionPolicyModeCustom,
|
|
||||||
'aria-checked': conflictResolutionPolicyMode() === 'Custom' ? 'true' : 'false'
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
dirty: conflictResolutionPolicyMode.editableIsDirty,
|
|
||||||
selectedRadio: conflictResolutionPolicyMode() === 'Custom',
|
|
||||||
unselectedRadio: conflictResolutionPolicyMode() !== 'Custom'
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
keypress: onConflictResolutionCustomKeyPress
|
|
||||||
}"
|
|
||||||
>Merge Procedure (custom)</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="conflictresolution"
|
|
||||||
value="Custom"
|
|
||||||
class="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
id: conflictResolutionPolicyModeCustom
|
|
||||||
},
|
|
||||||
checked: conflictResolutionPolicyMode"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div data-bind="visible: conflictResolutionPolicyMode() === 'LastWriterWins'">
|
|
||||||
<p class="formTitle">
|
|
||||||
Conflict Resolver Property
|
|
||||||
<span class="infoTooltip" role="tooltip" tabindex="0">
|
|
||||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
|
||||||
<span class="tooltiptext infoTooltipWidth"
|
|
||||||
>Gets or sets the name of a integer property in your documents which is used for the Last Write Wins
|
|
||||||
(LWW) based conflict resolution scheme. By default, the system uses the system defined timestamp
|
|
||||||
property, _ts to decide the winner for the conflicting versions of the document. Specify your own
|
|
||||||
integer property if you want to override the default timestamp based conflict resolution.</span
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
aria-label="Document path for conflict resolution"
|
|
||||||
data-bind="
|
|
||||||
css: {
|
|
||||||
dirty: conflictResolutionPolicyPath.editableIsDirty
|
|
||||||
},
|
|
||||||
textInput: conflictResolutionPolicyPath,
|
|
||||||
enable: conflictResolutionPolicyMode() === 'LastWriterWins'"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div data-bind="visible: conflictResolutionPolicyMode() === 'Custom'">
|
|
||||||
<p class="formTitle">
|
|
||||||
Stored procedure
|
|
||||||
<span class="infoTooltip" role="tooltip" tabindex="0">
|
|
||||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
|
||||||
<span class="tooltiptext infoTooltipWidth"
|
|
||||||
>Gets or sets the name of a stored procedure (aka merge procedure) for resolving the conflicts. You can
|
|
||||||
write application defined logic to determine the winner of the conflicting versions of a document. The
|
|
||||||
stored procedure will get executed transactionally, exactly once, on the server side. If you do not
|
|
||||||
provide a stored procedure, the conflicts will be populated in the
|
|
||||||
<a class="linkDarkBackground" href="https://aka.ms/dataexplorerconflics" target="_blank"
|
|
||||||
>conflicts feed</a
|
|
||||||
>. You can update/re-register the stored procedure at any time.</span
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
aria-label="Stored procedure name for conflict resolution"
|
|
||||||
data-bind="
|
|
||||||
css: {
|
|
||||||
dirty: conflictResolutionPolicyProcedure.editableIsDirty
|
|
||||||
},
|
|
||||||
textInput: conflictResolutionPolicyProcedure,
|
|
||||||
enable: conflictResolutionPolicyMode() === 'Custom'"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="formTitle"
|
|
||||||
data-bind="click:toggleSettings, event: { keypress: onSettingsKeyPress }, attr:{ 'aria-expanded': settingsExpanded() ? 'true' : 'false' }, visible: shouldShowIndexingPolicyEditor() || ttlVisible()"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
aria-label="Settings"
|
|
||||||
aria-controls="settingsRegion"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="themed-images"
|
|
||||||
type="text/html"
|
|
||||||
id="ExpandChevronRightSettings"
|
|
||||||
data-bind="visible: !settingsExpanded() && !hasDatabaseSharedThroughput()"
|
|
||||||
>
|
|
||||||
<img class="imgiconwidth ssExpandCollapseIcon ssCollapseIcon" src="/Triangle-right.svg" alt="Show settings" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="themed-images"
|
|
||||||
type="text/html"
|
|
||||||
id="ExpandChevronDownSettings"
|
|
||||||
data-bind="visible: settingsExpanded() && !hasDatabaseSharedThroughput()"
|
|
||||||
>
|
|
||||||
<img class="imgiconwidth ssExpandCollapseIcon" src="/Triangle-down.svg" alt="Show settings" />
|
|
||||||
</span>
|
|
||||||
<span class="scaleSettingTitle">Settings</span>
|
|
||||||
</div>
|
|
||||||
<div class="ssTextAllignment" data-bind="visible: settingsExpanded" id="settingsRegion">
|
|
||||||
<div data-bind="visible: ttlVisible">
|
|
||||||
<div class="formTitle">Time to Live</div>
|
|
||||||
<div class="tabs disableFocusDefaults" aria-label="Time to Live" role="radiogroup">
|
|
||||||
<div class="tab">
|
|
||||||
<label
|
|
||||||
class="ttlIndexingPolicyFocusElement"
|
|
||||||
tabindex="0"
|
|
||||||
role="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
for: ttlOffId,
|
|
||||||
'aria-checked': timeToLive() === 'off' ? 'true' : 'false'
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
dirty: timeToLive.editableIsDirty,
|
|
||||||
selectedRadio: timeToLive() === 'off',
|
|
||||||
unselectedRadio: timeToLive() !== 'off'
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
keypress: onTtlOffKeyPress
|
|
||||||
},
|
|
||||||
hasFocus: ttlOffFocused"
|
|
||||||
>Off</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="ttl"
|
|
||||||
value="off"
|
|
||||||
class="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
id: ttlOffId
|
|
||||||
},
|
|
||||||
checked: timeToLive"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab">
|
|
||||||
<label
|
|
||||||
class="ttlIndexingPolicyFocusElement"
|
|
||||||
tabindex="0"
|
|
||||||
role="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
for: ttlOnNoDefaultId,
|
|
||||||
'aria-checked': timeToLive() === 'on-nodefault' ? 'true' : 'false'
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
dirty: timeToLive.editableIsDirty,
|
|
||||||
selectedRadio: timeToLive() === 'on-nodefault',
|
|
||||||
unselectedRadio: timeToLive() !== 'on-nodefault'
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
keypress: onTtlOnNoDefaultKeyPress
|
|
||||||
},
|
|
||||||
hasFocus: ttlOnDefaultFocused"
|
|
||||||
>On (no default)</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="ttl"
|
|
||||||
value="on-nodefault"
|
|
||||||
class="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
id: ttlOnNoDefaultId
|
|
||||||
},
|
|
||||||
checked: timeToLive"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab">
|
|
||||||
<label
|
|
||||||
class="ttlIndexingPolicyFocusElement"
|
|
||||||
tabindex="0"
|
|
||||||
role="radio"
|
|
||||||
for="ttl3"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
for: ttlOnId,
|
|
||||||
'aria-checked': timeToLive() === 'on' ? 'true' : 'false'
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
dirty: timeToLive.editableIsDirty,
|
|
||||||
selectedRadio: timeToLive() === 'on',
|
|
||||||
unselectedRadio: timeToLive() !== 'on'
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
keypress: onTtlOnKeyPress
|
|
||||||
},
|
|
||||||
hasFocus: ttlOnFocused"
|
|
||||||
>On</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="ttl"
|
|
||||||
value="on"
|
|
||||||
class="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
id: ttlOnId
|
|
||||||
},
|
|
||||||
checked: timeToLive"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div data-bind="visible: timeToLive() === 'on'">
|
|
||||||
<input
|
|
||||||
class="dirtyTextbox"
|
|
||||||
type="number"
|
|
||||||
required
|
|
||||||
min="1"
|
|
||||||
max="2147483647"
|
|
||||||
aria-label="Time to live in seconds"
|
|
||||||
data-bind="
|
|
||||||
css: {
|
|
||||||
dirty: timeToLive.editableIsDirty
|
|
||||||
},
|
|
||||||
textInput: timeToLiveSeconds,
|
|
||||||
enable: timeToLive() === 'on'"
|
|
||||||
/>
|
|
||||||
second(s)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Geospatial - start -->
|
|
||||||
<div data-bind="visible: geospatialVisible">
|
|
||||||
<div class="formTitle">Geospatial Configuration</div>
|
|
||||||
|
|
||||||
<div class="tabs disableFocusDefaults" aria-label="Geospatial Configuration" role="radiogroup">
|
|
||||||
<div class="tab">
|
|
||||||
<label
|
|
||||||
for="geography"
|
|
||||||
tabindex="0"
|
|
||||||
role="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
'aria-checked': geospatialConfigType().toLowerCase() !== GEOMETRY.toLowerCase() ? 'true' : 'false'
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
dirty: geospatialConfigType.editableIsDirty,
|
|
||||||
selectedRadio: geospatialConfigType().toLowerCase() !== GEOMETRY.toLowerCase(),
|
|
||||||
unselectedRadio: geospatialConfigType().toLowerCase() === GEOMETRY.toLowerCase()
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
keypress: onGeographyKeyPress
|
|
||||||
}"
|
|
||||||
>Geography</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="geospatial"
|
|
||||||
id="geography"
|
|
||||||
class="radio"
|
|
||||||
data-bind="
|
|
||||||
attr: {
|
|
||||||
value: GEOGRAPHY
|
|
||||||
},
|
|
||||||
checked: geospatialConfigType"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab">
|
|
||||||
<label
|
|
||||||
for="geometry"
|
|
||||||
tabindex="0"
|
|
||||||
role="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
'aria-checked': geospatialConfigType().toLowerCase() === GEOMETRY.toLowerCase() ? 'true' : 'false'
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
dirty: geospatialConfigType.editableIsDirty,
|
|
||||||
selectedRadio: geospatialConfigType().toLowerCase() === GEOMETRY.toLowerCase(),
|
|
||||||
unselectedRadio: geospatialConfigType().toLowerCase() !== GEOMETRY.toLowerCase()
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
keypress: onGeometryKeyPress
|
|
||||||
}"
|
|
||||||
>Geometry</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="geospatial"
|
|
||||||
id="geometry"
|
|
||||||
class="radio"
|
|
||||||
data-bind="
|
|
||||||
attr: {
|
|
||||||
value: GEOMETRY
|
|
||||||
},
|
|
||||||
checked: geospatialConfigType"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Geospatial - end -->
|
|
||||||
|
|
||||||
<div data-bind="visible: isAnalyticalStorageEnabled">
|
|
||||||
<div class="formTitle">Analytical Storage Time to Live</div>
|
|
||||||
<div class="tabs disableFocusDefaults" aria-label="Analytical Storage Time to Live" role="radiogroup">
|
|
||||||
<div class="tab">
|
|
||||||
<label tabindex="0" role="radio" class="disabledRadio">Off </label>
|
|
||||||
</div>
|
|
||||||
<div class="tab">
|
|
||||||
<label
|
|
||||||
tabindex="0"
|
|
||||||
role="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
for: 'analyticalStorageTtlOnNoDefaultId',
|
|
||||||
'aria-checked': analyticalStorageTtlSelection() === 'on-nodefault' ? 'true' : 'false'
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
dirty: analyticalStorageTtlSelection.editableIsDirty,
|
|
||||||
selectedRadio: analyticalStorageTtlSelection() === 'on-nodefault',
|
|
||||||
unselectedRadio: analyticalStorageTtlSelection() !== 'on-nodefault'
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
keypress: onAnalyticalStorageTtlOnNoDefaultKeyPress
|
|
||||||
}"
|
|
||||||
>On (no default)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="analyticalStorageTtl"
|
|
||||||
value="on-nodefault"
|
|
||||||
class="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
id: 'analyticalStorageTtlOnNoDefaultId'
|
|
||||||
},
|
|
||||||
checked: analyticalStorageTtlSelection"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab">
|
|
||||||
<label
|
|
||||||
tabindex="0"
|
|
||||||
role="radio"
|
|
||||||
for="ttl3"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
for: 'analyticalStorageTtlOnId',
|
|
||||||
'aria-checked': analyticalStorageTtlSelection() === 'on' ? 'true' : 'false'
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
dirty: analyticalStorageTtlSelection.editableIsDirty,
|
|
||||||
selectedRadio: analyticalStorageTtlSelection() === 'on',
|
|
||||||
unselectedRadio: analyticalStorageTtlSelection() !== 'on'
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
keypress: onAnalyticalStorageTtlOnKeyPress
|
|
||||||
}"
|
|
||||||
>On</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="analyticalStorageTtl"
|
|
||||||
value="on"
|
|
||||||
class="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
id: 'analyticalStorageTtlOnId'
|
|
||||||
},
|
|
||||||
checked: analyticalStorageTtlSelection"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div data-bind="visible: analyticalStorageTtlSelection() === 'on'">
|
|
||||||
<input
|
|
||||||
class="dirtyTextbox"
|
|
||||||
type="number"
|
|
||||||
required
|
|
||||||
min="1"
|
|
||||||
max="2147483647"
|
|
||||||
aria-label="Time to live in seconds"
|
|
||||||
data-bind="
|
|
||||||
css: {
|
|
||||||
dirty: analyticalStorageTtlSelection.editableIsDirty
|
|
||||||
},
|
|
||||||
textInput: analyticalStorageTtlSeconds,
|
|
||||||
enable: analyticalStorageTtlSelection() === 'on'"
|
|
||||||
/>
|
|
||||||
second(s)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div data-bind="visible: changeFeedPolicyVisible">
|
|
||||||
<div class="formTitle">
|
|
||||||
<span>Change feed log retention policy</span>
|
|
||||||
<span class="infoTooltip" role="tooltip" tabindex="0">
|
|
||||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
|
||||||
<span class="tooltiptext infoTooltipWidth"
|
|
||||||
>Enable change feed log retention policy to retain last 10 minutes of history for items in the container
|
|
||||||
by default. To support this, the request unit (RU) charge for this container will be multiplied by a
|
|
||||||
factor of two for writes. Reads are unaffected.</span
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="tabs disableFocusDefaults" aria-label="Change feed selection tabs">
|
|
||||||
<div class="tab">
|
|
||||||
<label
|
|
||||||
tabindex="0"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
for: changeFeedPolicyOffId
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
dirty: changeFeedPolicyToggled.editableIsDirty,
|
|
||||||
selectedRadio: changeFeedPolicyToggled() === 'Off',
|
|
||||||
unselectedRadio: changeFeedPolicyToggled() === 'On'
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
keypress: onChangeFeedPolicyOffKeyPress
|
|
||||||
}"
|
|
||||||
>Off</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="changeFeedPolicy"
|
|
||||||
value="Off"
|
|
||||||
class="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
id: changeFeedPolicyOffId
|
|
||||||
},
|
|
||||||
checked: changeFeedPolicyToggled"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="tab">
|
|
||||||
<label
|
|
||||||
tabindex="0"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
for: changeFeedPolicyOnId
|
|
||||||
},
|
|
||||||
css: {
|
|
||||||
dirty: changeFeedPolicyToggled.editableIsDirty,
|
|
||||||
selectedRadio: changeFeedPolicyToggled() === 'On',
|
|
||||||
unselectedRadio: changeFeedPolicyToggled() === 'Off'
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
keypress: onChangeFeedPolicyOnKeyPress
|
|
||||||
}"
|
|
||||||
>On</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="changeFeedPolicy"
|
|
||||||
value="On"
|
|
||||||
class="radio"
|
|
||||||
data-bind="
|
|
||||||
attr:{
|
|
||||||
id: changeFeedPolicyOnId
|
|
||||||
},
|
|
||||||
checked: changeFeedPolicyToggled"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div data-bind="visible: partitionKeyVisible">
|
|
||||||
<div class="formTitle" data-bind="text: partitionKeyName">Partition Key</div>
|
|
||||||
<input
|
|
||||||
class="formReadOnly collid-white"
|
|
||||||
data-bind="textInput: partitionKeyValue, attr: { 'aria-label':partitionKeyName }"
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="largePartitionKeyEnabled" data-bind="visible: isLargePartitionKeyEnabled">
|
|
||||||
<p data-bind="visible: isLargePartitionKeyEnabled">
|
|
||||||
Large <span data-bind="text:lowerCasePartitionKeyName"></span> has been enabled
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div data-bind="visible: shouldShowIndexingPolicyEditor">
|
|
||||||
<div class="formTitle">Indexing Policy</div>
|
|
||||||
<div
|
|
||||||
class="indexingPolicyEditor ttlIndexingPolicyFocusElement"
|
|
||||||
tabindex="0"
|
|
||||||
data-bind="setTemplateReady: true, attr:{ id: indexingPolicyEditorId }"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,449 +0,0 @@
|
|||||||
import * as Constants from "../../Common/Constants";
|
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import * as ko from "knockout";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
import Collection from "../Tree/Collection";
|
|
||||||
import Database from "../Tree/Database";
|
|
||||||
import Explorer from "../Explorer";
|
|
||||||
import SettingsTab from "./SettingsTab";
|
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
|
||||||
import { IndexingPolicies } from "../../Shared/Constants";
|
|
||||||
|
|
||||||
describe("Settings tab", () => {
|
|
||||||
const baseCollection: DataModels.Collection = {
|
|
||||||
defaultTtl: 200,
|
|
||||||
partitionKey: null,
|
|
||||||
conflictResolutionPolicy: {
|
|
||||||
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
|
||||||
conflictResolutionPath: "/_ts"
|
|
||||||
},
|
|
||||||
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
|
||||||
_rid: "",
|
|
||||||
_self: "",
|
|
||||||
_etag: "",
|
|
||||||
_ts: 0,
|
|
||||||
id: "mycoll"
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseDatabase: DataModels.Database = {
|
|
||||||
_rid: "",
|
|
||||||
_self: "",
|
|
||||||
_etag: "",
|
|
||||||
_ts: 0,
|
|
||||||
id: "mydb",
|
|
||||||
collections: [baseCollection]
|
|
||||||
};
|
|
||||||
|
|
||||||
const quotaInfo: DataModels.CollectionQuotaInfo = {
|
|
||||||
storedProcedures: 0,
|
|
||||||
triggers: 0,
|
|
||||||
functions: 0,
|
|
||||||
documentsSize: 0,
|
|
||||||
documentsCount: 0,
|
|
||||||
collectionSize: 0,
|
|
||||||
usageSizeInKB: 0,
|
|
||||||
numPartitions: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("Conflict Resolution", () => {
|
|
||||||
describe("should show conflict resolution", () => {
|
|
||||||
let explorer: Explorer;
|
|
||||||
const baseCollectionWithoutConflict: DataModels.Collection = {
|
|
||||||
defaultTtl: 200,
|
|
||||||
partitionKey: null,
|
|
||||||
conflictResolutionPolicy: null,
|
|
||||||
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
|
||||||
_rid: "",
|
|
||||||
_self: "",
|
|
||||||
_etag: "",
|
|
||||||
_ts: 0,
|
|
||||||
id: "mycoll"
|
|
||||||
};
|
|
||||||
const getSettingsTab = (conflictResolution: boolean = true) => {
|
|
||||||
return new SettingsTab({
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
|
||||||
title: "Scale & Settings",
|
|
||||||
tabPath: "",
|
|
||||||
hashLocation: "",
|
|
||||||
isActive: ko.observable(false),
|
|
||||||
collection: new Collection(
|
|
||||||
explorer,
|
|
||||||
"mydb",
|
|
||||||
conflictResolution ? baseCollection : baseCollectionWithoutConflict,
|
|
||||||
quotaInfo,
|
|
||||||
null
|
|
||||||
),
|
|
||||||
onUpdateTabsButtons: undefined
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
explorer = new Explorer();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("single master, should not show conflict resolution", () => {
|
|
||||||
const settingsTab = getSettingsTab();
|
|
||||||
expect(settingsTab.hasConflictResolution()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("multi master with resolution conflict, show conflict resolution", () => {
|
|
||||||
explorer.databaseAccount({
|
|
||||||
id: "test",
|
|
||||||
kind: "",
|
|
||||||
location: "",
|
|
||||||
name: "",
|
|
||||||
tags: "",
|
|
||||||
type: "",
|
|
||||||
properties: {
|
|
||||||
enableMultipleWriteLocations: true,
|
|
||||||
documentEndpoint: "",
|
|
||||||
cassandraEndpoint: "",
|
|
||||||
gremlinEndpoint: "",
|
|
||||||
tableEndpoint: ""
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const settingsTab = getSettingsTab();
|
|
||||||
expect(settingsTab.hasConflictResolution()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("multi master without resolution conflict, show conflict resolution", () => {
|
|
||||||
explorer.databaseAccount({
|
|
||||||
id: "test",
|
|
||||||
kind: "",
|
|
||||||
location: "",
|
|
||||||
name: "",
|
|
||||||
tags: "",
|
|
||||||
type: "",
|
|
||||||
properties: {
|
|
||||||
enableMultipleWriteLocations: true,
|
|
||||||
documentEndpoint: "",
|
|
||||||
cassandraEndpoint: "",
|
|
||||||
gremlinEndpoint: "",
|
|
||||||
tableEndpoint: ""
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const settingsTab = getSettingsTab(false /* no resolution conflict*/);
|
|
||||||
expect(settingsTab.hasConflictResolution()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Parse Conflict Resolution Mode from backend", () => {
|
|
||||||
it("should parse any casing", () => {
|
|
||||||
expect(SettingsTab.parseConflictResolutionMode("custom")).toBe(DataModels.ConflictResolutionMode.Custom);
|
|
||||||
expect(SettingsTab.parseConflictResolutionMode("Custom")).toBe(DataModels.ConflictResolutionMode.Custom);
|
|
||||||
expect(SettingsTab.parseConflictResolutionMode("lastWriterWins")).toBe(
|
|
||||||
DataModels.ConflictResolutionMode.LastWriterWins
|
|
||||||
);
|
|
||||||
expect(SettingsTab.parseConflictResolutionMode("LastWriterWins")).toBe(
|
|
||||||
DataModels.ConflictResolutionMode.LastWriterWins
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse empty as null", () => {
|
|
||||||
expect(SettingsTab.parseConflictResolutionMode("")).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse null as null", () => {
|
|
||||||
expect(SettingsTab.parseConflictResolutionMode(null)).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Parse Conflict Resolution procedure from backend", () => {
|
|
||||||
it("should parse path as name", () => {
|
|
||||||
expect(SettingsTab.parseConflictResolutionProcedure("/dbs/xxxx/colls/xxxx/sprocs/validsproc")).toBe(
|
|
||||||
"validsproc"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse name as name", () => {
|
|
||||||
expect(SettingsTab.parseConflictResolutionProcedure("validsproc")).toBe("validsproc");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse invalid path as null", () => {
|
|
||||||
expect(SettingsTab.parseConflictResolutionProcedure("/not/a/valid/path")).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse empty or null as null", () => {
|
|
||||||
expect(SettingsTab.parseConflictResolutionProcedure("")).toBe(null);
|
|
||||||
expect(SettingsTab.parseConflictResolutionProcedure(null)).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Should update collection", () => {
|
|
||||||
let explorer: Explorer;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
explorer = new Explorer();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("On TTL changed", () => {
|
|
||||||
const settingsTab = new SettingsTab({
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
|
||||||
title: "Scale & Settings",
|
|
||||||
tabPath: "",
|
|
||||||
hashLocation: "",
|
|
||||||
isActive: ko.observable(false),
|
|
||||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
|
||||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
|
||||||
settingsTab.timeToLive("off");
|
|
||||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
|
||||||
|
|
||||||
settingsTab.onRevertClick();
|
|
||||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
|
||||||
settingsTab.timeToLiveSeconds(100);
|
|
||||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("On Index Policy changed", () => {
|
|
||||||
const settingsTab = new SettingsTab({
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
|
||||||
title: "Scale & Settings",
|
|
||||||
tabPath: "",
|
|
||||||
hashLocation: "",
|
|
||||||
isActive: ko.observable(false),
|
|
||||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
|
||||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
|
||||||
settingsTab.indexingPolicyContent({ somethingDifferent: "" });
|
|
||||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("On Conflict Resolution Mode changed", () => {
|
|
||||||
const settingsTab = new SettingsTab({
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
|
||||||
title: "Scale & Settings",
|
|
||||||
tabPath: "",
|
|
||||||
hashLocation: "",
|
|
||||||
isActive: ko.observable(false),
|
|
||||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
|
||||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
|
||||||
settingsTab.conflictResolutionPolicyMode(DataModels.ConflictResolutionMode.Custom);
|
|
||||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
|
||||||
|
|
||||||
settingsTab.onRevertClick();
|
|
||||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
|
||||||
settingsTab.conflictResolutionPolicyPath("/somethingElse");
|
|
||||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
|
||||||
|
|
||||||
settingsTab.onRevertClick();
|
|
||||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
|
||||||
settingsTab.conflictResolutionPolicyMode(DataModels.ConflictResolutionMode.Custom);
|
|
||||||
settingsTab.conflictResolutionPolicyProcedure("resolver");
|
|
||||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Get Conflict Resolution configuration from user", () => {
|
|
||||||
let explorer: Explorer;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
explorer = new Explorer();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("null if it didnt change", () => {
|
|
||||||
const settingsTab = new SettingsTab({
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
|
||||||
title: "Scale & Settings",
|
|
||||||
tabPath: "",
|
|
||||||
hashLocation: "",
|
|
||||||
isActive: ko.observable(false),
|
|
||||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
|
||||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Custom contains valid backend path", () => {
|
|
||||||
const settingsTab = new SettingsTab({
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
|
||||||
title: "Scale & Settings",
|
|
||||||
tabPath: "",
|
|
||||||
hashLocation: "",
|
|
||||||
isActive: ko.observable(false),
|
|
||||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
|
||||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
|
|
||||||
settingsTab.conflictResolutionPolicyMode(DataModels.ConflictResolutionMode.Custom);
|
|
||||||
settingsTab.conflictResolutionPolicyProcedure("resolver");
|
|
||||||
let updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
|
||||||
expect(updatedPolicy.mode).toBe(DataModels.ConflictResolutionMode.Custom);
|
|
||||||
expect(updatedPolicy.conflictResolutionProcedure).toBe("/dbs/mydb/colls/mycoll/sprocs/resolver");
|
|
||||||
|
|
||||||
settingsTab.conflictResolutionPolicyProcedure("");
|
|
||||||
updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
|
||||||
expect(updatedPolicy.conflictResolutionProcedure).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("LWW contains valid property path", () => {
|
|
||||||
const settingsTab = new SettingsTab({
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
|
||||||
title: "Scale & Settings",
|
|
||||||
tabPath: "",
|
|
||||||
hashLocation: "",
|
|
||||||
isActive: ko.observable(false),
|
|
||||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
|
||||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
|
|
||||||
settingsTab.conflictResolutionPolicyPath("someAttr");
|
|
||||||
let updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
|
||||||
expect(updatedPolicy.conflictResolutionPath).toBe("/someAttr");
|
|
||||||
|
|
||||||
settingsTab.conflictResolutionPolicyPath("/someAttr");
|
|
||||||
updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
|
||||||
expect(updatedPolicy.conflictResolutionPath).toBe("/someAttr");
|
|
||||||
|
|
||||||
settingsTab.conflictResolutionPolicyPath("");
|
|
||||||
updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
|
||||||
expect(updatedPolicy.conflictResolutionPath).toBe("");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("partitionKeyVisible", () => {
|
|
||||||
enum PartitionKeyOption {
|
|
||||||
None,
|
|
||||||
System,
|
|
||||||
NonSystem
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCollection(defaultApi: string, partitionKeyOption: PartitionKeyOption) {
|
|
||||||
const explorer = new Explorer();
|
|
||||||
explorer.defaultExperience(defaultApi);
|
|
||||||
|
|
||||||
const offer: DataModels.Offer = null;
|
|
||||||
const defaultTtl = 200;
|
|
||||||
const conflictResolutionPolicy = {
|
|
||||||
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
|
||||||
conflictResolutionPath: "/_ts"
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Collection(
|
|
||||||
explorer,
|
|
||||||
"mydb",
|
|
||||||
{
|
|
||||||
defaultTtl: defaultTtl,
|
|
||||||
partitionKey:
|
|
||||||
partitionKeyOption != PartitionKeyOption.None
|
|
||||||
? {
|
|
||||||
paths: ["/foo"],
|
|
||||||
kind: "Hash",
|
|
||||||
version: 2,
|
|
||||||
systemKey: partitionKeyOption === PartitionKeyOption.System
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
conflictResolutionPolicy: conflictResolutionPolicy,
|
|
||||||
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
|
||||||
_rid: "",
|
|
||||||
_self: "",
|
|
||||||
_etag: "",
|
|
||||||
_ts: 0,
|
|
||||||
id: "mycoll"
|
|
||||||
},
|
|
||||||
quotaInfo,
|
|
||||||
offer
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSettingsTab(defaultApi: string, partitionKeyOption: PartitionKeyOption): SettingsTab {
|
|
||||||
return new SettingsTab({
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
|
||||||
title: "Scale & Settings",
|
|
||||||
tabPath: "",
|
|
||||||
hashLocation: "",
|
|
||||||
isActive: ko.observable(false),
|
|
||||||
collection: getCollection(defaultApi, partitionKeyOption),
|
|
||||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
it("on SQL container with no partition key should be false", () => {
|
|
||||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.DocumentDB, PartitionKeyOption.None);
|
|
||||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on Mongo container with no partition key should be false", () => {
|
|
||||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.MongoDB, PartitionKeyOption.None);
|
|
||||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on Gremlin container with no partition key should be false", () => {
|
|
||||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Graph, PartitionKeyOption.None);
|
|
||||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on Cassandra container with no partition key should be false", () => {
|
|
||||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Cassandra, PartitionKeyOption.None);
|
|
||||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on Table container with no partition key should be false", () => {
|
|
||||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Table, PartitionKeyOption.None);
|
|
||||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on SQL container with system partition key should be true", () => {
|
|
||||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.DocumentDB, PartitionKeyOption.System);
|
|
||||||
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on Mongo container with system partition key should be false", () => {
|
|
||||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.MongoDB, PartitionKeyOption.System);
|
|
||||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on Gremlin container with system partition key should be true", () => {
|
|
||||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Graph, PartitionKeyOption.System);
|
|
||||||
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on Cassandra container with system partition key should be false", () => {
|
|
||||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Cassandra, PartitionKeyOption.System);
|
|
||||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on Table container with system partition key should be false", () => {
|
|
||||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Table, PartitionKeyOption.System);
|
|
||||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on SQL container with non-system partition key should be true", () => {
|
|
||||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.DocumentDB, PartitionKeyOption.NonSystem);
|
|
||||||
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on Mongo container with non-system partition key should be true", () => {
|
|
||||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.MongoDB, PartitionKeyOption.NonSystem);
|
|
||||||
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on Gremlin container with non-system partition key should be true", () => {
|
|
||||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Graph, PartitionKeyOption.NonSystem);
|
|
||||||
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on Cassandra container with non-system partition key should be false", () => {
|
|
||||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Cassandra, PartitionKeyOption.NonSystem);
|
|
||||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("on Table container with non-system partition key should be false", () => {
|
|
||||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Table, PartitionKeyOption.NonSystem);
|
|
||||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -47,7 +47,7 @@ export default class SettingsTabV2 extends TabsBase {
|
|||||||
this.currentCollection.loadOffer().then(
|
this.currentCollection.loadOffer().then(
|
||||||
() => {
|
() => {
|
||||||
// passed in options and set by parent as "Settings" by default
|
// passed in options and set by parent as "Settings" by default
|
||||||
this.tabTitle("Scale & Settings");
|
this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings");
|
||||||
this.offerRead(true);
|
this.offerRead(true);
|
||||||
this.options.getPendingNotification.then(
|
this.options.getPendingNotification.then(
|
||||||
(data: DataModels.Notification) => {
|
(data: DataModels.Notification) => {
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ import SparkMasterTabTemplate from "./SparkMasterTab.html";
|
|||||||
import NotebookV2TabTemplate from "./NotebookV2Tab.html";
|
import NotebookV2TabTemplate from "./NotebookV2Tab.html";
|
||||||
import TerminalTabTemplate from "./TerminalTab.html";
|
import TerminalTabTemplate from "./TerminalTab.html";
|
||||||
import MongoDocumentsTabTemplate from "./MongoDocumentsTab.html";
|
import MongoDocumentsTabTemplate from "./MongoDocumentsTab.html";
|
||||||
import MongoDocumentsTabV2Template from "./MongoDocumentsTabV2.html";
|
|
||||||
import MongoQueryTabTemplate from "./MongoQueryTab.html";
|
import MongoQueryTabTemplate from "./MongoQueryTab.html";
|
||||||
import MongoShellTabTemplate from "./MongoShellTab.html";
|
import MongoShellTabTemplate from "./MongoShellTab.html";
|
||||||
import QueryTabTemplate from "./QueryTab.html";
|
import QueryTabTemplate from "./QueryTab.html";
|
||||||
import QueryTablesTabTemplate from "./QueryTablesTab.html";
|
import QueryTablesTabTemplate from "./QueryTablesTab.html";
|
||||||
import SettingsTabTemplate from "./SettingsTab.html";
|
|
||||||
import SettingsTabV2Template from "./SettingsTabV2.html";
|
import SettingsTabV2Template from "./SettingsTabV2.html";
|
||||||
import DatabaseSettingsTabTemplate from "./DatabaseSettingsTab.html";
|
import DatabaseSettingsTabTemplate from "./DatabaseSettingsTab.html";
|
||||||
import StoredProcedureTabTemplate from "./StoredProcedureTab.html";
|
import StoredProcedureTabTemplate from "./StoredProcedureTab.html";
|
||||||
@@ -107,15 +105,6 @@ export class MongoQueryTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MongoDocumentsTabV2 {
|
|
||||||
constructor() {
|
|
||||||
return {
|
|
||||||
viewModel: TabComponent,
|
|
||||||
template: MongoDocumentsTabV2Template
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MongoShellTab {
|
export class MongoShellTab {
|
||||||
constructor() {
|
constructor() {
|
||||||
return {
|
return {
|
||||||
@@ -143,15 +132,6 @@ export class QueryTablesTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SettingsTab {
|
|
||||||
constructor() {
|
|
||||||
return {
|
|
||||||
viewModel: TabComponent,
|
|
||||||
template: SettingsTabTemplate
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SettingsTabV2 {
|
export class SettingsTabV2 {
|
||||||
constructor() {
|
constructor() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ describe("Collection", () => {
|
|||||||
container: Explorer,
|
container: Explorer,
|
||||||
databaseId: string,
|
databaseId: string,
|
||||||
data: DataModels.Collection,
|
data: DataModels.Collection,
|
||||||
quotaInfo: DataModels.CollectionQuotaInfo,
|
|
||||||
offer: DataModels.Offer
|
offer: DataModels.Offer
|
||||||
): Collection {
|
): Collection {
|
||||||
return new Collection(container, databaseId, data, quotaInfo, offer);
|
return new Collection(container, databaseId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateMockCollectionsDataModelWithPartitionKey(
|
function generateMockCollectionsDataModelWithPartitionKey(
|
||||||
@@ -50,7 +49,7 @@ describe("Collection", () => {
|
|||||||
});
|
});
|
||||||
mockContainer.deleteCollectionText = ko.observable<string>("delete collection");
|
mockContainer.deleteCollectionText = ko.observable<string>("delete collection");
|
||||||
|
|
||||||
return generateCollection(mockContainer, "abc", data, {} as DataModels.CollectionQuotaInfo, {} as DataModels.Offer);
|
return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Partition key path parsing", () => {
|
describe("Partition key path parsing", () => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { readTriggers } from "../../Common/dataAccess/readTriggers";
|
|||||||
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
|
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
|
||||||
import { createDocument } from "../../Common/DocumentClientUtilityBase";
|
import { createDocument } from "../../Common/DocumentClientUtilityBase";
|
||||||
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
|
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
|
||||||
import { readCollectionQuotaInfo } from "../../Common/dataAccess/readCollectionQuotaInfo";
|
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
@@ -24,13 +24,11 @@ import ConflictsTab from "../Tabs/ConflictsTab";
|
|||||||
import DocumentsTab from "../Tabs/DocumentsTab";
|
import DocumentsTab from "../Tabs/DocumentsTab";
|
||||||
import GraphTab from "../Tabs/GraphTab";
|
import GraphTab from "../Tabs/GraphTab";
|
||||||
import MongoDocumentsTab from "../Tabs/MongoDocumentsTab";
|
import MongoDocumentsTab from "../Tabs/MongoDocumentsTab";
|
||||||
import MongoDocumentsTabV2 from "../Tabs/MongoDocumentsTabV2";
|
|
||||||
import MongoQueryTab from "../Tabs/MongoQueryTab";
|
import MongoQueryTab from "../Tabs/MongoQueryTab";
|
||||||
import MongoShellTab from "../Tabs/MongoShellTab";
|
import MongoShellTab from "../Tabs/MongoShellTab";
|
||||||
import QueryTab from "../Tabs/QueryTab";
|
import QueryTab from "../Tabs/QueryTab";
|
||||||
import QueryTablesTab from "../Tabs/QueryTablesTab";
|
import QueryTablesTab from "../Tabs/QueryTablesTab";
|
||||||
import SettingsTabV2 from "../Tabs/SettingsTabV2";
|
import SettingsTabV2 from "../Tabs/SettingsTabV2";
|
||||||
import SettingsTab from "../Tabs/SettingsTab";
|
|
||||||
import ConflictId from "./ConflictId";
|
import ConflictId from "./ConflictId";
|
||||||
import DocumentId from "./DocumentId";
|
import DocumentId from "./DocumentId";
|
||||||
import StoredProcedure from "./StoredProcedure";
|
import StoredProcedure from "./StoredProcedure";
|
||||||
@@ -39,7 +37,6 @@ import UserDefinedFunction from "./UserDefinedFunction";
|
|||||||
import { configContext, Platform } from "../../ConfigContext";
|
import { configContext, Platform } from "../../ConfigContext";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import TabsBase from "../Tabs/TabsBase";
|
|
||||||
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
|
||||||
@@ -56,7 +53,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
public defaultTtl: ko.Observable<number>;
|
public defaultTtl: ko.Observable<number>;
|
||||||
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||||
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||||
public quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
|
public usageSizeInKB: ko.Observable<number>;
|
||||||
|
|
||||||
public offer: ko.Observable<DataModels.Offer>;
|
public offer: ko.Observable<DataModels.Offer>;
|
||||||
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||||
public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||||
@@ -97,13 +95,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
public userDefinedFunctionsFocused: ko.Observable<boolean>;
|
public userDefinedFunctionsFocused: ko.Observable<boolean>;
|
||||||
public triggersFocused: ko.Observable<boolean>;
|
public triggersFocused: ko.Observable<boolean>;
|
||||||
|
|
||||||
constructor(
|
constructor(container: Explorer, databaseId: string, data: DataModels.Collection) {
|
||||||
container: Explorer,
|
|
||||||
databaseId: string,
|
|
||||||
data: DataModels.Collection,
|
|
||||||
quotaInfo: DataModels.CollectionQuotaInfo,
|
|
||||||
offer: DataModels.Offer
|
|
||||||
) {
|
|
||||||
this.nodeKind = "Collection";
|
this.nodeKind = "Collection";
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.self = data._self;
|
this.self = data._self;
|
||||||
@@ -115,8 +107,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
this.id = ko.observable(data.id);
|
this.id = ko.observable(data.id);
|
||||||
this.defaultTtl = ko.observable(data.defaultTtl);
|
this.defaultTtl = ko.observable(data.defaultTtl);
|
||||||
this.indexingPolicy = ko.observable(data.indexingPolicy);
|
this.indexingPolicy = ko.observable(data.indexingPolicy);
|
||||||
this.quotaInfo = ko.observable(quotaInfo);
|
this.usageSizeInKB = ko.observable();
|
||||||
this.offer = ko.observable(offer);
|
this.offer = ko.observable();
|
||||||
this.conflictResolutionPolicy = ko.observable(data.conflictResolutionPolicy);
|
this.conflictResolutionPolicy = ko.observable(data.conflictResolutionPolicy);
|
||||||
this.changeFeedPolicy = ko.observable<DataModels.ChangeFeedPolicy>(data.changeFeedPolicy);
|
this.changeFeedPolicy = ko.observable<DataModels.ChangeFeedPolicy>(data.changeFeedPolicy);
|
||||||
this.analyticalStorageTtl = ko.observable(data.analyticalStorageTtl);
|
this.analyticalStorageTtl = ko.observable(data.analyticalStorageTtl);
|
||||||
@@ -507,11 +499,11 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
dataExplorerArea: Constants.Areas.ResourceTree
|
dataExplorerArea: Constants.Areas.ResourceTree
|
||||||
});
|
});
|
||||||
|
|
||||||
const mongoDocumentsTabs: MongoDocumentsTabV2[] = this.container.tabsManager.getTabs(
|
const mongoDocumentsTabs: MongoDocumentsTab[] = this.container.tabsManager.getTabs(
|
||||||
ViewModels.CollectionTabKind.Documents,
|
ViewModels.CollectionTabKind.Documents,
|
||||||
tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
|
tab => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
|
||||||
) as MongoDocumentsTabV2[];
|
) as MongoDocumentsTab[];
|
||||||
let mongoDocumentsTab: MongoDocumentsTabV2 = mongoDocumentsTabs && mongoDocumentsTabs[0];
|
let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0];
|
||||||
|
|
||||||
if (mongoDocumentsTab) {
|
if (mongoDocumentsTab) {
|
||||||
this.container.tabsManager.activateTab(mongoDocumentsTab);
|
this.container.tabsManager.activateTab(mongoDocumentsTab);
|
||||||
@@ -526,8 +518,9 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
this.documentIds([]);
|
this.documentIds([]);
|
||||||
|
|
||||||
mongoDocumentsTab = new MongoDocumentsTabV2({
|
mongoDocumentsTab = new MongoDocumentsTab({
|
||||||
container: this.container,
|
partitionKey: this.partitionKey,
|
||||||
|
documentIds: this.documentIds,
|
||||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||||
title: "Documents",
|
title: "Documents",
|
||||||
tabPath: "",
|
tabPath: "",
|
||||||
@@ -556,11 +549,6 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
dataExplorerArea: Constants.Areas.ResourceTree
|
dataExplorerArea: Constants.Areas.ResourceTree
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSettingsV2Enabled = this.container.isSettingsV2Enabled();
|
|
||||||
if (!isSettingsV2Enabled) {
|
|
||||||
await this.loadOffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
|
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
|
||||||
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
|
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
|
||||||
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => {
|
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => {
|
||||||
@@ -587,68 +575,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons
|
onUpdateTabsButtons: this.container.onUpdateTabsButtons
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isSettingsV2Enabled) {
|
let settingsTabV2 = matchingTabs && (matchingTabs[0] as SettingsTabV2);
|
||||||
let settingsTabV2 = matchingTabs && (matchingTabs[0] as SettingsTabV2);
|
this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions, pendingNotificationsPromise);
|
||||||
this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions, pendingNotificationsPromise);
|
|
||||||
} else {
|
|
||||||
let settingsTab = matchingTabs && (matchingTabs[0] as SettingsTab);
|
|
||||||
this.launchSettingsTabV1(settingsTab, traceStartData, settingsTabOptions, pendingNotificationsPromise);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private launchSettingsTabV1 = (
|
|
||||||
settingsTab: SettingsTab,
|
|
||||||
traceStartData: any,
|
|
||||||
settingsTabOptions: ViewModels.TabOptions,
|
|
||||||
getPendingNotification: Q.Promise<DataModels.Notification>
|
|
||||||
): void => {
|
|
||||||
if (!settingsTab) {
|
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, traceStartData);
|
|
||||||
settingsTabOptions.onLoadStartKey = startKey;
|
|
||||||
|
|
||||||
getPendingNotification.then(
|
|
||||||
(data: any) => {
|
|
||||||
const pendingNotification: DataModels.Notification = data && data[0];
|
|
||||||
settingsTabOptions.tabKind = ViewModels.CollectionTabKind.Settings;
|
|
||||||
settingsTab = new SettingsTab(settingsTabOptions);
|
|
||||||
this.container.tabsManager.activateNewTab(settingsTab);
|
|
||||||
settingsTab.pendingNotification(pendingNotification);
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
const errorMessage = getErrorMessage(error);
|
|
||||||
TelemetryProcessor.traceFailure(
|
|
||||||
Action.Tab,
|
|
||||||
{
|
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
|
||||||
databaseName: this.databaseId,
|
|
||||||
collectionName: this.id(),
|
|
||||||
defaultExperience: this.container.defaultExperience(),
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: settingsTabOptions.title,
|
|
||||||
error: errorMessage,
|
|
||||||
errorStack: getErrorStack(error)
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Error while fetching container settings for container ${this.id()}: ${errorMessage}`
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
getPendingNotification.then(
|
|
||||||
(pendingNotification: DataModels.Notification) => {
|
|
||||||
settingsTab.pendingNotification(pendingNotification);
|
|
||||||
this.container.tabsManager.activateTab(settingsTab);
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
settingsTab.pendingNotification(undefined);
|
|
||||||
this.container.tabsManager.activateTab(settingsTab);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private launchSettingsTabV2 = (
|
private launchSettingsTabV2 = (
|
||||||
@@ -673,14 +601,6 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private async loadCollectionQuotaInfo(): Promise<void> {
|
|
||||||
// TODO: Use the collection entity cache to get quota info
|
|
||||||
const quotaInfoWithUniqueKeyPolicy = await readCollectionQuotaInfo(this);
|
|
||||||
this.uniqueKeyPolicy = quotaInfoWithUniqueKeyPolicy.uniqueKeyPolicy;
|
|
||||||
const quotaInfo = _.omit(quotaInfoWithUniqueKeyPolicy, "uniqueKeyPolicy");
|
|
||||||
this.quotaInfo(quotaInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
|
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
|
||||||
const collection: ViewModels.Collection = source.collection || source;
|
const collection: ViewModels.Collection = source.collection || source;
|
||||||
const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1;
|
||||||
@@ -1353,7 +1273,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this.offer(await readCollectionOffer(params));
|
this.offer(await readCollectionOffer(params));
|
||||||
await this.loadCollectionQuotaInfo();
|
this.usageSizeInKB(await getCollectionUsageSizeInKB(this.databaseId, this.id()));
|
||||||
|
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
Action.LoadOffers,
|
Action.LoadOffers,
|
||||||
@@ -1361,8 +1281,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
databaseAccountName: this.container.databaseAccount().name,
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
databaseName: this.databaseId,
|
databaseName: this.databaseId,
|
||||||
collectionName: this.id(),
|
collectionName: this.id(),
|
||||||
defaultExperience: this.container.defaultExperience(),
|
defaultExperience: this.container.defaultExperience()
|
||||||
offerVersion: this.offer()?.offerVersion
|
|
||||||
},
|
},
|
||||||
startKey
|
startKey
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export default class Database implements ViewModels.Database {
|
|||||||
});
|
});
|
||||||
|
|
||||||
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
|
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
|
||||||
const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
|
const collectionVM: Collection = new Collection(this.container, this.id(), collection);
|
||||||
collectionVMs.push(collectionVM);
|
collectionVMs.push(collectionVM);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -229,9 +229,7 @@ const createMockCollection = (): ViewModels.Collection => {
|
|||||||
const mockCollectionVM: ViewModels.Collection = new Collection(
|
const mockCollectionVM: ViewModels.Collection = new Collection(
|
||||||
createMockContainer(),
|
createMockContainer(),
|
||||||
"fakeDatabaseId",
|
"fakeDatabaseId",
|
||||||
mockCollection,
|
mockCollection
|
||||||
undefined,
|
|
||||||
undefined
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return mockCollectionVM;
|
return mockCollectionVM;
|
||||||
|
|||||||
@@ -178,8 +178,7 @@ export class JunoClient {
|
|||||||
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`);
|
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// will be renamed once feature.enableCodeOfConduct flag is removed
|
public async getPublicGalleryData(): Promise<IJunoResponse<IPublicGalleryData>> {
|
||||||
public async fetchPublicNotebooks(): Promise<IJunoResponse<IPublicGalleryData>> {
|
|
||||||
const url = `${this.getNotebooksAccountUrl()}/gallery/public`;
|
const url = `${this.getNotebooksAccountUrl()}/gallery/public`;
|
||||||
const response = await window.fetch(url, {
|
const response = await window.fetch(url, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
@@ -405,7 +404,7 @@ export class JunoClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async reportAbuse(notebookId: string, abuseCategory: string, notes: string): Promise<IJunoResponse<boolean>> {
|
public async reportAbuse(notebookId: string, abuseCategory: string, notes: string): Promise<IJunoResponse<boolean>> {
|
||||||
const response = await window.fetch(`${this.getNotebooksUrl()}/avert/reportAbuse`, {
|
const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/reportAbuse`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
notebookId,
|
notebookId,
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
|||||||
export class NotebookWorkspaceManager {
|
export class NotebookWorkspaceManager {
|
||||||
private resourceProviderClientFactory: IResourceProviderClientFactory<any>;
|
private resourceProviderClientFactory: IResourceProviderClientFactory<any>;
|
||||||
|
|
||||||
constructor(private _armEndpoint: string) {
|
constructor() {
|
||||||
this.resourceProviderClientFactory = new ResourceProviderClientFactory(this._armEndpoint);
|
this.resourceProviderClientFactory = new ResourceProviderClientFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getNotebookWorkspacesAsync(cosmosdbResourceId: string): Promise<NotebookWorkspace[]> {
|
public async getNotebookWorkspacesAsync(cosmosdbResourceId: string): Promise<NotebookWorkspace[]> {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function initializeExplorer(): Explorer {
|
|||||||
cassandraEndpoint: ""
|
cassandraEndpoint: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
explorer.isAccountReady(true);
|
explorer.isAccountReady(true);
|
||||||
return explorer;
|
return explorer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ export default class Main {
|
|||||||
masterKey?: string /* master key extracted from connection string if available */,
|
masterKey?: string /* master key extracted from connection string if available */,
|
||||||
account?: DatabaseAccount,
|
account?: DatabaseAccount,
|
||||||
authorizationToken?: string /* access key */
|
authorizationToken?: string /* access key */
|
||||||
): Q.Promise<void> {
|
): void {
|
||||||
const serverId: string = AuthHeadersUtil.serverId;
|
const serverId: string = AuthHeadersUtil.serverId;
|
||||||
const authType: string = (<any>window).authType;
|
const authType: string = (<any>window).authType;
|
||||||
const accountResourceId =
|
const accountResourceId =
|
||||||
@@ -373,7 +373,7 @@ export default class Main {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Q.reject(`Unsupported AuthType ${authType}`);
|
throw new Error(`Unsupported AuthType ${authType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static _instantiateExplorer(): Explorer {
|
private static _instantiateExplorer(): Explorer {
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
import "../../Explorer/Tables/DataTable/DataTableBindingManager";
|
import "../../Explorer/Tables/DataTable/DataTableBindingManager";
|
||||||
import Explorer from "../../Explorer/Explorer";
|
import Explorer from "../../Explorer/Explorer";
|
||||||
|
import { handleMessage } from "../../Controls/Heatmap/Heatmap";
|
||||||
|
|
||||||
export function initializeExplorer(): Explorer {
|
export function initializeExplorer(): Explorer {
|
||||||
const explorer = new Explorer();
|
const explorer = new Explorer();
|
||||||
|
|
||||||
|
// In development mode, try to load the iframe message from session storage.
|
||||||
|
// This allows webpack hot reload to funciton properly
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage");
|
||||||
|
if (initMessage) {
|
||||||
|
const message = JSON.parse(initMessage);
|
||||||
|
console.warn("Loaded cached portal iframe message from session storage");
|
||||||
|
console.dir(message);
|
||||||
|
explorer.initDataExplorerWithFrameInputs(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
|
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
|
||||||
|
|
||||||
return explorer;
|
return explorer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
|
import { configContext } from "../ConfigContext";
|
||||||
import { IResourceProviderClientFactory, IResourceProviderClient } from "./IResourceProviderClient";
|
import { IResourceProviderClientFactory, IResourceProviderClient } from "./IResourceProviderClient";
|
||||||
import { ResourceProviderClient } from "./ResourceProviderClient";
|
import { ResourceProviderClient } from "./ResourceProviderClient";
|
||||||
|
|
||||||
export class ResourceProviderClientFactory implements IResourceProviderClientFactory<any> {
|
export class ResourceProviderClientFactory implements IResourceProviderClientFactory<any> {
|
||||||
|
private armEndpoint: string;
|
||||||
private cachedClients: { [url: string]: IResourceProviderClient<any> } = {};
|
private cachedClients: { [url: string]: IResourceProviderClient<any> } = {};
|
||||||
|
|
||||||
constructor(private armEndpoint: string) {}
|
constructor() {
|
||||||
|
this.armEndpoint = configContext.ARM_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
public getOrCreate(url: string): IResourceProviderClient<any> {
|
public getOrCreate(url: string): IResourceProviderClient<any> {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
|
|||||||
@@ -126,7 +126,6 @@ export class OfferPricing {
|
|||||||
Standard: {
|
Standard: {
|
||||||
StartingPrice: 24 / hoursInAMonth, // per hour
|
StartingPrice: 24 / hoursInAMonth, // per hour
|
||||||
PricePerRU: 0.00008,
|
PricePerRU: 0.00008,
|
||||||
PricePerRUPM: (10 * 2) / 1000 / hoursInAMonth, // preview price: $2 per 1000 RU/m per month -> 100 RU/s
|
|
||||||
PricePerGB: 0.25 / hoursInAMonth
|
PricePerGB: 0.25 / hoursInAMonth
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -139,24 +138,18 @@ export class OfferPricing {
|
|||||||
Standard: {
|
Standard: {
|
||||||
StartingPrice: OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice / hoursInAMonth, // per hour
|
StartingPrice: OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice / hoursInAMonth, // per hour
|
||||||
PricePerRU: 0.00051,
|
PricePerRU: 0.00051,
|
||||||
PricePerRUPM: (10 * 20) / 1000 / hoursInAMonth, // preview price: 20rmb per 1000 RU/m per month -> 100 RU/s
|
|
||||||
PricePerGB: OfferPricing.MonthlyPricing.mooncake.Standard.PricePerGB / hoursInAMonth
|
PricePerGB: OfferPricing.MonthlyPricing.mooncake.Standard.PricePerGB / hoursInAMonth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GeneralResources {
|
|
||||||
public static loadingText: string = "Loading...";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CollectionCreation {
|
export class CollectionCreation {
|
||||||
// TODO generate these values based on Product\Services\Documents\ImageStore\GatewayApplication\Settings.xml
|
// TODO generate these values based on Product\Services\Documents\ImageStore\GatewayApplication\Settings.xml
|
||||||
public static readonly MinRUPerPartitionBelow7Partitions: number = 400;
|
public static readonly MinRUPerPartitionBelow7Partitions: number = 400;
|
||||||
public static readonly MinRU7PartitionsTo25Partitions: number = 2500;
|
public static readonly MinRU7PartitionsTo25Partitions: number = 2500;
|
||||||
public static readonly MinRUPerPartitionAbove25Partitions: number = 100;
|
public static readonly MinRUPerPartitionAbove25Partitions: number = 100;
|
||||||
public static readonly MaxRUPerPartition: number = 10000;
|
public static readonly MaxRUPerPartition: number = 10000;
|
||||||
public static readonly MaxRUPMPerPartition: number = 5000;
|
|
||||||
public static readonly MinPartitionedCollectionRUs: number = 2500;
|
public static readonly MinPartitionedCollectionRUs: number = 2500;
|
||||||
|
|
||||||
public static readonly NumberOfPartitionsInFixedCollection: number = 1;
|
public static readonly NumberOfPartitionsInFixedCollection: number = 1;
|
||||||
@@ -231,32 +224,6 @@ export class IndexingPolicies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SubscriptionUtilMappings {
|
export class SubscriptionUtilMappings {
|
||||||
// TODO: Expose this through a web API from the portal
|
|
||||||
public static SubscriptionTypeMap: { [key: string]: SubscriptionType } = {
|
|
||||||
"AAD_2015-09-01": SubscriptionType.Free,
|
|
||||||
"AzureDynamics_2014-09-01": SubscriptionType.Free,
|
|
||||||
"AzureInOpen_2014-09-01": SubscriptionType.EA,
|
|
||||||
"AzurePass_2014-09-01": SubscriptionType.Free,
|
|
||||||
"BackupStorage_2014-09-01": SubscriptionType.PAYG,
|
|
||||||
"BizSpark_2014-09-01": SubscriptionType.Benefits,
|
|
||||||
"BizSparkPlus_2014-09-01": SubscriptionType.Benefits,
|
|
||||||
"CSP_2015-05-01": SubscriptionType.EA,
|
|
||||||
"Default_2014-09-01": SubscriptionType.PAYG,
|
|
||||||
"DevEssentials_2016-01-01": SubscriptionType.Benefits,
|
|
||||||
"DreamSpark_2015-02-01": SubscriptionType.Benefits,
|
|
||||||
"EnterpriseAgreement_2014-09-01": SubscriptionType.EA,
|
|
||||||
"FreeTrial_2014-09-01": SubscriptionType.Free,
|
|
||||||
"Internal_2014-09-01": SubscriptionType.Internal,
|
|
||||||
"LegacyMonetaryCommitment_2014-09-01": SubscriptionType.EA,
|
|
||||||
"LightweightTrial_2016-09-01": SubscriptionType.Free,
|
|
||||||
"MonetaryCommitment_2015-05-01": SubscriptionType.EA,
|
|
||||||
"MPN_2014-09-01": SubscriptionType.Benefits,
|
|
||||||
"MSDN_2014-09-01": SubscriptionType.Benefits,
|
|
||||||
"MSDNDevTest_2014-09-01": SubscriptionType.Benefits,
|
|
||||||
"PayAsYouGo_2014-09-01": SubscriptionType.PAYG,
|
|
||||||
"Sponsored_2016-01-01": SubscriptionType.Benefits
|
|
||||||
};
|
|
||||||
|
|
||||||
public static FreeTierSubscriptionIds: string[] = [
|
public static FreeTierSubscriptionIds: string[] = [
|
||||||
"b8f2ff04-0a81-4cf9-95ef-5828d16981d2",
|
"b8f2ff04-0a81-4cf9-95ef-5828d16981d2",
|
||||||
"39b1fdff-e5b2-4f83-adb4-33cb3aabf5ea",
|
"39b1fdff-e5b2-4f83-adb4-33cb3aabf5ea",
|
||||||
@@ -267,57 +234,6 @@ export class SubscriptionUtilMappings {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Offers {
|
|
||||||
public static offerTypeS1: string = "S1";
|
|
||||||
public static offerTypeS2: string = "S2";
|
|
||||||
public static offerTypeS3: string = "S3";
|
|
||||||
public static offerTypeStandard: string = "Standard";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class OfferThoughput {
|
|
||||||
public static offerS1Throughput: number = 250;
|
|
||||||
public static offerS2Throughput: number = 1000;
|
|
||||||
public static offerS3Throughput: number = 2500;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class OfferVersions {
|
|
||||||
public static offerV1: string = "V1";
|
|
||||||
public static offerV2: string = "V2";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class InvalidOffers {
|
|
||||||
public static offerTypeInvalid: string = "Invalid";
|
|
||||||
public static offerTypeError: string = "Loading Error";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SpecTypes {
|
|
||||||
public static collection: string = "DocumentDbCollection";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CurrencyCodes {
|
|
||||||
public static usd: string = "USD";
|
|
||||||
public static rmb: string = "RMB";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ColorSchemes {
|
|
||||||
public static standard: string = "mediumBlue";
|
|
||||||
public static legacy: string = "yellowGreen";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FeatureIds {
|
|
||||||
public static storage: string = "storage";
|
|
||||||
public static sla: string = "sla";
|
|
||||||
public static partitioned: string = "partitioned";
|
|
||||||
public static singlePartitioned: string = "singlePartition";
|
|
||||||
public static legacySinglePartitioned: string = "legacySinglePartition";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FeatureIconNames {
|
|
||||||
public static storage: string = "SSD";
|
|
||||||
public static sla: string = "Monitoring";
|
|
||||||
public static productionReady: string = "ProductionReadyDb";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AutopilotDocumentation {
|
export class AutopilotDocumentation {
|
||||||
public static Url: string = "https://aka.ms/cosmos-autoscale-info";
|
public static Url: string = "https://aka.ms/cosmos-autoscale-info";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import * as Constants from "./Constants";
|
import * as Constants from "./Constants";
|
||||||
|
|
||||||
export function computeRUUsagePrice(serverId: string, rupmEnabled: boolean, requestUnits: number): string {
|
export function computeRUUsagePrice(serverId: string, requestUnits: number): string {
|
||||||
if (serverId === "mooncake") {
|
if (serverId === "mooncake") {
|
||||||
let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU,
|
let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU;
|
||||||
rupmCharge = rupmEnabled ? requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRUPM : 0;
|
return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency;
|
||||||
return (
|
|
||||||
calculateEstimateNumber(ruCharge + rupmCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU,
|
let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU;
|
||||||
rupmCharge = rupmEnabled ? requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRUPM : 0;
|
return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency;
|
||||||
return calculateEstimateNumber(ruCharge + rupmCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeStorageUsagePrice(serverId: string, storageUsedRoundUpToGB: number): string {
|
export function computeStorageUsagePrice(serverId: string, storageUsedRoundUpToGB: number): string {
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ import { ArmApiVersions, ArmResourceTypes } from "../Common/Constants";
|
|||||||
import { IResourceProviderClient, IResourceProviderClientFactory } from "../ResourceProvider/IResourceProviderClient";
|
import { IResourceProviderClient, IResourceProviderClientFactory } from "../ResourceProvider/IResourceProviderClient";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory";
|
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory";
|
||||||
import { configContext } from "../ConfigContext";
|
|
||||||
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
||||||
|
|
||||||
export class ArcadiaResourceManager {
|
export class ArcadiaResourceManager {
|
||||||
private resourceProviderClientFactory: IResourceProviderClientFactory<any>;
|
private resourceProviderClientFactory: IResourceProviderClientFactory<any>;
|
||||||
|
|
||||||
constructor(private armEndpoint = configContext.ARM_ENDPOINT) {
|
constructor() {
|
||||||
this.resourceProviderClientFactory = new ResourceProviderClientFactory(this.armEndpoint);
|
this.resourceProviderClientFactory = new ResourceProviderClientFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getWorkspacesAsync(arcadiaResourceId: string): Promise<ArcadiaWorkspace[]> {
|
public async getWorkspacesAsync(arcadiaResourceId: string): Promise<ArcadiaWorkspace[]> {
|
||||||
|
|||||||
@@ -59,9 +59,8 @@ const main = async (): Promise<void> => {
|
|||||||
|
|
||||||
const serverSettings = createServerSettings(urlVars);
|
const serverSettings = createServerSettings(urlVars);
|
||||||
|
|
||||||
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, {
|
const data = { baseUrl: serverSettings.baseUrl };
|
||||||
baseUrl: serverSettings.baseUrl
|
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (urlVars.hasOwnProperty(TerminalQueryParams.Terminal)) {
|
if (urlVars.hasOwnProperty(TerminalQueryParams.Terminal)) {
|
||||||
@@ -70,9 +69,9 @@ const main = async (): Promise<void> => {
|
|||||||
throw new Error("Only terminal is supported");
|
throw new Error("Only terminal is supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
TelemetryProcessor.traceSuccess(Action.OpenTerminal, startTime);
|
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
TelemetryProcessor.traceFailure(Action.OpenTerminal, startTime);
|
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface UserContext {
|
|||||||
defaultExperience?: DefaultAccountExperienceType;
|
defaultExperience?: DefaultAccountExperienceType;
|
||||||
useSDKOperations?: boolean;
|
useSDKOperations?: boolean;
|
||||||
subscriptionType?: SubscriptionType;
|
subscriptionType?: SubscriptionType;
|
||||||
|
quotaId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userContext: Readonly<UserContext> = {} as const;
|
const userContext: Readonly<UserContext> = {} as const;
|
||||||
|
|||||||
@@ -7,15 +7,6 @@ export const minAutoPilotThroughput = 4000;
|
|||||||
|
|
||||||
export const autoPilotIncrementStep = 1000;
|
export const autoPilotIncrementStep = 1000;
|
||||||
|
|
||||||
export function isValidV3AutoPilotOffer(offer: Offer): boolean {
|
|
||||||
const maxThroughput =
|
|
||||||
offer &&
|
|
||||||
offer.content &&
|
|
||||||
offer.content.offerAutopilotSettings &&
|
|
||||||
offer.content.offerAutopilotSettings.maxThroughput;
|
|
||||||
return isValidAutoPilotThroughput(maxThroughput);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
||||||
if (!maxThroughput) {
|
if (!maxThroughput) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Explorer from "../Explorer/Explorer";
|
|||||||
import { IChoiceGroupOption, IChoiceGroupProps } from "office-ui-fabric-react";
|
import { IChoiceGroupOption, IChoiceGroupProps } from "office-ui-fabric-react";
|
||||||
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
|
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
|
||||||
import { handleError } from "../Common/ErrorHandlingUtils";
|
import { handleError } from "../Common/ErrorHandlingUtils";
|
||||||
|
import { HttpStatusCodes } from "../Common/Constants";
|
||||||
|
|
||||||
const defaultSelectedAbuseCategory = "Other";
|
const defaultSelectedAbuseCategory = "Other";
|
||||||
const abuseCategories: IChoiceGroupOption[] = [
|
const abuseCategories: IChoiceGroupOption[] = [
|
||||||
@@ -113,7 +114,7 @@ export function reportAbuse(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await junoClient.reportAbuse(notebookId, abuseCategory, additionalDetails);
|
const response = await junoClient.reportAbuse(notebookId, abuseCategory, additionalDetails);
|
||||||
if (!response.data) {
|
if (response.status !== HttpStatusCodes.Accepted) {
|
||||||
throw new Error(`Received HTTP ${response.status} when submitting report for ${data.name}`);
|
throw new Error(`Received HTTP ${response.status} when submitting report for ${data.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import * as Constants from "../../src/Common/Constants";
|
|
||||||
import * as DataModels from "../../src/Contracts/DataModels";
|
|
||||||
import { OfferUtils } from "../../src/Utils/OfferUtils";
|
|
||||||
|
|
||||||
describe("OfferUtils tests", () => {
|
|
||||||
const offerV1: DataModels.Offer = {
|
|
||||||
_rid: "",
|
|
||||||
_self: "",
|
|
||||||
_ts: 0,
|
|
||||||
_etag: "",
|
|
||||||
id: "v1",
|
|
||||||
offerVersion: Constants.OfferVersions.V1,
|
|
||||||
offerType: "Standard",
|
|
||||||
offerResourceId: "",
|
|
||||||
content: null,
|
|
||||||
resource: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
const offerV2: DataModels.Offer = {
|
|
||||||
_rid: "",
|
|
||||||
_self: "",
|
|
||||||
_ts: 0,
|
|
||||||
_etag: "",
|
|
||||||
id: "v1",
|
|
||||||
offerVersion: Constants.OfferVersions.V2,
|
|
||||||
offerType: "Standard",
|
|
||||||
offerResourceId: "",
|
|
||||||
content: null,
|
|
||||||
resource: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("isOfferV1()", () => {
|
|
||||||
it("should return true for V1", () => {
|
|
||||||
expect(OfferUtils.isOfferV1(offerV1)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false for V2", () => {
|
|
||||||
expect(OfferUtils.isOfferV1(offerV2)).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("isNotOfferV1()", () => {
|
|
||||||
it("should return true for V2", () => {
|
|
||||||
expect(OfferUtils.isNotOfferV1(offerV2)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false for V1", () => {
|
|
||||||
expect(OfferUtils.isNotOfferV1(offerV1)).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import * as Constants from "../Common/Constants";
|
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
|
||||||
|
|
||||||
export class OfferUtils {
|
|
||||||
public static isOfferV1(offer: DataModels.Offer): boolean {
|
|
||||||
return !offer || offer.offerVersion !== Constants.OfferVersions.V2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static isNotOfferV1(offer: DataModels.Offer): boolean {
|
|
||||||
return !OfferUtils.isOfferV1(offer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,37 +25,37 @@ describe("PricingUtils Tests", () => {
|
|||||||
|
|
||||||
describe("computeRUUsagePriceHourly()", () => {
|
describe("computeRUUsagePriceHourly()", () => {
|
||||||
it("should return 0 for NaN regions default cloud", () => {
|
it("should return 0 for NaN regions default cloud", () => {
|
||||||
const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, null, false);
|
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, null, false);
|
||||||
expect(value).toBe(0);
|
expect(value).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 0 for -1 regions", () => {
|
it("should return 0 for -1 regions", () => {
|
||||||
const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, -1, false);
|
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, -1, false);
|
||||||
expect(value).toBe(0);
|
expect(value).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 0.00008 for default cloud, rupm disabled, 1RU, 1 region, multimaster disabled", () => {
|
it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster disabled", () => {
|
||||||
const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, 1, false);
|
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 1, false);
|
||||||
expect(value).toBe(0.00008);
|
expect(value).toBe(0.00008);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 0.00051 for Mooncake cloud, rupm disabled, 1RU, 1 region, multimaster disabled", () => {
|
it("should return 0.00051 for Mooncake cloud, 1RU, 1 region, multimaster disabled", () => {
|
||||||
const value = PricingUtils.computeRUUsagePriceHourly("mooncake", false, 1, 1, false);
|
const value = PricingUtils.computeRUUsagePriceHourly("mooncake", 1, 1, false);
|
||||||
expect(value).toBe(0.00051);
|
expect(value).toBe(0.00051);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 0.00016 for default cloud, rupm disabled, 1RU, 2 regions, multimaster disabled", () => {
|
it("should return 0.00016 for default cloud, 1RU, 2 regions, multimaster disabled", () => {
|
||||||
const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, 2, false);
|
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 2, false);
|
||||||
expect(value).toBe(0.00016);
|
expect(value).toBe(0.00016);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 0.00008 for default cloud, rupm disabled, 1RU, 1 region, multimaster enabled", () => {
|
it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster enabled", () => {
|
||||||
const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, 1, true);
|
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 1, true);
|
||||||
expect(value).toBe(0.00008);
|
expect(value).toBe(0.00008);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 0.00048 for default cloud, rupm disabled, 1RU, 2 region, multimaster enabled", () => {
|
it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled", () => {
|
||||||
const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, 2, true);
|
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 2, true);
|
||||||
expect(value).toBe(0.00048);
|
expect(value).toBe(0.00048);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -150,18 +150,6 @@ describe("PricingUtils Tests", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getPricePerRuPm()", () => {
|
|
||||||
it("should return 0.000027397260273972603 for default clouds", () => {
|
|
||||||
const value = PricingUtils.getPricePerRuPm("default");
|
|
||||||
expect(value).toBe(0.000027397260273972603);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return 0.00027397260273972606 for mooncake", () => {
|
|
||||||
const value = PricingUtils.getPricePerRuPm("mooncake");
|
|
||||||
expect(value).toBe(0.00027397260273972606);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getRegionMultiplier()", () => {
|
describe("getRegionMultiplier()", () => {
|
||||||
describe("without multimaster", () => {
|
describe("without multimaster", () => {
|
||||||
it("should return 0 for null", () => {
|
it("should return 0 for null", () => {
|
||||||
@@ -254,103 +242,95 @@ describe("PricingUtils Tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("getEstimatedSpendHtml()", () => {
|
describe("getEstimatedSpendHtml()", () => {
|
||||||
it("should return 'Estimated cost (USD): <b>$0.000080 hourly / $0.0019 daily / $0.058 monthly </b> (1 region, 1RU/s, $0.00008/RU)' for 1RU/s on default cloud, 1 region, with multimaster, and no rupm", () => {
|
it("should return 'Cost (USD): <b>$0.000080 hourly / $0.0019 daily / $0.058 monthly </b> (1 region, 1RU/s, $0.00008/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>' for 1RU/s on default cloud, 1 region, with multimaster", () => {
|
||||||
const value = PricingUtils.getEstimatedSpendHtml(
|
const value = PricingUtils.getEstimatedSpendHtml(
|
||||||
1 /*RU/s*/,
|
1 /*RU/s*/,
|
||||||
"default" /* cloud */,
|
"default" /* cloud */,
|
||||||
1 /* region */,
|
1 /* region */,
|
||||||
true /* multimaster */,
|
true /* multimaster */
|
||||||
false /* rupm */
|
|
||||||
);
|
);
|
||||||
expect(value).toBe(
|
expect(value).toBe(
|
||||||
"Estimated cost (USD): <b>$0.000080 hourly / $0.0019 daily / $0.058 monthly </b> (1 region, 1RU/s, $0.00008/RU)"
|
"Cost (USD): <b>$0.000080 hourly / $0.0019 daily / $0.058 monthly </b> (1 region, 1RU/s, $0.00008/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 'Estimated cost (RMB): <b>¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly </b> (1 region, 1RU/s, ¥0.00051/RU)' for 1RU/s on mooncake, 1 region, with multimaster, and no rupm", () => {
|
it("should return 'Cost (RMB): <b>¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly </b> (1 region, 1RU/s, ¥0.00051/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>' for 1RU/s on mooncake, 1 region, with multimaster", () => {
|
||||||
const value = PricingUtils.getEstimatedSpendHtml(
|
const value = PricingUtils.getEstimatedSpendHtml(
|
||||||
1 /*RU/s*/,
|
1 /*RU/s*/,
|
||||||
"mooncake" /* cloud */,
|
"mooncake" /* cloud */,
|
||||||
1 /* region */,
|
1 /* region */,
|
||||||
true /* multimaster */,
|
true /* multimaster */
|
||||||
false /* rupm */
|
|
||||||
);
|
);
|
||||||
expect(value).toBe(
|
expect(value).toBe(
|
||||||
"Estimated cost (RMB): <b>¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly </b> (1 region, 1RU/s, ¥0.00051/RU)"
|
"Cost (RMB): <b>¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly </b> (1 region, 1RU/s, ¥0.00051/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 'Estimated cost (USD): <b>$0.13 hourly / $3.07 daily / $140.16 monthly </b> (2 regions, 400RU/s, $0.00016/RU)' for 400RU/s on default cloud, 2 region, with multimaster, and no rupm", () => {
|
it("should return 'Cost (USD): <b>$0.13 hourly / $3.07 daily / $140.16 monthly </b> (2 regions, 400RU/s, $0.00016/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>' for 400RU/s on default cloud, 2 region, with multimaster", () => {
|
||||||
const value = PricingUtils.getEstimatedSpendHtml(
|
const value = PricingUtils.getEstimatedSpendHtml(
|
||||||
400 /*RU/s*/,
|
400 /*RU/s*/,
|
||||||
"default" /* cloud */,
|
"default" /* cloud */,
|
||||||
2 /* region */,
|
2 /* region */,
|
||||||
true /* multimaster */,
|
true /* multimaster */
|
||||||
false /* rupm */
|
|
||||||
);
|
);
|
||||||
expect(value).toBe(
|
expect(value).toBe(
|
||||||
"Estimated cost (USD): <b>$0.19 hourly / $4.61 daily / $140.16 monthly </b> (2 regions, 400RU/s, $0.00016/RU)"
|
"Cost (USD): <b>$0.19 hourly / $4.61 daily / $140.16 monthly </b> (2 regions, 400RU/s, $0.00016/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 'Estimated cost (USD): <b>$0.064 hourly / $1.54 daily / $46.72 monthly </b> (2 regions, 400RU/s, $0.00008/RU)' for 400RU/s on default cloud, 2 region, without multimaster, and no rupm", () => {
|
it("should return 'Cost (USD): <b>$0.064 hourly / $1.54 daily / $46.72 monthly </b> (2 regions, 400RU/s, $0.00008/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>' for 400RU/s on default cloud, 2 region, without multimaster", () => {
|
||||||
const value = PricingUtils.getEstimatedSpendHtml(
|
const value = PricingUtils.getEstimatedSpendHtml(
|
||||||
400 /*RU/s*/,
|
400 /*RU/s*/,
|
||||||
"default" /* cloud */,
|
"default" /* cloud */,
|
||||||
2 /* region */,
|
2 /* region */,
|
||||||
false /* multimaster */,
|
false /* multimaster */
|
||||||
false /* rupm */
|
|
||||||
);
|
);
|
||||||
expect(value).toBe(
|
expect(value).toBe(
|
||||||
"Estimated cost (USD): <b>$0.064 hourly / $1.54 daily / $46.72 monthly </b> (2 regions, 400RU/s, $0.00008/RU)"
|
"Cost (USD): <b>$0.064 hourly / $1.54 daily / $46.72 monthly </b> (2 regions, 400RU/s, $0.00008/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getEstimatedSpendAcknowledgeString()", () => {
|
describe("getEstimatedSpendAcknowledgeString()", () => {
|
||||||
it("should return 'I acknowledge the estimated $0.0019 daily cost for the throughput above.' for 1RU/s on default cloud, 1 region, with multimaster, and no rupm", () => {
|
it("should return 'I acknowledge the estimated $0.0019 daily cost for the throughput above.' for 1RU/s on default cloud, 1 region, with multimaster", () => {
|
||||||
const value = PricingUtils.getEstimatedSpendAcknowledgeString(
|
const value = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||||
1 /*RU/s*/,
|
1 /*RU/s*/,
|
||||||
"default" /* cloud */,
|
"default" /* cloud */,
|
||||||
1 /* region */,
|
1 /* region */,
|
||||||
true /* multimaster */,
|
true /* multimaster */,
|
||||||
false /* rupm */,
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
expect(value).toBe("I acknowledge the estimated $0.0019 daily cost for the throughput above.");
|
expect(value).toBe("I acknowledge the estimated $0.0019 daily cost for the throughput above.");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 'I acknowledge the estimated ¥0.012 daily cost for the throughput above.' for 1RU/s on mooncake, 1 region, with multimaster, and no rupm", () => {
|
it("should return 'I acknowledge the estimated ¥0.012 daily cost for the throughput above.' for 1RU/s on mooncake, 1 region, with multimaster", () => {
|
||||||
const value = PricingUtils.getEstimatedSpendAcknowledgeString(
|
const value = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||||
1 /*RU/s*/,
|
1 /*RU/s*/,
|
||||||
"mooncake" /* cloud */,
|
"mooncake" /* cloud */,
|
||||||
1 /* region */,
|
1 /* region */,
|
||||||
true /* multimaster */,
|
true /* multimaster */,
|
||||||
false /* rupm */,
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
expect(value).toBe("I acknowledge the estimated ¥0.012 daily cost for the throughput above.");
|
expect(value).toBe("I acknowledge the estimated ¥0.012 daily cost for the throughput above.");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 'I acknowledge the estimated $3.07 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, with multimaster, and no rupm", () => {
|
it("should return 'I acknowledge the estimated $3.07 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, with multimaster", () => {
|
||||||
const value = PricingUtils.getEstimatedSpendAcknowledgeString(
|
const value = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||||
400 /*RU/s*/,
|
400 /*RU/s*/,
|
||||||
"default" /* cloud */,
|
"default" /* cloud */,
|
||||||
2 /* region */,
|
2 /* region */,
|
||||||
true /* multimaster */,
|
true /* multimaster */,
|
||||||
false /* rupm */,
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
expect(value).toBe("I acknowledge the estimated $4.61 daily cost for the throughput above.");
|
expect(value).toBe("I acknowledge the estimated $4.61 daily cost for the throughput above.");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 'I acknowledge the estimated $1.54 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, without multimaster, and no rupm", () => {
|
it("should return 'I acknowledge the estimated $1.54 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, without multimaster", () => {
|
||||||
const value = PricingUtils.getEstimatedSpendAcknowledgeString(
|
const value = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||||
400 /*RU/s*/,
|
400 /*RU/s*/,
|
||||||
"default" /* cloud */,
|
"default" /* cloud */,
|
||||||
2 /* region */,
|
2 /* region */,
|
||||||
false /* multimaster */,
|
false /* multimaster */,
|
||||||
false /* rupm */,
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
expect(value).toBe("I acknowledge the estimated $1.54 daily cost for the throughput above.");
|
expect(value).toBe("I acknowledge the estimated $1.54 daily cost for the throughput above.");
|
||||||
|
|||||||
@@ -49,21 +49,16 @@ export function getMultimasterMultiplier(numberOfRegions: number, multimasterEna
|
|||||||
|
|
||||||
export function computeRUUsagePriceHourly(
|
export function computeRUUsagePriceHourly(
|
||||||
serverId: string,
|
serverId: string,
|
||||||
rupmEnabled: boolean,
|
|
||||||
requestUnits: number,
|
requestUnits: number,
|
||||||
numberOfRegions: number,
|
numberOfRegions: number,
|
||||||
multimasterEnabled: boolean
|
multimasterEnabled: boolean
|
||||||
): number {
|
): number {
|
||||||
const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled);
|
const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled);
|
||||||
const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
|
const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
|
||||||
|
|
||||||
const pricePerRu = getPricePerRu(serverId);
|
const pricePerRu = getPricePerRu(serverId);
|
||||||
const pricePerRuPm = getPricePerRuPm(serverId);
|
|
||||||
|
|
||||||
const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier;
|
const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier;
|
||||||
const rupmCharge = rupmEnabled ? requestUnits * pricePerRuPm : 0;
|
|
||||||
|
|
||||||
return Number((ruCharge + rupmCharge).toFixed(5));
|
return Number(ruCharge.toFixed(5));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPriceCurrency(serverId: string): string {
|
export function getPriceCurrency(serverId: string): string {
|
||||||
@@ -149,14 +144,6 @@ export function getPricePerRu(serverId: string): number {
|
|||||||
return Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU;
|
return Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPricePerRuPm(serverId: string): number {
|
|
||||||
if (serverId === "mooncake") {
|
|
||||||
return Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRUPM;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRUPM;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAutoPilotV3SpendHtml(maxAutoPilotThroughputSet: number, isDatabaseThroughput: boolean): string {
|
export function getAutoPilotV3SpendHtml(maxAutoPilotThroughputSet: number, isDatabaseThroughput: boolean): string {
|
||||||
if (!maxAutoPilotThroughputSet) {
|
if (!maxAutoPilotThroughputSet) {
|
||||||
return "";
|
return "";
|
||||||
@@ -214,10 +201,9 @@ export function getEstimatedSpendHtml(
|
|||||||
throughput: number,
|
throughput: number,
|
||||||
serverId: string,
|
serverId: string,
|
||||||
regions: number,
|
regions: number,
|
||||||
multimaster: boolean,
|
multimaster: boolean
|
||||||
rupmEnabled: boolean
|
|
||||||
): string {
|
): string {
|
||||||
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, rupmEnabled, throughput, regions, multimaster);
|
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster);
|
||||||
const dailyPrice: number = hourlyPrice * 24;
|
const dailyPrice: number = hourlyPrice * 24;
|
||||||
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
|
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
|
||||||
const currency: string = getPriceCurrency(serverId);
|
const currency: string = getPriceCurrency(serverId);
|
||||||
@@ -225,11 +211,13 @@ export function getEstimatedSpendHtml(
|
|||||||
const pricePerRu = getPricePerRu(serverId) * getMultimasterMultiplier(regions, multimaster);
|
const pricePerRu = getPricePerRu(serverId) * getMultimasterMultiplier(regions, multimaster);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
`Estimated cost (${currency}): <b>` +
|
`Cost (${currency}): <b>` +
|
||||||
`${currencySign}${calculateEstimateNumber(hourlyPrice)} hourly / ` +
|
`${currencySign}${calculateEstimateNumber(hourlyPrice)} hourly / ` +
|
||||||
`${currencySign}${calculateEstimateNumber(dailyPrice)} daily / ` +
|
`${currencySign}${calculateEstimateNumber(dailyPrice)} daily / ` +
|
||||||
`${currencySign}${calculateEstimateNumber(monthlyPrice)} monthly </b> ` +
|
`${currencySign}${calculateEstimateNumber(monthlyPrice)} monthly </b> ` +
|
||||||
`(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)`
|
`(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)` +
|
||||||
|
`<p style='padding: 10px 0px 0px 0px;'>` +
|
||||||
|
`<em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,12 +226,11 @@ export function getEstimatedSpendAcknowledgeString(
|
|||||||
serverId: string,
|
serverId: string,
|
||||||
regions: number,
|
regions: number,
|
||||||
multimaster: boolean,
|
multimaster: boolean,
|
||||||
rupmEnabled: boolean,
|
|
||||||
isAutoscale: boolean
|
isAutoscale: boolean
|
||||||
): string {
|
): string {
|
||||||
const hourlyPrice: number = isAutoscale
|
const hourlyPrice: number = isAutoscale
|
||||||
? computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster)
|
? computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster)
|
||||||
: computeRUUsagePriceHourly(serverId, rupmEnabled, throughput, regions, multimaster);
|
: computeRUUsagePriceHourly(serverId, throughput, regions, multimaster);
|
||||||
const dailyPrice: number = hourlyPrice * 24;
|
const dailyPrice: number = hourlyPrice * 24;
|
||||||
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
|
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
|
||||||
const currencySign: string = getCurrencySign(serverId);
|
const currencySign: string = getCurrencySign(serverId);
|
||||||
|
|||||||
@@ -33,18 +33,36 @@ export class ARMError extends Error {
|
|||||||
public code: string | number;
|
public code: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ARMQueryParams {
|
||||||
|
filter?: string;
|
||||||
|
metricNames?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
host: string;
|
host: string;
|
||||||
path: string;
|
path: string;
|
||||||
apiVersion: string;
|
apiVersion: string;
|
||||||
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
|
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
|
||||||
body?: unknown;
|
body?: unknown;
|
||||||
|
queryParams?: ARMQueryParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This is very similar to what is happening in ResourceProviderClient.ts. Should probably merge them.
|
// TODO: This is very similar to what is happening in ResourceProviderClient.ts. Should probably merge them.
|
||||||
export async function armRequest<T>({ host, path, apiVersion, method, body: requestBody }: Options): Promise<T> {
|
export async function armRequest<T>({
|
||||||
|
host,
|
||||||
|
path,
|
||||||
|
apiVersion,
|
||||||
|
method,
|
||||||
|
body: requestBody,
|
||||||
|
queryParams
|
||||||
|
}: Options): Promise<T> {
|
||||||
const url = new URL(path, host);
|
const url = new URL(path, host);
|
||||||
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
|
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
|
||||||
|
if (queryParams) {
|
||||||
|
queryParams.filter && url.searchParams.append("$filter", queryParams.filter);
|
||||||
|
queryParams.metricNames && url.searchParams.append("metricnames", queryParams.metricNames);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await window.fetch(url.href, {
|
const response = await window.fetch(url.href, {
|
||||||
method,
|
method,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -82,10 +82,6 @@ describe("Collection Add and Delete Mongo spec", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (collections.length) {
|
if (collections.length) {
|
||||||
await frame.waitFor(`div[class="collectionHeader main2 nodeItem "] > div[class="treeNodeHeader "]`, {
|
|
||||||
visible: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const textId = await frame.evaluate(element => {
|
const textId = await frame.evaluate(element => {
|
||||||
return element.attributes["data-test"].textContent;
|
return element.attributes["data-test"].textContent;
|
||||||
}, collections[0]);
|
}, collections[0]);
|
||||||
|
|||||||
101
test/notebooks/notebookTestUtils.ts
Normal file
101
test/notebooks/notebookTestUtils.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { ElementHandle, Frame } from "puppeteer";
|
||||||
|
import { TestExplorerParams } from "./testExplorer/TestExplorerParams";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
export const NOTEBOOK_OPERATION_DELAY = 5000;
|
||||||
|
export const RENDER_DELAY = 2500;
|
||||||
|
|
||||||
|
let testExplorerFrame: Frame;
|
||||||
|
export const getTestExplorerFrame = async (): Promise<Frame> => {
|
||||||
|
if (testExplorerFrame) {
|
||||||
|
return testExplorerFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notebooksTestRunnerTenantId = process.env.NOTEBOOKS_TEST_RUNNER_TENANT_ID;
|
||||||
|
const notebooksTestRunnerClientId = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_ID;
|
||||||
|
const notebooksTestRunnerClientSecret = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET;
|
||||||
|
const portalRunnerDatabaseAccount = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT;
|
||||||
|
const portalRunnerDatabaseAccountKey = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY;
|
||||||
|
const portalRunnerSubscripton = process.env.PORTAL_RUNNER_SUBSCRIPTION;
|
||||||
|
const portalRunnerResourceGroup = process.env.PORTAL_RUNNER_RESOURCE_GROUP;
|
||||||
|
|
||||||
|
const testExplorerUrl = new URL("testExplorer.html", "https://localhost:1234");
|
||||||
|
testExplorerUrl.searchParams.append(
|
||||||
|
TestExplorerParams.notebooksTestRunnerTenantId,
|
||||||
|
encodeURI(notebooksTestRunnerTenantId)
|
||||||
|
);
|
||||||
|
testExplorerUrl.searchParams.append(
|
||||||
|
TestExplorerParams.notebooksTestRunnerClientId,
|
||||||
|
encodeURI(notebooksTestRunnerClientId)
|
||||||
|
);
|
||||||
|
testExplorerUrl.searchParams.append(
|
||||||
|
TestExplorerParams.notebooksTestRunnerClientSecret,
|
||||||
|
encodeURI(notebooksTestRunnerClientSecret)
|
||||||
|
);
|
||||||
|
testExplorerUrl.searchParams.append(
|
||||||
|
TestExplorerParams.portalRunnerDatabaseAccount,
|
||||||
|
encodeURI(portalRunnerDatabaseAccount)
|
||||||
|
);
|
||||||
|
testExplorerUrl.searchParams.append(
|
||||||
|
TestExplorerParams.portalRunnerDatabaseAccountKey,
|
||||||
|
encodeURI(portalRunnerDatabaseAccountKey)
|
||||||
|
);
|
||||||
|
testExplorerUrl.searchParams.append(TestExplorerParams.portalRunnerSubscripton, encodeURI(portalRunnerSubscripton));
|
||||||
|
testExplorerUrl.searchParams.append(
|
||||||
|
TestExplorerParams.portalRunnerResourceGroup,
|
||||||
|
encodeURI(portalRunnerResourceGroup)
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.goto(testExplorerUrl.toString());
|
||||||
|
|
||||||
|
const handle = await page.waitForSelector("iframe");
|
||||||
|
testExplorerFrame = await handle.contentFrame();
|
||||||
|
await testExplorerFrame.waitForSelector(".galleryHeader");
|
||||||
|
return testExplorerFrame;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadNotebookIfNotExist = async (frame: Frame, notebookName: string): Promise<ElementHandle<Element>> => {
|
||||||
|
const notebookNode = await getNotebookNode(frame, notebookName);
|
||||||
|
if (notebookNode) {
|
||||||
|
return notebookNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadNotebookPath = path.join(__dirname, "testNotebooks", notebookName);
|
||||||
|
const notebookResourceTree = await frame.waitForSelector(".notebookResourceTree");
|
||||||
|
const treeNodeHeadersBeforeUpload = await notebookResourceTree.$$(".treeNodeHeader");
|
||||||
|
|
||||||
|
const ellipses = await treeNodeHeadersBeforeUpload[2].$("button");
|
||||||
|
await ellipses.click();
|
||||||
|
|
||||||
|
await frame.waitFor(RENDER_DELAY);
|
||||||
|
|
||||||
|
const menuItems = await frame.$$(".ms-ContextualMenu-item");
|
||||||
|
await menuItems[4].click();
|
||||||
|
|
||||||
|
const uploadFileButton = await frame.waitForSelector("#importFileButton");
|
||||||
|
uploadFileButton.click();
|
||||||
|
|
||||||
|
const fileChooser = await page.waitForFileChooser();
|
||||||
|
fileChooser.accept([uploadNotebookPath]);
|
||||||
|
|
||||||
|
const submitButton = await frame.waitForSelector("#uploadFileButton");
|
||||||
|
await submitButton.click();
|
||||||
|
|
||||||
|
await frame.waitFor(NOTEBOOK_OPERATION_DELAY);
|
||||||
|
return await getNotebookNode(frame, notebookName);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNotebookNode = async (frame: Frame, uploadNotebookName: string): Promise<ElementHandle<Element>> => {
|
||||||
|
const notebookResourceTree = await frame.waitForSelector(".notebookResourceTree");
|
||||||
|
let currentNotebookNode: ElementHandle<Element>;
|
||||||
|
|
||||||
|
const treeNodeHeaders = await notebookResourceTree.$$(".treeNodeHeader");
|
||||||
|
for (let i = 1; i < treeNodeHeaders.length; i++) {
|
||||||
|
currentNotebookNode = treeNodeHeaders[i];
|
||||||
|
const nodeLabel = await currentNotebookNode.$eval(".nodeLabel", element => element.textContent);
|
||||||
|
if (nodeLabel === uploadNotebookName) {
|
||||||
|
return currentNotebookNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
138
test/notebooks/testExplorer/TestExplorer.ts
Normal file
138
test/notebooks/testExplorer/TestExplorer.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { MessageTypes } from "../../../src/Contracts/ExplorerContracts";
|
||||||
|
import "../../../less/hostedexplorer.less";
|
||||||
|
import { TestExplorerParams } from "./TestExplorerParams";
|
||||||
|
import { ClientSecretCredential } from "@azure/identity";
|
||||||
|
import { DatabaseAccountsGetResponse } from "@azure/arm-cosmosdb/esm/models";
|
||||||
|
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
|
||||||
|
import * as msRest from "@azure/ms-rest-js";
|
||||||
|
import * as ViewModels from "../../../src/Contracts/ViewModels";
|
||||||
|
|
||||||
|
class CustomSigner implements msRest.ServiceClientCredentials {
|
||||||
|
private token: string;
|
||||||
|
constructor(token: string) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async signRequest(webResource: msRest.WebResourceLike): Promise<msRest.WebResourceLike> {
|
||||||
|
webResource.headers.set("authorization", `bearer ${this.token}`);
|
||||||
|
return webResource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMessage = (event: MessageEvent): void => {
|
||||||
|
if (event.data.type === MessageTypes.InitTestExplorer) {
|
||||||
|
sendMessageToExplorerFrame(event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const AADLogin = async (
|
||||||
|
notebooksTestRunnerApplicationId: string,
|
||||||
|
notebooksTestRunnerClientId: string,
|
||||||
|
notebooksTestRunnerClientSecret: string
|
||||||
|
): Promise<string> => {
|
||||||
|
const credentials = new ClientSecretCredential(
|
||||||
|
notebooksTestRunnerApplicationId,
|
||||||
|
notebooksTestRunnerClientId,
|
||||||
|
notebooksTestRunnerClientSecret
|
||||||
|
);
|
||||||
|
const token = await credentials.getToken("https://management.core.windows.net/.default");
|
||||||
|
return token.token;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDatabaseAccount = async (
|
||||||
|
token: string,
|
||||||
|
notebooksAccountSubscriptonId: string,
|
||||||
|
notebooksAccountResourceGroup: string,
|
||||||
|
notebooksAccountName: string
|
||||||
|
): Promise<DatabaseAccountsGetResponse> => {
|
||||||
|
const client = new CosmosDBManagementClient(new CustomSigner(token), notebooksAccountSubscriptonId);
|
||||||
|
return client.databaseAccounts.get(notebooksAccountResourceGroup, notebooksAccountName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessageToExplorerFrame = (data: unknown): void => {
|
||||||
|
const explorerFrame = document.getElementById("explorerMenu") as HTMLIFrameElement;
|
||||||
|
|
||||||
|
explorerFrame &&
|
||||||
|
explorerFrame.contentDocument &&
|
||||||
|
explorerFrame.contentDocument.referrer &&
|
||||||
|
explorerFrame.contentWindow.postMessage(
|
||||||
|
{
|
||||||
|
signature: "pcIframe",
|
||||||
|
data: data
|
||||||
|
},
|
||||||
|
explorerFrame.contentDocument.referrer || window.location.href
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const initTestExplorer = async (): Promise<void> => {
|
||||||
|
window.addEventListener("message", handleMessage, false);
|
||||||
|
|
||||||
|
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||||
|
const notebooksTestRunnerTenantId = decodeURIComponent(
|
||||||
|
urlSearchParams.get(TestExplorerParams.notebooksTestRunnerTenantId)
|
||||||
|
);
|
||||||
|
const notebooksTestRunnerClientId = decodeURIComponent(
|
||||||
|
urlSearchParams.get(TestExplorerParams.notebooksTestRunnerClientId)
|
||||||
|
);
|
||||||
|
const notebooksTestRunnerClientSecret = decodeURIComponent(
|
||||||
|
urlSearchParams.get(TestExplorerParams.notebooksTestRunnerClientSecret)
|
||||||
|
);
|
||||||
|
const portalRunnerDatabaseAccount = decodeURIComponent(
|
||||||
|
urlSearchParams.get(TestExplorerParams.portalRunnerDatabaseAccount)
|
||||||
|
);
|
||||||
|
const portalRunnerDatabaseAccountKey = decodeURIComponent(
|
||||||
|
urlSearchParams.get(TestExplorerParams.portalRunnerDatabaseAccountKey)
|
||||||
|
);
|
||||||
|
const portalRunnerSubscripton = decodeURIComponent(urlSearchParams.get(TestExplorerParams.portalRunnerSubscripton));
|
||||||
|
const portalRunnerResourceGroup = decodeURIComponent(
|
||||||
|
urlSearchParams.get(TestExplorerParams.portalRunnerResourceGroup)
|
||||||
|
);
|
||||||
|
|
||||||
|
const token = await AADLogin(
|
||||||
|
notebooksTestRunnerTenantId,
|
||||||
|
notebooksTestRunnerClientId,
|
||||||
|
notebooksTestRunnerClientSecret
|
||||||
|
);
|
||||||
|
const databaseAccount = await getDatabaseAccount(
|
||||||
|
token,
|
||||||
|
portalRunnerSubscripton,
|
||||||
|
portalRunnerResourceGroup,
|
||||||
|
portalRunnerDatabaseAccount
|
||||||
|
);
|
||||||
|
|
||||||
|
const initTestExplorerContent = {
|
||||||
|
type: MessageTypes.InitTestExplorer,
|
||||||
|
inputs: {
|
||||||
|
databaseAccount: databaseAccount,
|
||||||
|
subscriptionId: portalRunnerSubscripton,
|
||||||
|
resourceGroup: portalRunnerResourceGroup,
|
||||||
|
authorizationToken: `Bearer ${token}`,
|
||||||
|
features: {},
|
||||||
|
hasWriteAccess: true,
|
||||||
|
csmEndpoint: "https://management.azure.com",
|
||||||
|
dnsSuffix: "documents.azure.com",
|
||||||
|
serverId: "prod1",
|
||||||
|
extensionEndpoint: "/proxy",
|
||||||
|
subscriptionType: 3,
|
||||||
|
quotaId: "Internal_2014-09-01",
|
||||||
|
addCollectionDefaultFlight: "2",
|
||||||
|
isTryCosmosDBSubscription: false,
|
||||||
|
masterKey: portalRunnerDatabaseAccountKey,
|
||||||
|
loadDatabaseAccountTimestamp: 1604663109836,
|
||||||
|
dataExplorerVersion: "1.0.1",
|
||||||
|
sharedThroughputMinimum: 400,
|
||||||
|
sharedThroughputMaximum: 1000000,
|
||||||
|
sharedThroughputDefault: 400,
|
||||||
|
defaultCollectionThroughput: {
|
||||||
|
storage: "100",
|
||||||
|
throughput: { fixed: 400, unlimited: 400, unlimitedmax: 100000, unlimitedmin: 400, shared: 400 }
|
||||||
|
},
|
||||||
|
// add UI test only when feature is not dependent on flights anymore
|
||||||
|
flights: []
|
||||||
|
} as ViewModels.DataExplorerInputsFrame
|
||||||
|
};
|
||||||
|
|
||||||
|
window.postMessage(initTestExplorerContent, window.location.href);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("load", initTestExplorer);
|
||||||
9
test/notebooks/testExplorer/TestExplorerParams.ts
Normal file
9
test/notebooks/testExplorer/TestExplorerParams.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export enum TestExplorerParams {
|
||||||
|
notebooksTestRunnerTenantId = "notebooksTestRunnerTenantId",
|
||||||
|
notebooksTestRunnerClientId = "notebooksTestRunnerClientId",
|
||||||
|
notebooksTestRunnerClientSecret = "notebooksTestRunnerClientSecret",
|
||||||
|
portalRunnerDatabaseAccount = "portalRunnerDatabaseAccount",
|
||||||
|
portalRunnerDatabaseAccountKey = "portalRunnerDatabaseAccountKey",
|
||||||
|
portalRunnerSubscripton = "portalRunnerSubscripton",
|
||||||
|
portalRunnerResourceGroup = "portalRunnerResourceGroup"
|
||||||
|
}
|
||||||
18
test/notebooks/testExplorer/testExplorer.html
Normal file
18
test/notebooks/testExplorer/testExplorer.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Azure Cosmos DB</title>
|
||||||
|
<link rel="shortcut icon" href="images/CosmosDB_rgb_ui_lighttheme.ico" type="image/x-icon" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<iframe
|
||||||
|
id="explorerMenu"
|
||||||
|
name="explorer"
|
||||||
|
class="iframe"
|
||||||
|
title="explorer"
|
||||||
|
src="explorer.html?v=1.0.1&platform=Portal"
|
||||||
|
></iframe>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user