Compare commits

..

20 Commits

Author SHA1 Message Date
Steve Faulkner
0f8c36bbf0 Move serverId 2020-12-15 23:26:30 -06:00
Steve Faulkner
661fb66f7b Readme updates 2020-12-15 19:40:36 -06:00
Steve Faulkner
2eabc377b0 Update README 2020-12-15 19:31:43 -06:00
Steve Faulkner
1aa3fb8e7b Update snapshot 2020-12-15 19:16:53 -06:00
Steve Faulkner
57aef782d8 Explorer.ts Cleanup 2020-12-15 19:11:51 -06:00
Steve Faulkner
ea39c1d092 Fix offer update notification for AAD users (#338) 2020-12-11 13:38:57 -06:00
vchske
c21f42159f Updated cost messaging for new db/container pane (#333)
* Adds information text further explaining estimated cost.

* Minor tweak to cost messaging

* Updated unit tests

* Added text and link for capacity planner when choosing manual RUs
2020-12-11 10:06:43 -08:00
victor-meng
31e4b49f11 Only call getCollectionDataUsageSize for AAD users (#337) 2020-12-10 14:13:08 -08:00
Tanuj Mittal
40491ec9c5 Gallery related fixes (#312)
* AVERT fixes

* Remove enableCodeOfConduct feature flag

* Fix reporting abuse

* Add empty screen for Liked and Published tabs in Gallery

* fix build

* Remove unused code

* Fix standalone public gallery
2020-12-10 13:09:18 -08:00
Tanuj Mittal
e133df18dd Record baseUrl for OpenTerminal success/failure telemetry (#335)
This is useful to know which terminal is opening.
2020-12-10 19:54:21 +00:00
Steve Faulkner
0532ed26a2 Remove runner workflow that is no longer functioning (#332) 2020-12-01 10:23:18 -06:00
victor-meng
fd60c9c15e Remove RUPM (#328)
Remove all RUPM code
2020-12-01 07:06:38 +00:00
Chris-MS-896
04ab1f3918 '[Visual Requirement-Data Explorer (iframe)] On the Data Explorer page, luminosity contrast ratio of the borderline button is less than 3.:1.' (#331) 2020-11-30 15:32:28 -06:00
Chris-MS-896
b784ac0f86 [967093][Screen Readers- CosmosDB – Notification] Screen reader does not pass the combo-box list information (#329)
* ‘Bug fix: Screen reader does not pass the combo-box list information under notification field.’

* ‘update for comments’

* ‘load path refator’
2020-11-30 14:33:18 -06:00
Srinath Narayanan
28899f63d7 Fixed bug in fetching 'index transformation progress' header (#330)
* bug fix

* fixed formatting errors
2020-11-24 10:32:18 -08:00
victor-meng
9cbf632577 Get collection usage size with ARM metrics call (#327)
- Removed `readCollectionQuotaInfo` call. The only data we need is the usage size which we can get via the ARM metrics call instead.
- Added `getCollectionUsageSize` which fetches the `DataUsage` and `IndexUsage` metrics, converts them to KB, and returns the sum as the total usage size
2020-11-20 20:21:16 +00:00
victor-meng
17fd2185dc Move read offer to RP (#326) 2020-11-19 17:13:11 -08:00
Srinath Narayanan
a93c8509cd Added testExplorer and notebooks UI automated tests (#323)
* initial commit for notbooks pupeteer tests

* Added Auth

* added try catch block with screenshot for error

* Addressed PR comments

* renamed params

* renamed param

* fixed formatting error

* Updates mongo spec to remove waitFor on already awaited selector

* added logging statements

* format errors fixed

* added ci env variables

* increased delay for render

* removed logging

* added delay

* fix format error

* removed deletion

* reverted package.json change

Co-authored-by: zfoster <notzachfoster@gmail.com>
2020-11-19 09:29:38 -08:00
Laurent Nguyen
5c93c11bd9 Bug fix: match monaco-editor version with @nteract/monaco-editor (#322)
Opening notebook (which contains code cell), then "Items"-> document editor is broken. Our JsonEditor component will hang at `monaco.editor.create()`.

Matching the `monaco-editor` version with `@nteract/monaco-editor` fixes it.

I looked into using one, but we cannot rely nteract, because it does not get loaded if notebook isn't enabled. Forcing nteract to use ours (if at all possible) isn't a good idea, since their code is tuned to their version.

For now, we'll have to keep the versions in sync.
2020-11-19 14:33:23 +00:00
Srinath Narayanan
85d2378d3a Removed SettingsV1 code paths (#325)
* removed settingsv1 code path in collection.ts

* removed Settingsv1 code

* Moved AAD error message up the chain
2020-11-18 12:11:25 -08:00
105 changed files with 3620 additions and 6274 deletions

View File

@@ -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=

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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'))"
] ]
} }

View File

@@ -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";

View File

@@ -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);

View File

@@ -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;
} }

View 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);
});
});

View 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"
};
};

View File

@@ -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 [];
} }

View 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;
};

View File

@@ -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;
}; };

View File

@@ -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();
}
};

View File

@@ -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;
}; };

View 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);
};

View File

@@ -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 {

View File

@@ -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);
}; };

View File

@@ -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();
});
});

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;

View File

@@ -32,7 +32,8 @@ export enum MessageTypes {
GetArcadiaToken, GetArcadiaToken,
CreateWorkspace, CreateWorkspace,
CreateSparkPool, CreateSparkPool,
RefreshDatabaseAccount RefreshDatabaseAccount,
InitTestExplorer
} }
export { Versions, ActionContracts, Diagnostics }; export { Versions, ActionContracts, Diagnostics };

View File

@@ -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>;

View File

@@ -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);
}); });

View File

@@ -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());

View File

@@ -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",

View File

@@ -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"

View File

@@ -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);
}); });
}; };

View File

@@ -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 });

View File

@@ -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()) {

View File

@@ -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>)}

View File

@@ -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>
); );

View File

@@ -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} />;
} }
} }
} }

View File

@@ -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");
}); });
}); });

View File

@@ -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()}
/> />
); );

View File

@@ -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(

View File

@@ -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

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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
), ),

View File

@@ -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],

View File

@@ -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"

View File

@@ -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="

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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[] {

View File

@@ -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"
/> />

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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>
);
}
}

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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;

View File

@@ -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()
}, },

View File

@@ -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(),

View File

@@ -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;

View File

@@ -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 -->

View File

@@ -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;

View File

@@ -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[] {

View File

@@ -1 +0,0 @@
<div data-bind="react:mongoQueryComponentAdapter" style="height: 100%"></div>

View File

@@ -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();
}
}

View File

@@ -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/";
} }

View File

@@ -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
});
}
}

View File

@@ -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
});
}
} }

View File

@@ -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>

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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 {

View File

@@ -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", () => {

View File

@@ -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
); );

View File

@@ -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);
}); });

View File

@@ -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;

View File

@@ -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,

View File

@@ -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[]> {

View File

@@ -19,6 +19,7 @@ export function initializeExplorer(): Explorer {
cassandraEndpoint: "" cassandraEndpoint: ""
} }
}); });
explorer.isAccountReady(true); explorer.isAccountReady(true);
return explorer; return explorer;
} }

View File

@@ -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 {

View File

@@ -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;
} }

View File

@@ -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) {

View File

@@ -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";
} }

View File

@@ -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 {

View File

@@ -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[]> {

View File

@@ -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);
} }
}; };

View File

@@ -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;

View File

@@ -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;

View File

@@ -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}`);
} }

View File

@@ -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();
});
});
});

View File

@@ -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);
}
}

View File

@@ -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.");

View File

@@ -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);

View File

@@ -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: {

View File

@@ -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]);

View 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;
};

View 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);

View File

@@ -0,0 +1,9 @@
export enum TestExplorerParams {
notebooksTestRunnerTenantId = "notebooksTestRunnerTenantId",
notebooksTestRunnerClientId = "notebooksTestRunnerClientId",
notebooksTestRunnerClientSecret = "notebooksTestRunnerClientSecret",
portalRunnerDatabaseAccount = "portalRunnerDatabaseAccount",
portalRunnerDatabaseAccountKey = "portalRunnerDatabaseAccountKey",
portalRunnerSubscripton = "portalRunnerSubscripton",
portalRunnerResourceGroup = "portalRunnerResourceGroup"
}

View 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