Compare commits

...

17 Commits

Author SHA1 Message Date
Srinath Narayanan
9e8793d1b1 Moved constants 2020-11-11 11:31:07 -08:00
Srinath Narayanan
f41de718a0 fixed formatting error. 2020-11-11 08:58:43 -08:00
Srinath Narayanan
5820afc25a changed error message 2020-11-11 08:52:07 -08:00
Srinath Narayanan
6d94f18f9a Added loadOfffer with retry 2020-11-11 08:46:28 -08:00
victor-meng
a133134b8b Fix create and delete database (#317) 2020-11-09 15:22:51 -08:00
victor-meng
79dec6a8a8 Refactor error handling in data explorer Part 3 (#315)
- Make sure we pass the error message string instead of an error object when we call `TelemetryProcessor.traceFailure` since TelemetryProcessor will call `JSON.stringify` on the error object which would result in an empty object
- Removed ErrorParserUtility since it only works on specific error types. We can just log the full error message and manually derive information we need from the message.
- Added option to include stack trace in `getErrorMessage`. This is useful for figuring out where the client side script errors are coming from.
- Some minor refactors
2020-11-06 04:02:57 +00:00
Tanuj Mittal
53a8cea95e Hide Settings for Cassandra Serverless accounts (#311)
In case of Serverless Cassandra accounts we don't have any Settings to tweak in the Settings Tab. So this change hides the option to open Settings tab in those cases.
2020-11-05 00:11:36 +00:00
victor-meng
5f1f7a8266 Refactor error handling part 2 (#313) 2020-11-03 13:40:44 -08:00
Zachary Foster
a009a8ba5f Adds e2e readme and new endpoint env var (#314) 2020-11-03 14:05:54 -05:00
victor-meng
3e782527d0 Poll on Location header for operation status (#309) 2020-11-02 16:59:08 -08:00
Srinath Narayanan
e6ca1d25c9 Added index refresh to SQL API indexing policy editor (#306)
* Index refresh component introduced

- Made all notifications in Mongo Index editor have 12 font size
- Added indexing policy refresh to sql indexing policy editor
- Added "you have unsaved changes" message, replace old message for lazy indexing policy changes

* formatting changes

* addressed PR comments
2020-11-02 13:19:45 -08:00
Zachary Foster
473f722dcc E2E Test Rewrite (#300)
* Adds tables test

* Include .env var

* Adds asElement on again

* Add further loading states

* Format

* Hope to not lose focus

* Adds ID to shared key and modifies value of input directly

* Fix tables test

* Format

* Try uploading screenshots

* indent

* Fixes connection string

* Try wildcard upload path

* Rebuilds test structure, assertions, dependencies

* Wait longer for container create

* format
2020-11-02 14:33:14 -05:00
victor-meng
5741802c25 refactor error handling part 1 (#307)
- created `getErrorMessage` function which takes in an error string or any type of error object and returns the correct error message
- replaced `error.message` with `getErrorMessage` since `error` could be a string in some cases
- merged sendNotificationForError.ts with ErrorHandlingUtils.ts
- some minor refactoring

In part 2, I will make the following changes:
 - Make `Logger.logError` function take an error message string instead of an error object. This will reduce some redundancy where the `getErrorMessage` function is being called twice (the error object passed by the caller is already an error message).
 - Update every `TelemetryProcessor.traceFailure` call to make sure we pass in an error message instead of an error object since we stringify the data we send.
2020-10-30 22:09:24 +00:00
Laurent Nguyen
e2e58f73b1 Update @nteract/monaco-editor package to get c# syntax coloring bug fix (#305)
Integrate `monaco-editor` from this nteract [release](39c604fcb0/changelogs/10-2020.md).
2020-10-29 21:34:04 +00:00
Steve Faulkner
79769e9689 Remove AutoPilot v2 (#304)
* Remove AutoPilot v2

* Update DatabaseSettingsTab.ts

* Update DatabaseSettingsTab.ts

* Update src/Explorer/Tabs/DatabaseSettingsTab.ts

Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>

* Update src/Explorer/Tabs/SettingsTab.ts

* Update src/Explorer/Tabs/DatabaseSettingsTab.ts

Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>

* Update src/Explorer/Tabs/SettingsTab.ts

* Remove more unused code

* Remove import

Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>
2020-10-29 11:26:37 -05:00
Srinath Narayanan
542abf4d3a Added extra logging for mongo index update RP call (#303)
* Added extra logging

* changes error -> error.message in traceFailure
2020-10-27 13:39:38 -07:00
Steve Faulkner
4bee46ce55 Add Tab Name to Telemetry (#302) 2020-10-27 10:36:31 -05:00
147 changed files with 1480 additions and 2862 deletions

View File

@@ -1,7 +1,10 @@
# These options are only needed when if running end to end tests locally
PORTAL_RUNNER_USERNAME= PORTAL_RUNNER_USERNAME=
PORTAL_RUNNER_PASSWORD= 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_CONNECTION_STRING= PORTAL_RUNNER_CONNECTION_STRING=
CASSANDRA_CONNECTION_STRING=
MONGO_CONNECTION_STRING=
TABLES_CONNECTION_STRING=
DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html

View File

@@ -15,8 +15,6 @@ 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/EnvironmentUtility.ts
src/Common/ErrorParserUtility.test.ts
src/Common/ErrorParserUtility.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

View File

@@ -150,6 +150,12 @@ jobs:
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 }}
TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }}
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html"
- uses: actions/upload-artifact@v2
with:
name: screenshots
path: failed-*
nuget: nuget:
name: Publish Nuget name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')

View File

@@ -33,7 +33,7 @@ To run pure hosted mode, in `webpack.config.js` change index HtmlWebpackPlugin t
### 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 enironment you can specify an alternate endpoint using `EMULATOR_ENDPOINT` and webpack dev server will proxy requests for you. 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.
`PLATFORM=Emulator EMULATOR_ENDPOINT=https://my-vm.azure.com:8081 npm run watch` `PLATFORM=Emulator EMULATOR_ENDPOINT=https://my-vm.azure.com:8081 npm run watch`
@@ -60,7 +60,7 @@ The Cosmos Portal that consumes this repo is not currently open source. If you h
You can however load a local running instance of data explorer in the production portal. 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) 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. Whitelist `https://localhost:1234` domain for CORS in the Azure Cosmos DB portal 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` 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 4. Load the portal using the following link: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html
@@ -84,16 +84,19 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
4. Install dependencies: `npm install` 4. Install dependencies: `npm install`
5. Run cypress headless(`npm run test`) or in interactive mode(`npm run test:debug`) 5. Run cypress headless(`npm run test`) or in interactive mode(`npm run test:debug`)
#### End to End Production Runners #### End to End Production Tests
Jest and Puppeteer are used for end to end production runners and are contained in `test/`. To run these tests locally: Jest and Puppeteer are used for end to end production runners and are contained in `test/`. To run these tests locally:
1. Copy .env.example to .env and fill in all variables 1. Copy .env.example to .env
2. Run `npm run test:e2e` 2. Update the values in .env including your local data explorer endpoint (ask a teammate/codeowner for help with .env values)
3. Make sure all packages are installed `npm install`
4. Run the server `npm run start` and wait for it to start
5. Run `npm run test:e2e`
### Releasing ### Releasing
We generally adhear to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details. We generally adhere to the release strategy [documented by the Azure SDK Guidelines](https://azure.github.io/azure-sdk/policies_repobranching.html#release-branches). Most releases should happen from the master branch. If master contains commits that cannot be released, you may create a release from a `release/` or `hotfix/` branch. See linked documentation for more details.
# Contributing # Contributing

48
package-lock.json generated
View File

@@ -2803,12 +2803,13 @@
} }
}, },
"@nteract/monaco-editor": { "@nteract/monaco-editor": {
"version": "3.2.0", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/@nteract/monaco-editor/-/monaco-editor-3.2.0.tgz", "resolved": "https://registry.npmjs.org/@nteract/monaco-editor/-/monaco-editor-3.2.2.tgz",
"integrity": "sha512-PGEUvy/GTBMECy4RUfh4wxO7GfA9YDBSV3hGt8MyrVz/GxUDtjB7FqrYS0ZhmVQPYl8hnV2i48F3YlypC+xIXA==", "integrity": "sha512-51Pxt6v6qaAlbDY0BgEydk/Jxuu93t+uB8Geg3vJfE6VDphTEakB0wocBIfvcTKVV55Lx53/rTSp6QHqtaHiGg==",
"requires": { "requires": {
"@nteract/core": "^14.0.0", "@nteract/core": "^14.0.0",
"@nteract/messaging": "^7.0.10", "@nteract/messaging": "^7.0.12",
"lodash.debounce": "^4.0.6",
"monaco-editor": "0.18.1", "monaco-editor": "0.18.1",
"rxjs": "^6.3.3" "rxjs": "^6.3.3"
}, },
@@ -2857,6 +2858,40 @@
"rxjs": "^6.3.3" "rxjs": "^6.3.3"
} }
}, },
"@nteract/messaging": {
"version": "7.0.12",
"resolved": "https://registry.npmjs.org/@nteract/messaging/-/messaging-7.0.12.tgz",
"integrity": "sha512-5z2Ffd1hj7AsGBJTAoqJshLlUZ+ISJBjiZAdNDjb70PNEv0x8UOMk/di80RI3WBLK5MKxSJkGXfs4jfzfdW6bA==",
"requires": {
"@nteract/types": "^7.1.2",
"@types/uuid": "^8.0.0",
"lodash.clonedeep": "^4.5.0",
"rxjs": "^6.6.0",
"uuid": "^8.0.0"
},
"dependencies": {
"@nteract/commutable": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/@nteract/commutable/-/commutable-7.3.4.tgz",
"integrity": "sha512-Z6aUtIZN0CKUMJwbZjUUqaaBhT6P0RiEG5nHso+oG/FOXF20Qv+hf/TyvYhw9SXQVmmacaMk4zj0iOID20pIng==",
"requires": {
"immutable": "^4.0.0-rc.12",
"uuid": "^8.0.0"
}
},
"@nteract/types": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@nteract/types/-/types-7.1.2.tgz",
"integrity": "sha512-I/1TvaUC/m9I/LFk1HemsOUqB0eNdagu0KRLA1YEtChPh9pk5F9flglA7m5+0/j31gLXBISj5+6tL8ikA8BxOQ==",
"requires": {
"@nteract/commutable": "^7.3.4",
"immutable": "^4.0.0-rc.12",
"rxjs": "^6.6.0",
"uuid": "^8.0.0"
}
}
}
},
"@nteract/mythic-notifications": { "@nteract/mythic-notifications": {
"version": "0.1.9", "version": "0.1.9",
"resolved": "https://registry.npmjs.org/@nteract/mythic-notifications/-/mythic-notifications-0.1.9.tgz", "resolved": "https://registry.npmjs.org/@nteract/mythic-notifications/-/mythic-notifications-0.1.9.tgz",
@@ -14422,6 +14457,11 @@
"resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz",
"integrity": "sha1-JI42By7ekGUB11lmIAqG2riyMXA=" "integrity": "sha1-JI42By7ekGUB11lmIAqG2riyMXA="
}, },
"lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
},
"lodash.defaults": { "lodash.defaults": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",

View File

@@ -21,7 +21,7 @@
"@nteract/jupyter-widgets": "2.0.0", "@nteract/jupyter-widgets": "2.0.0",
"@nteract/logos": "1.0.0", "@nteract/logos": "1.0.0",
"@nteract/markdown": "4.4.0", "@nteract/markdown": "4.4.0",
"@nteract/monaco-editor": "3.2.0", "@nteract/monaco-editor": "3.2.2",
"@nteract/octicons": "2.0.0", "@nteract/octicons": "2.0.0",
"@nteract/outputs": "3.0.9", "@nteract/outputs": "3.0.9",
"@nteract/presentational-components": "3.0.7", "@nteract/presentational-components": "3.0.7",

View File

@@ -1,4 +1,3 @@
import { AutopilotTier } from "../Contracts/DataModels";
import { HashMap } from "./HashMap"; import { HashMap } from "./HashMap";
export class AuthorizationEndpoints { export class AuthorizationEndpoints {
@@ -124,7 +123,6 @@ export class Features {
public static readonly notebookBasePath = "notebookbasepath"; public static readonly notebookBasePath = "notebookbasepath";
public static readonly canExceedMaximumValue = "canexceedmaximumvalue"; public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput"; public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
public static readonly enableAutoPilotV2 = "enableautopilotv2";
public static readonly ttl90Days = "ttl90days"; public static readonly ttl90Days = "ttl90days";
public static readonly enableRightPanelV2 = "enablerightpanelv2"; public static readonly enableRightPanelV2 = "enablerightpanelv2";
public static readonly enableSDKoperations = "enablesdkoperations"; public static readonly enableSDKoperations = "enablesdkoperations";
@@ -262,7 +260,6 @@ export class HttpHeaders {
public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere"; public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere";
public static autoPilotThroughput = "autoscaleSettings"; public static autoPilotThroughput = "autoscaleSettings";
public static autoPilotThroughputSDK = "x-ms-cosmos-offer-autopilot-settings"; public static autoPilotThroughputSDK = "x-ms-cosmos-offer-autopilot-settings";
public static autoPilotTier = "x-ms-cosmos-offer-autopilot-tier";
public static partitionKey: string = "x-ms-documentdb-partitionkey"; public static partitionKey: string = "x-ms-documentdb-partitionkey";
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput"; public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot"; public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
@@ -407,54 +404,6 @@ export enum ConflictOperationType {
Delete = "delete" Delete = "delete"
} }
export class AutoPilot {
public static tier1Text: string = "4,000 RU/s";
public static tier2Text: string = "20,000 RU/s";
public static tier3Text: string = "100,000 RU/s";
public static tier4Text: string = "500,000 RU/s";
public static tierText = {
[AutopilotTier.Tier1]: "Tier 1",
[AutopilotTier.Tier2]: "Tier 2",
[AutopilotTier.Tier3]: "Tier 3",
[AutopilotTier.Tier4]: "Tier 4"
};
public static tierMaxRus = {
[AutopilotTier.Tier1]: 2000,
[AutopilotTier.Tier2]: 20000,
[AutopilotTier.Tier3]: 100000,
[AutopilotTier.Tier4]: 500000
};
public static tierMinRus = {
[AutopilotTier.Tier1]: 0,
[AutopilotTier.Tier2]: 0,
[AutopilotTier.Tier3]: 0,
[AutopilotTier.Tier4]: 0
};
public static tierStorageInGB = {
[AutopilotTier.Tier1]: 50,
[AutopilotTier.Tier2]: 200,
[AutopilotTier.Tier3]: 1000,
[AutopilotTier.Tier4]: 5000
};
}
export class DataExplorerVersions {
public static readonly v_1_0_0: string = "1.0.0";
public static readonly v_1_0_1: string = "1.0.1";
}
export class DataExplorerFeatures {
public static offerCache: string = "OfferCache";
}
export const DataExplorerFeaturesVersions: any = {
OfferCache: DataExplorerVersions.v_1_0_1
};
export const EmulatorMasterKey = export const EmulatorMasterKey =
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")] //[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";

View File

@@ -1,6 +1,7 @@
import * as Cosmos from "@azure/cosmos"; import * as Cosmos from "@azure/cosmos";
import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos"; import { RequestInfo, setAuthorizationTokenHeaderUsingMasterKey } from "@azure/cosmos";
import { configContext, Platform } from "../ConfigContext"; import { configContext, Platform } from "../ConfigContext";
import { getErrorMessage } from "./ErrorHandlingUtils";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants"; import { EmulatorMasterKey, HttpHeaders } from "./Constants";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
@@ -69,7 +70,7 @@ export async function getTokenFromAuthService(verb: string, resourceType: string
const result = JSON.parse(await response.json()); const result = JSON.parse(await response.json());
return result; return result;
} catch (error) { } catch (error) {
logConsoleError(`Failed to get authorization headers for ${resourceType}: ${error.message}`); logConsoleError(`Failed to get authorization headers for ${resourceType}: ${getErrorMessage(error)}`);
return Promise.reject(error); return Promise.reject(error);
} }
} }

View File

@@ -58,8 +58,8 @@ export function executeStoredProcedure(
(error: any) => { (error: any) => {
handleError( handleError(
error, error,
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`, "ExecuteStoredProcedure",
"ExecuteStoredProcedure" `Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
); );
deferred.reject(error); deferred.reject(error);
} }
@@ -88,7 +88,7 @@ export function queryDocumentsPage(
deferred.resolve(result); deferred.resolve(result);
}, },
(error: any) => { (error: any) => {
handleError(error, `Failed to query ${entityName} for container ${resourceName}`, "QueryDocumentsPage"); handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -109,7 +109,7 @@ export function readDocument(collection: ViewModels.CollectionBase, documentId:
deferred.resolve(document); deferred.resolve(document);
}, },
(error: any) => { (error: any) => {
handleError(error, `Failed to read ${entityName} ${documentId.id()}`, "ReadDocument"); handleError(error, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -135,7 +135,7 @@ export function updateDocument(
deferred.resolve(updatedDocument); deferred.resolve(updatedDocument);
}, },
(error: any) => { (error: any) => {
handleError(error, `Failed to update ${entityName} ${documentId.id()}`, "UpdateDocument"); handleError(error, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -157,7 +157,7 @@ export function createDocument(collection: ViewModels.CollectionBase, newDocumen
deferred.resolve(savedDocument); deferred.resolve(savedDocument);
}, },
(error: any) => { (error: any) => {
handleError(error, `Error while creating new ${entityName} for container ${collection.id()}`, "CreateDocument"); handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -179,7 +179,7 @@ export function deleteDocument(collection: ViewModels.CollectionBase, documentId
deferred.resolve(response); deferred.resolve(response);
}, },
(error: any) => { (error: any) => {
handleError(error, `Error while deleting ${entityName} ${documentId.id()}`, "DeleteDocument"); handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -205,7 +205,7 @@ export function deleteConflict(
deferred.resolve(response); deferred.resolve(response);
}, },
(error: any) => { (error: any) => {
handleError(error, `Error while deleting conflict ${conflictId.id()}`, "DeleteConflict"); handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`);
deferred.reject(error); deferred.reject(error);
} }
) )

View File

@@ -1,11 +1,56 @@
import { CosmosError, sendNotificationForError } from "./dataAccess/sendNotificationForError"; import { ARMError } from "../Utils/arm/request";
import { HttpStatusCodes } from "./Constants";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/ViewModels";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { logError } from "./Logger"; import { logError } from "./Logger";
import { replaceKnownError } from "./ErrorParserUtility"; import { sendMessage } from "./MessageHandler";
export const handleError = (error: CosmosError, consoleErrorPrefix: string, area: string): void => { export const handleError = (error: string | ARMError | Error, area: string, consoleErrorPrefix?: string): void => {
const sanitizedErrorMsg = replaceKnownError(error.message); const errorMessage = getErrorMessage(error);
logConsoleError(`${consoleErrorPrefix}:\n ${sanitizedErrorMsg}`); const errorCode = error instanceof ARMError ? error.code : undefined;
logError(sanitizedErrorMsg, area, error.code);
sendNotificationForError(error); // logs error to data explorer console
const consoleErrorMessage = consoleErrorPrefix ? `${consoleErrorPrefix}:\n ${errorMessage}` : errorMessage;
logConsoleError(consoleErrorMessage);
// logs error to both app insight and kusto
logError(errorMessage, area, errorCode);
// checks for errors caused by firewall and sends them to portal to handle
sendNotificationForError(errorMessage, errorCode);
};
export const getErrorMessage = (error: string | Error): string => {
const errorMessage = typeof error === "string" ? error : error.message;
return replaceKnownError(errorMessage);
};
export const getErrorStack = (error: string | Error): string => {
return typeof error === "string" ? undefined : error.stack;
};
const sendNotificationForError = (errorMessage: string, errorCode: number | string): void => {
if (errorCode === HttpStatusCodes.Forbidden) {
if (errorMessage?.toLowerCase().indexOf("sharedoffer is disabled for your account") > 0) {
return;
}
sendMessage({
type: MessageTypes.ForbiddenError,
reason: errorMessage
});
}
};
const replaceKnownError = (errorMessage: string): string => {
if (
window.dataExplorer?.subscriptionType() === SubscriptionType.Internal &&
errorMessage.indexOf("SharedOffer is Disabled for your account") >= 0
) {
return "Database throughput is not supported for internal subscriptions.";
} else if (errorMessage.indexOf("Partition key paths must contain only valid") >= 0) {
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
}
return errorMessage;
}; };

View File

@@ -1,24 +0,0 @@
import * as ErrorParserUtility from "./ErrorParserUtility";
describe("Error Parser Utility", () => {
describe("shouldEnableCrossPartitionKeyForResourceWithPartitionKey()", () => {
it("should parse a backend error correctly", () => {
// A fake error matching what is thrown by the SDK on a bad collection create request
const innerMessage =
"The partition key component definition path '/asdwqr31 @#$#$WRadf' could not be accepted, failed near position '10'. Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
const message = `Message: {\"Errors\":[\"${innerMessage}\"]}\r\nActivityId: 97b2e684-7505-4921-85f6-2513b9b28220, Request URI: /apps/89fdcf25-2a0b-4d2a-aab6-e161e565b26f/services/54911149-7bb1-4e7d-a1fa-22c8b36a4bb9/partitions/cc2a7a04-5f5a-4709-bcf7-8509b264963f/replicas/132304018743619218p, RequestStats: , SDK: Microsoft.Azure.Documents.Common/2.10.0`;
const err = new Error(message) as any;
err.code = 400;
err.body = {
code: "BadRequest",
message
};
err.headers = {};
err.activityId = "97b2e684-7505-4921-85f6-2513b9b28220";
const parsedError = ErrorParserUtility.parse(err);
expect(parsedError.length).toBe(1);
expect(parsedError[0].message).toBe(innerMessage);
});
});
});

View File

@@ -1,69 +0,0 @@
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
export function replaceKnownError(err: string): string {
if (
window.dataExplorer.subscriptionType() === ViewModels.SubscriptionType.Internal &&
err.indexOf("SharedOffer is Disabled for your account") >= 0
) {
return "Database throughput is not supported for internal subscriptions.";
} else if (err.indexOf("Partition key paths must contain only valid") >= 0) {
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
}
return err;
}
export function parse(err: any): DataModels.ErrorDataModel[] {
try {
return _parse(err);
} catch (e) {
return [<DataModels.ErrorDataModel>{ message: JSON.stringify(err) }];
}
}
function _parse(err: any): DataModels.ErrorDataModel[] {
var normalizedErrors: DataModels.ErrorDataModel[] = [];
if (err.message && !err.code) {
normalizedErrors.push(err);
} else {
const innerErrors: any[] = _getInnerErrors(err.message);
normalizedErrors = innerErrors.map(innerError =>
typeof innerError === "string" ? { message: innerError } : innerError
);
}
return normalizedErrors;
}
function _getInnerErrors(message: string): any[] {
/*
The backend error message has an inner-message which is a stringified object.
For SQL errors, the "errors" property is an array of SqlErrorDataModel.
Example:
"Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p"
For non-SQL errors the "Errors" propery is an array of string.
Example:
"Message: {"errors":[{"severity":"Error","location":{"start":7,"end":8},"code":"SC1001","message":"Syntax error, incorrect syntax near '.'."}]}\r\nActivityId: d3300016d4084e310a, Request URI: /apps/12401f9e1df77/services/dc100232b1f44545/partitions/f86f3bc0001a2f78/replicas/13085003638s"
*/
let innerMessage: any = null;
const singleLineMessage = message.replace(/[\r\n]|\r|\n/g, "");
try {
// Multi-Partition error flavor
const regExp = /^(.*)ActivityId: (.*)/g;
const regString = regExp.exec(singleLineMessage);
const innerMessageString = regString[1];
innerMessage = JSON.parse(innerMessageString);
} catch (e) {
// Single-partition error flavor
const regExp = /^Message: (.*)ActivityId: (.*), Request URI: (.*)/g;
const regString = regExp.exec(singleLineMessage);
const innerMessageString = regString[1];
innerMessage = JSON.parse(innerMessageString);
}
return innerMessage.errors ? innerMessage.errors : innerMessage.Errors;
}

View File

@@ -21,14 +21,8 @@ export function logWarning(message: string, area: string, code?: number): void {
return _logEntry(entry); return _logEntry(entry);
} }
export function logError(message: string | Error, area: string, code?: number): void { export function logError(errorMessage: string, area: string, code?: number | string): void {
let logMessage: string; const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Error, errorMessage, area, code);
if (typeof message === "string") {
logMessage = message;
} else {
logMessage = JSON.stringify(message, Object.getOwnPropertyNames(message));
}
const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Error, logMessage, area, code);
return _logEntry(entry); return _logEntry(entry);
} }
@@ -59,7 +53,7 @@ function _generateLogEntry(
level: Diagnostics.LogEntryLevel, level: Diagnostics.LogEntryLevel,
message: string, message: string,
area: string, area: string,
code?: number code?: number | string
): Diagnostics.LogEntry { ): Diagnostics.LogEntry {
return { return {
timestamp: new Date().getUTCSeconds(), timestamp: new Date().getUTCSeconds(),

View File

@@ -12,8 +12,7 @@ import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase"; import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase";
import { createCollection } from "./dataAccess/createCollection"; import { createCollection } from "./dataAccess/createCollection";
import * as ErrorParserUtility from "./ErrorParserUtility"; import { handleError } from "./ErrorHandlingUtils";
import * as Logger from "./Logger";
export class QueriesClient { export class QueriesClient {
private static readonly PartitionKey: DataModels.PartitionKey = { private static readonly PartitionKey: DataModels.PartitionKey = {
@@ -53,13 +52,8 @@ export class QueriesClient {
return Promise.resolve(collection); return Promise.resolve(collection);
}, },
(error: any) => { (error: any) => {
const stringifiedError: string = error.message; handleError(error, "setupQueriesCollection", "Failed to set up account for saving queries");
NotificationConsoleUtils.logConsoleMessage( return Promise.reject(error);
ConsoleDataType.Error,
`Failed to set up account for saving queries: ${stringifiedError}`
);
Logger.logError(stringifiedError, "setupQueriesCollection");
return Promise.reject(stringifiedError);
} }
) )
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
@@ -102,19 +96,11 @@ export class QueriesClient {
return Promise.resolve(); return Promise.resolve();
}, },
(error: any) => { (error: any) => {
let errorMessage: string; if (error.code === HttpStatusCodes.Conflict.toString()) {
const parsedError: DataModels.ErrorDataModel = ErrorParserUtility.parse(error)[0]; error = `Query ${query.queryName} already exists`;
if (parsedError.code === HttpStatusCodes.Conflict.toString()) {
errorMessage = `Query ${query.queryName} already exists`;
} else {
errorMessage = parsedError.message;
} }
NotificationConsoleUtils.logConsoleMessage( handleError(error, "saveQuery", `Failed to save query ${query.queryName}`);
ConsoleDataType.Error, return Promise.reject(error);
`Failed to save query ${query.queryName}: ${errorMessage}`
);
Logger.logError(JSON.stringify(parsedError), "saveQuery");
return Promise.reject(errorMessage);
} }
) )
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
@@ -163,25 +149,15 @@ export class QueriesClient {
return Promise.resolve(queries); return Promise.resolve(queries);
}, },
(error: any) => { (error: any) => {
const stringifiedError: string = error.message; handleError(error, "getSavedQueries", "Failed to fetch saved queries");
NotificationConsoleUtils.logConsoleMessage( return Promise.reject(error);
ConsoleDataType.Error,
`Failed to fetch saved queries: ${stringifiedError}`
);
Logger.logError(stringifiedError, "getSavedQueries");
return Promise.reject(stringifiedError);
} }
); );
}, },
(error: any) => { (error: any) => {
// should never get into this state but we handle this regardless // should never get into this state but we handle this regardless
const stringifiedError: string = error.message; handleError(error, "getSavedQueries", "Failed to fetch saved queries");
NotificationConsoleUtils.logConsoleMessage( return Promise.reject(error);
ConsoleDataType.Error,
`Failed to fetch saved queries: ${stringifiedError}`
);
Logger.logError(stringifiedError, "getSavedQueries");
return Promise.reject(stringifiedError);
} }
) )
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
@@ -232,13 +208,8 @@ export class QueriesClient {
return Promise.resolve(); return Promise.resolve();
}, },
(error: any) => { (error: any) => {
const stringifiedError: string = error.message; handleError(error, "deleteQuery", `Failed to delete query ${query.queryName}`);
NotificationConsoleUtils.logConsoleMessage( return Promise.reject(error);
ConsoleDataType.Error,
`Failed to delete query ${query.queryName}: ${stringifiedError}`
);
Logger.logError(stringifiedError, "deleteQuery");
return Promise.reject(stringifiedError);
} }
) )
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id)); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));

View File

@@ -55,7 +55,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
logConsoleInfo(`Successfully created container ${params.collectionId}`); logConsoleInfo(`Successfully created container ${params.collectionId}`);
return collection; return collection;
} catch (error) { } catch (error) {
handleError(error, `Error while creating container ${params.collectionId}`, "CreateCollection"); handleError(error, "CreateCollection", `Error while creating container ${params.collectionId}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -41,7 +41,7 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
logConsoleInfo(`Successfully created database ${params.databaseId}`); logConsoleInfo(`Successfully created database ${params.databaseId}`);
return database; return database;
} catch (error) { } catch (error) {
handleError(error, `Error while creating database ${params.databaseId}`, "CreateDatabase"); handleError(error, "CreateDatabase", `Error while creating database ${params.databaseId}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -70,7 +70,7 @@ export async function createStoredProcedure(
.scripts.storedProcedures.create(storedProcedure); .scripts.storedProcedures.create(storedProcedure);
return response?.resource; return response?.resource;
} catch (error) { } catch (error) {
handleError(error, `Error while creating stored procedure ${storedProcedure.id}`, "CreateStoredProcedure"); handleError(error, "CreateStoredProcedure", `Error while creating stored procedure ${storedProcedure.id}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -7,9 +7,8 @@ import {
} from "../../Utils/arm/generatedClients/2020-04-01/types"; } from "../../Utils/arm/generatedClients/2020-04-01/types";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources"; import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { handleError } from "../ErrorHandlingUtils";
import { logError } from "../Logger"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { sendNotificationForError } from "./sendNotificationForError";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
export async function createTrigger( export async function createTrigger(
@@ -66,9 +65,7 @@ export async function createTrigger(
.scripts.triggers.create(trigger); .scripts.triggers.create(trigger);
return response.resource; return response.resource;
} catch (error) { } catch (error) {
logConsoleError(`Error while creating trigger ${trigger.id}:\n ${error.message}`); handleError(error, "CreateTrigger", `Error while creating trigger ${trigger.id}`);
logError(error.message, "CreateTrigger", error.code);
sendNotificationForError(error);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -72,8 +72,8 @@ export async function createUserDefinedFunction(
} catch (error) { } catch (error) {
handleError( handleError(
error, error,
`Error while creating user defined function ${userDefinedFunction.id}`, "CreateUserupdateUserDefinedFunction",
"CreateUserupdateUserDefinedFunction" `Error while creating user defined function ${userDefinedFunction.id}`
); );
throw error; throw error;
} finally { } finally {

View File

@@ -23,7 +23,7 @@ export async function deleteCollection(databaseId: string, collectionId: string)
} }
logConsoleInfo(`Successfully deleted container ${collectionId}`); logConsoleInfo(`Successfully deleted container ${collectionId}`);
} catch (error) { } catch (error) {
handleError(error, `Error while deleting container ${collectionId}`, "DeleteCollection"); handleError(error, "DeleteCollection", `Error while deleting container ${collectionId}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -25,7 +25,7 @@ export async function deleteDatabase(databaseId: string): Promise<void> {
} }
logConsoleInfo(`Successfully deleted database ${databaseId}`); logConsoleInfo(`Successfully deleted database ${databaseId}`);
} catch (error) { } catch (error) {
handleError(error, `Error while deleting database ${databaseId}`, "DeleteDatabase"); handleError(error, "DeleteDatabase", `Error while deleting database ${databaseId}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -34,7 +34,7 @@ export async function deleteStoredProcedure(
.delete(); .delete();
} }
} catch (error) { } catch (error) {
handleError(error, `Error while deleting stored procedure ${storedProcedureId}`, "DeleteStoredProcedure"); handleError(error, "DeleteStoredProcedure", `Error while deleting stored procedure ${storedProcedureId}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -30,7 +30,7 @@ export async function deleteTrigger(databaseId: string, collectionId: string, tr
.delete(); .delete();
} }
} catch (error) { } catch (error) {
handleError(error, `Error while deleting trigger ${triggerId}`, "DeleteTrigger"); handleError(error, "DeleteTrigger", `Error while deleting trigger ${triggerId}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -30,7 +30,7 @@ export async function deleteUserDefinedFunction(databaseId: string, collectionId
.delete(); .delete();
} }
} catch (error) { } catch (error) {
handleError(error, `Error while deleting user defined function ${id}`, "DeleteUserDefinedFunction"); handleError(error, "DeleteUserDefinedFunction", `Error while deleting user defined function ${id}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -0,0 +1,28 @@
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import * as Constants from "../Constants";
import { AuthType } from "../../AuthType";
export async function getIndexTransformationProgress(databaseId: string, collectionId: string): Promise<number> {
if (window.authType !== AuthType.AAD) {
return undefined;
}
let indexTransformationPercentage: number;
const clearMessage = logConsoleProgress(`Reading container ${collectionId}`);
try {
const response = await client()
.database(databaseId)
.container(collectionId)
.read({ populateQuotaInfo: true });
indexTransformationPercentage = parseInt(
response.headers[Constants.HttpHeaders.collectionIndexTransformationProgress] as string
);
} catch (error) {
handleError(error, "ReadMongoDBCollection", `Error while reading container ${collectionId}`);
throw error;
}
clearMessage();
return indexTransformationPercentage;
}

View File

@@ -13,7 +13,7 @@ export async function readCollection(databaseId: string, collectionId: string):
.read(); .read();
collection = response.resource as DataModels.Collection; collection = response.resource as DataModels.Collection;
} catch (error) { } catch (error) {
handleError(error, `Error while querying container ${collectionId}`, "ReadCollection"); handleError(error, "ReadCollection", `Error while querying container ${collectionId}`);
throw error; throw error;
} }
clearMessage(); clearMessage();

View File

@@ -56,7 +56,7 @@ export const readCollectionOffer = async (
} }
); );
} catch (error) { } catch (error) {
handleError(error, `Error while querying offer for collection ${params.collectionId}`, "ReadCollectionOffer"); handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -37,7 +37,7 @@ export const readCollectionQuotaInfo = async (
return quota; return quota;
} catch (error) { } catch (error) {
handleError(error, `Error while querying quota info for container ${collection.id}`, "ReadCollectionQuotaInfo"); handleError(error, "ReadCollectionQuotaInfo", `Error while querying quota info for container ${collection.id}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -29,7 +29,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
.fetchAll(); .fetchAll();
return sdkResponse.resources as DataModels.Collection[]; return sdkResponse.resources as DataModels.Collection[];
} catch (error) { } catch (error) {
handleError(error, `Error while querying containers for database ${databaseId}`, "ReadCollections"); handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -47,7 +47,7 @@ export const readDatabaseOffer = async (
} }
); );
} catch (error) { } catch (error) {
handleError(error, `Error while querying offer for database ${params.databaseId}`, "ReadDatabaseOffer"); handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -27,7 +27,7 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
databases = sdkResponse.resources as DataModels.Database[]; databases = sdkResponse.resources as DataModels.Database[];
} }
} catch (error) { } catch (error) {
handleError(error, `Error while querying databases`, "ReadDatabases"); handleError(error, "ReadDatabases", `Error while querying databases`);
throw error; throw error;
} }
clearMessage(); clearMessage();

View File

@@ -2,8 +2,6 @@ import { userContext } from "../../UserContext";
import { getMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources"; import { getMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { MongoDBCollectionResource } from "../../Utils/arm/generatedClients/2020-04-01/types"; import { MongoDBCollectionResource } from "../../Utils/arm/generatedClients/2020-04-01/types";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import * as Constants from "../Constants";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
@@ -24,35 +22,9 @@ export async function readMongoDBCollectionThroughRP(
const response = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId); const response = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
collection = response.properties.resource; collection = response.properties.resource;
} catch (error) { } catch (error) {
handleError(error, `Error while reading container ${collectionId}`, "ReadMongoDBCollection"); handleError(error, "ReadMongoDBCollection", `Error while reading container ${collectionId}`);
throw error; throw error;
} }
clearMessage(); clearMessage();
return collection; return collection;
} }
export async function getMongoDBCollectionIndexTransformationProgress(
databaseId: string,
collectionId: string
): Promise<number> {
if (window.authType !== AuthType.AAD) {
return undefined;
}
let indexTransformationPercentage: number;
const clearMessage = logConsoleProgress(`Reading container ${collectionId}`);
try {
const response = await client()
.database(databaseId)
.container(collectionId)
.read({ populateQuotaInfo: true });
indexTransformationPercentage = parseInt(
response.headers[Constants.HttpHeaders.collectionIndexTransformationProgress] as string
);
} catch (error) {
handleError(error, `Error while reading container ${collectionId}`, "ReadMongoDBCollection");
throw error;
}
clearMessage();
return indexTransformationPercentage;
}

View File

@@ -1,7 +1,7 @@
import { Offer } from "../../Contracts/DataModels"; import { Offer } from "../../Contracts/DataModels";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
export const readOffers = async (): Promise<Offer[]> => { export const readOffers = async (): Promise<Offer[]> => {
const clearMessage = logConsoleProgress(`Querying offers`); const clearMessage = logConsoleProgress(`Querying offers`);
@@ -13,11 +13,11 @@ export const readOffers = async (): Promise<Offer[]> => {
return response?.resources; return response?.resources;
} catch (error) { } catch (error) {
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too. // This should be removed when we can correctly identify if an account is serverless when connected using connection string too.
if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) { if (getErrorMessage(error)?.includes("Reading or replacing offers is not supported for serverless accounts")) {
return []; return [];
} }
handleError(error, `Error while querying offers`, "ReadOffers"); handleError(error, "ReadOffers", `Error while querying offers`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -35,7 +35,7 @@ export async function readStoredProcedures(
.fetchAll(); .fetchAll();
return response?.resources; return response?.resources;
} catch (error) { } catch (error) {
handleError(error, `Failed to query stored procedures for container ${collectionId}`, "ReadStoredProcedures"); handleError(error, "ReadStoredProcedures", `Failed to query stored procedures for container ${collectionId}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -35,7 +35,7 @@ export async function readTriggers(
.fetchAll(); .fetchAll();
return response?.resources; return response?.resources;
} catch (error) { } catch (error) {
handleError(error, `Failed to query triggers for container ${collectionId}`, "ReadTriggers"); handleError(error, "ReadTriggers", `Failed to query triggers for container ${collectionId}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -37,8 +37,8 @@ export async function readUserDefinedFunctions(
} catch (error) { } catch (error) {
handleError( handleError(
error, error,
`Failed to query user defined functions for container ${collectionId}`, "ReadUserDefinedFunctions",
"ReadUserDefinedFunctions" `Failed to query user defined functions for container ${collectionId}`
); );
throw error; throw error;
} finally { } finally {

View File

@@ -1,20 +0,0 @@
import * as Constants from "../Constants";
import { sendMessage } from "../MessageHandler";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
export interface CosmosError {
code: number;
message?: string;
}
export function sendNotificationForError(error: CosmosError): void {
if (error && error.code === Constants.HttpStatusCodes.Forbidden) {
if (error.message && error.message.toLowerCase().indexOf("sharedoffer is disabled for your account") > 0) {
return;
}
sendMessage({
type: MessageTypes.ForbiddenError,
reason: error && error.message ? error.message : error
});
}
}

View File

@@ -59,7 +59,7 @@ export async function updateCollection(
logConsoleInfo(`Successfully updated container ${collectionId}`); logConsoleInfo(`Successfully updated container ${collectionId}`);
return collection; return collection;
} catch (error) { } catch (error) {
handleError(error, `Failed to update container ${collectionId}`, "UpdateCollection"); handleError(error, "UpdateCollection", `Failed to update container ${collectionId}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -72,7 +72,7 @@ export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> =>
logConsoleInfo(`Successfully updated offer for ${offerResourceText}`); logConsoleInfo(`Successfully updated offer for ${offerResourceText}`);
return updatedOffer; return updatedOffer;
} catch (error) { } catch (error) {
handleError(error, `Error updating offer for ${offerResourceText}`, "UpdateCollection"); handleError(error, "UpdateCollection", `Error updating offer for ${offerResourceText}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -1,8 +1,9 @@
import { Platform, configContext } from "../../ConfigContext"; import { Platform, configContext } from "../../ConfigContext";
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import { AutoPilotOfferSettings } from "../../Contracts/DataModels"; import { AutoPilotOfferSettings } from "../../Contracts/DataModels";
import { logConsoleProgress, logConsoleInfo, logConsoleError } from "../../Utils/NotificationConsoleUtils"; import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { HttpHeaders } from "../Constants"; import { HttpHeaders } from "../Constants";
import { handleError } from "../ErrorHandlingUtils";
interface UpdateOfferThroughputRequest { interface UpdateOfferThroughputRequest {
subscriptionId: string; subscriptionId: string;
@@ -44,8 +45,13 @@ export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThrou
clearMessage(); clearMessage();
return undefined; return undefined;
} }
const error = await response.json(); const error = await response.json();
logConsoleError(`Failed to request an increase in throughput for ${request.throughput}: ${error.message}`); handleError(
error,
"updateOfferThroughputBeyondLimit",
`Failed to request an increase in throughput for ${request.throughput}`
);
clearMessage(); clearMessage();
throw new Error(error.message); throw error;
} }

View File

@@ -64,7 +64,7 @@ export async function updateStoredProcedure(
.replace(storedProcedure); .replace(storedProcedure);
return response?.resource; return response?.resource;
} catch (error) { } catch (error) {
handleError(error, `Error while updating stored procedure ${storedProcedure.id}`, "UpdateStoredProcedure"); handleError(error, "UpdateStoredProcedure", `Error while updating stored procedure ${storedProcedure.id}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -61,7 +61,7 @@ export async function updateTrigger(
.replace(trigger); .replace(trigger);
return response?.resource; return response?.resource;
} catch (error) { } catch (error) {
handleError(error, `Error while updating trigger ${trigger.id}`, "UpdateTrigger"); handleError(error, "UpdateTrigger", `Error while updating trigger ${trigger.id}`);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -66,8 +66,8 @@ export async function updateUserDefinedFunction(
} catch (error) { } catch (error) {
handleError( handleError(
error, error,
`Error while updating user defined function ${userDefinedFunction.id}`, "UpdateUserupdateUserDefinedFunction",
"UpdateUserupdateUserDefinedFunction" `Error while updating user defined function ${userDefinedFunction.id}`
); );
throw error; throw error;
} finally { } finally {

View File

@@ -216,18 +216,6 @@ export interface UniqueKey {
paths: string[]; paths: string[];
} }
// Returned by DocumentDb client proxy
// Inner errors in BackendErrorDataModel when error is in SQL syntax
export interface ErrorDataModel {
message: string;
severity?: string;
location?: {
start: string;
end: string;
};
code?: string;
}
export interface CreateDatabaseAndCollectionRequest { export interface CreateDatabaseAndCollectionRequest {
databaseId: string; databaseId: string;
collectionId: string; collectionId: string;
@@ -239,21 +227,12 @@ export interface CreateDatabaseAndCollectionRequest {
uniqueKeyPolicy?: UniqueKeyPolicy; uniqueKeyPolicy?: UniqueKeyPolicy;
autoPilot?: AutoPilotCreationSettings; autoPilot?: AutoPilotCreationSettings;
analyticalStorageTtl?: number; analyticalStorageTtl?: number;
hasAutoPilotV2FeatureFlag?: boolean;
} }
export interface AutoPilotCreationSettings { export interface AutoPilotCreationSettings {
autopilotTier?: AutopilotTier;
maxThroughput?: number; maxThroughput?: number;
} }
export enum AutopilotTier {
Tier1 = 1,
Tier2 = 2,
Tier3 = 3,
Tier4 = 4
}
export interface Query { export interface Query {
id: string; id: string;
resourceId: string; resourceId: string;
@@ -262,9 +241,7 @@ export interface Query {
} }
export interface AutoPilotOfferSettings { export interface AutoPilotOfferSettings {
tier?: AutopilotTier;
maximumTierThroughput?: number; maximumTierThroughput?: number;
targetTier?: AutopilotTier;
maxThroughput?: number; maxThroughput?: number;
targetMaxThroughput?: number; targetMaxThroughput?: number;
} }
@@ -491,7 +468,6 @@ export interface MongoParameters extends RpParameters {
rid?: string; rid?: string;
rtype?: string; rtype?: string;
isAutoPilot?: Boolean; isAutoPilot?: Boolean;
autoPilotTier?: string;
autoPilotThroughput?: string; autoPilotThroughput?: string;
analyticalStorageTtl?: number; analyticalStorageTtl?: number;
} }

View File

@@ -46,7 +46,7 @@ export interface LogEntry {
/** /**
* The message code. * The message code.
*/ */
code?: number; code?: number | string;
/** /**
* Any additional data to be logged. * Any additional data to be logged.
*/ */

View File

@@ -162,6 +162,7 @@ export interface Collection extends CollectionBase {
loadStoredProcedures(): Promise<any>; loadStoredProcedures(): Promise<any>;
loadTriggers(): Promise<any>; loadTriggers(): Promise<any>;
loadOffer(): Promise<void>; loadOffer(): Promise<void>;
loadAutopilotOfferWithRetry(): Promise<DataModels.Offer>;
createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure; createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure;
createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction; createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction;

View File

@@ -123,8 +123,4 @@ describe("Component Registerer", () => {
it("should register dynamic-list component", () => { it("should register dynamic-list component", () => {
expect(ko.components.isRegistered("dynamic-list")).toBe(true); expect(ko.components.isRegistered("dynamic-list")).toBe(true);
}); });
it("should register throughput-input component", () => {
expect(ko.components.isRegistered("throughput-input")).toBe(true);
});
}); });

View File

@@ -11,7 +11,6 @@ import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahea
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent"; import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent";
import { TabsManagerKOComponent } from "./Tabs/TabsManager"; import { TabsManagerKOComponent } from "./Tabs/TabsManager";
import { ThroughputInputComponent } from "./Controls/ThroughputInput/ThroughputInputComponent";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3"; import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
ko.components.register("input-typeahead", new InputTypeaheadComponent()); ko.components.register("input-typeahead", new InputTypeaheadComponent());
@@ -23,7 +22,6 @@ ko.components.register("editor", new EditorComponent());
ko.components.register("json-editor", new JsonEditorComponent()); ko.components.register("json-editor", new JsonEditorComponent());
ko.components.register("diff-editor", new DiffEditorComponent()); ko.components.register("diff-editor", new DiffEditorComponent());
ko.components.register("dynamic-list", DynamicListComponent); ko.components.register("dynamic-list", DynamicListComponent);
ko.components.register("throughput-input", ThroughputInputComponent);
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3); ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
ko.components.register("tabs-manager", TabsManagerKOComponent()); ko.components.register("tabs-manager", TabsManagerKOComponent());

View File

@@ -1,12 +1,9 @@
import * as React from "react"; import * as React from "react";
import { ArcadiaWorkspace, SparkPool } from "../../../Contracts/DataModels"; import { ArcadiaWorkspace, SparkPool } from "../../../Contracts/DataModels";
import { DefaultButton, IButtonStyles } from "office-ui-fabric-react/lib/Button"; import { DefaultButton, IButtonStyles } from "office-ui-fabric-react/lib/Button";
import { import { IContextualMenuItem, IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
IContextualMenuItem,
IContextualMenuProps,
ContextualMenuItemType
} from "office-ui-fabric-react/lib/ContextualMenu";
import * as Logger from "../../../Common/Logger"; import * as Logger from "../../../Common/Logger";
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
export interface ArcadiaMenuPickerProps { export interface ArcadiaMenuPickerProps {
selectText?: string; selectText?: string;
@@ -47,7 +44,7 @@ export class ArcadiaMenuPicker extends React.Component<ArcadiaMenuPickerProps, A
selectedSparkPool: item.text selectedSparkPool: item.text
}); });
} catch (error) { } catch (error) {
Logger.logError(error, "ArcadiaMenuPicker/_onSparkPoolClicked"); Logger.logError(getErrorMessage(error), "ArcadiaMenuPicker/_onSparkPoolClicked");
throw error; throw error;
} }
}; };

View File

@@ -4,12 +4,10 @@
import * as React from "react"; import * as React from "react";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as Logger from "../../../Common/Logger";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { StringUtils } from "../../../Utils/StringUtils"; import { StringUtils } from "../../../Utils/StringUtils";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { TerminalQueryParams } from "../../../Common/Constants"; import { TerminalQueryParams } from "../../../Common/Constants";
import { handleError } from "../../../Common/ErrorHandlingUtils";
export interface NotebookTerminalComponentProps { export interface NotebookTerminalComponentProps {
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo; notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
@@ -71,9 +69,10 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
params: Map<string, string> params: Map<string, string>
): string { ): string {
if (!serverInfo.notebookServerEndpoint) { if (!serverInfo.notebookServerEndpoint) {
const error = "Notebook server endpoint not defined. Terminal will fail to connect to jupyter server."; handleError(
Logger.logError(error, "NotebookTerminalComponent/createNotebookAppSrc"); "Notebook server endpoint not defined. Terminal will fail to connect to jupyter server.",
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error); "NotebookTerminalComponent/createNotebookAppSrc"
);
return ""; return "";
} }

View File

@@ -1,9 +1,8 @@
import * as React from "react"; import * as React from "react";
import { JunoClient } from "../../../Juno/JunoClient"; import { JunoClient } from "../../../Juno/JunoClient";
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants"; import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
import * as Logger from "../../../Common/Logger";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react"; import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react";
import { handleError } from "../../../Common/ErrorHandlingUtils";
export interface CodeOfConductComponentProps { export interface CodeOfConductComponentProps {
junoClient: JunoClient; junoClient: JunoClient;
@@ -45,9 +44,7 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
this.props.onAcceptCodeOfConduct(response.data); this.props.onAcceptCodeOfConduct(response.data);
} catch (error) { } catch (error) {
const message = `Failed to accept code of conduct: ${error}`; handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct");
Logger.logError(message, "CodeOfConductComponent/acceptCodeOfConduct");
logConsoleError(message);
} }
} }

View File

@@ -16,11 +16,8 @@ import {
Text Text
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import * as Logger from "../../../Common/Logger";
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient"; import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils"; import * as GalleryUtils from "../../../Utils/GalleryUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent"; import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent"; import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
import "./GalleryViewerComponent.less"; import "./GalleryViewerComponent.less";
@@ -28,6 +25,7 @@ import { HttpStatusCodes } from "../../../Common/Constants";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { CodeOfConductComponent } from "./CodeOfConductComponent"; import { CodeOfConductComponent } from "./CodeOfConductComponent";
import { InfoComponent } from "./InfoComponent/InfoComponent"; import { InfoComponent } from "./InfoComponent/InfoComponent";
import { handleError } from "../../../Common/ErrorHandlingUtils";
export interface GalleryViewerComponentProps { export interface GalleryViewerComponentProps {
container?: Explorer; container?: Explorer;
@@ -354,9 +352,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
this.sampleNotebooks = response.data; this.sampleNotebooks = response.data;
} catch (error) { } catch (error) {
const message = `Failed to load sample notebooks: ${error}`; handleError(error, "GalleryViewerComponent/loadSampleNotebooks", "Failed to load sample notebooks");
Logger.logError(message, "GalleryViewerComponent/loadSampleNotebooks");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
} }
} }
@@ -382,9 +378,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
throw new Error(`Received HTTP ${response.status} when loading public notebooks`); throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
} }
} catch (error) { } catch (error) {
const message = `Failed to load public notebooks: ${error}`; handleError(error, "GalleryViewerComponent/loadPublicNotebooks", "Failed to load public notebooks");
Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
} }
} }
@@ -404,9 +398,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
this.favoriteNotebooks = response.data; this.favoriteNotebooks = response.data;
} catch (error) { } catch (error) {
const message = `Failed to load favorite notebooks: ${error}`; handleError(error, "GalleryViewerComponent/loadFavoriteNotebooks", "Failed to load favorite notebooks");
Logger.logError(message, "GalleryViewerComponent/loadFavoriteNotebooks");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
} }
} }
@@ -432,9 +424,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
this.publishedNotebooks = response.data; this.publishedNotebooks = response.data;
} catch (error) { } catch (error) {
const message = `Failed to load published notebooks: ${error}`; handleError(error, "GalleryViewerComponent/loadPublishedNotebooks", "Failed to load published notebooks");
Logger.logError(message, "GalleryViewerComponent/loadPublishedNotebooks");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
} }
} }

View File

@@ -21,6 +21,7 @@ import Explorer from "../../Explorer";
import { NotebookV4 } from "@nteract/commutable/lib/v4"; import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { SessionStorageUtility } from "../../../Shared/StorageUtility"; import { SessionStorageUtility } from "../../../Shared/StorageUtility";
import { DialogHost } from "../../../Utils/GalleryUtils"; import { DialogHost } from "../../../Utils/GalleryUtils";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
export interface NotebookViewerComponentProps { export interface NotebookViewerComponentProps {
container?: Explorer; container?: Explorer;
@@ -100,9 +101,7 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
} }
} catch (error) { } catch (error) {
this.setState({ showProgressBar: false }); this.setState({ showProgressBar: false });
const message = `Failed to load notebook content: ${error}`; handleError(error, "NotebookViewerComponent/loadNotebookContent", "Failed to load notebook content");
Logger.logError(message, "NotebookViewerComponent/loadNotebookContent");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
} }
} }

View File

@@ -28,6 +28,7 @@ import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcesso
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png"; import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
import { QueriesClient } from "../../../Common/QueriesClient"; import { QueriesClient } from "../../../Common/QueriesClient";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
export interface QueriesGridComponentProps { export interface QueriesGridComponentProps {
queriesClient: QueriesClient; queriesClient: QueriesClient;
@@ -244,7 +245,9 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
databaseAccountName: container && container.databaseAccount().name, databaseAccountName: container && container.databaseAccount().name,
defaultExperience: container && container.defaultExperience(), defaultExperience: container && container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: container && container.browseQueriesPane.title() paneTitle: container && container.browseQueriesPane.title(),
error: getErrorMessage(error),
errorStack: getErrorStack(error)
}, },
startKey startKey
); );

View File

@@ -8,8 +8,8 @@ import * as DataModels from "../../../Contracts/DataModels";
import ko from "knockout"; import ko from "knockout";
import { TtlType, isDirty } from "./SettingsUtils"; import { TtlType, isDirty } from "./SettingsUtils";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
jest.mock("../../../Common/dataAccess/readMongoDBCollection", () => ({ jest.mock("../../../Common/dataAccess/getIndexTransformationProgress", () => ({
getMongoDBCollectionIndexTransformationProgress: jest.fn().mockReturnValue(undefined) getIndexTransformationProgress: jest.fn().mockReturnValue(undefined)
})); }));
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection"; import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
jest.mock("../../../Common/dataAccess/updateCollection", () => ({ jest.mock("../../../Common/dataAccess/updateCollection", () => ({

View File

@@ -46,10 +46,9 @@ import { Pivot, PivotItem, IPivotProps, IPivotItemProps } from "office-ui-fabric
import "./SettingsComponent.less"; import "./SettingsComponent.less";
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent"; import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types"; import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import { import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
getMongoDBCollectionIndexTransformationProgress, import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
readMongoDBCollectionThroughRP import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
} from "../../../Common/dataAccess/readMongoDBCollection";
interface SettingsV2TabInfo { interface SettingsV2TabInfo {
tab: SettingsV2TabTypes; tab: SettingsV2TabTypes;
@@ -125,7 +124,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private container: Explorer; private container: Explorer;
private changeFeedPolicyVisible: boolean; private changeFeedPolicyVisible: boolean;
private isFixedContainer: boolean; private isFixedContainer: boolean;
private autoPilotTiersList: ViewModels.DropdownOption<DataModels.AutopilotTier>[];
private shouldShowIndexingPolicyEditor: boolean; private shouldShowIndexingPolicyEditor: boolean;
public mongoDBCollectionResource: MongoDBCollectionResource; public mongoDBCollectionResource: MongoDBCollectionResource;
@@ -212,6 +210,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
} }
componentDidMount(): void { componentDidMount(): void {
this.refreshIndexTransformationProgress();
this.loadMongoIndexes(); this.loadMongoIndexes();
this.setAutoPilotStates(); this.setAutoPilotStates();
this.setBaseline(); this.setBaseline();
@@ -230,10 +229,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
if ( if (
this.container.isMongoIndexEditorEnabled() && this.container.isMongoIndexEditorEnabled() &&
this.container.isPreferredApiMongoDB() && this.container.isPreferredApiMongoDB() &&
this.container.isEnableMongoCapabilityPresent() &&
this.container.databaseAccount() this.container.databaseAccount()
) { ) {
await this.refreshIndexTransformationProgress();
this.mongoDBCollectionResource = await readMongoDBCollectionThroughRP( this.mongoDBCollectionResource = await readMongoDBCollectionThroughRP(
this.collection.databaseId, this.collection.databaseId,
this.collection.id() this.collection.id()
@@ -248,10 +246,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}; };
public refreshIndexTransformationProgress = async (): Promise<void> => { public refreshIndexTransformationProgress = async (): Promise<void> => {
const currentProgress = await getMongoDBCollectionIndexTransformationProgress( const currentProgress = await getIndexTransformationProgress(this.collection.databaseId, this.collection.id());
this.collection.databaseId,
this.collection.id()
);
this.setState({ indexTransformationProgress: currentProgress }); this.setState({ indexTransformationProgress: currentProgress });
}; };
@@ -351,6 +346,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
break; break;
} }
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
newCollection.defaultTtl = defaultTtl; newCollection.defaultTtl = defaultTtl;
newCollection.indexingPolicy = this.state.indexingPolicyContent; newCollection.indexingPolicy = this.state.indexingPolicyContent;
@@ -386,6 +382,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy); this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy); this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
this.collection.geospatialConfig(updatedCollection.geospatialConfig); this.collection.geospatialConfig(updatedCollection.geospatialConfig);
if (wasIndexingPolicyModified) {
await this.refreshIndexTransformationProgress();
}
this.setState({ this.setState({
isSubSettingsSaveable: false, isSubSettingsSaveable: false,
isSubSettingsDiscardable: false, isSubSettingsDiscardable: false,
@@ -395,24 +396,55 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
} }
if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) { if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) {
const newMongoIndexes = this.getMongoIndexesToSave(); try {
const newMongoCollection: MongoDBCollectionResource = { const newMongoIndexes = this.getMongoIndexesToSave();
...this.mongoDBCollectionResource, const newMongoCollection: MongoDBCollectionResource = {
indexes: newMongoIndexes ...this.mongoDBCollectionResource,
}; indexes: newMongoIndexes
this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP( };
this.collection.databaseId,
this.collection.id(),
newMongoCollection
);
await this.refreshIndexTransformationProgress(); this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP(
this.setState({ this.collection.databaseId,
isMongoIndexingPolicySaveable: false, this.collection.id(),
indexesToDrop: [], newMongoCollection
indexesToAdd: [], );
currentMongoIndexes: [...this.mongoDBCollectionResource.indexes]
}); await this.refreshIndexTransformationProgress();
this.setState({
isMongoIndexingPolicySaveable: false,
indexesToDrop: [],
indexesToAdd: [],
currentMongoIndexes: [...this.mongoDBCollectionResource.indexes]
});
traceSuccess(
Action.MongoIndexUpdated,
{
databaseAccountName: this.container.databaseAccount()?.name,
databaseName: this.collection?.databaseId,
collectionName: this.collection?.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle()
},
startKey
);
} catch (error) {
traceFailure(
Action.MongoIndexUpdated,
{
databaseAccountName: this.container.databaseAccount()?.name,
databaseName: this.collection?.databaseId,
collectionName: this.collection?.id(),
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error)
},
startKey
);
throw error;
}
} }
if (this.state.isScaleSaveable) { if (this.state.isScaleSaveable) {
@@ -500,11 +532,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
} }
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams); const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
this.collection.offer(updatedOffer); this.collection.offer(updatedOffer);
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
if (this.state.isAutoPilotSelected) { if (this.state.isAutoPilotSelected) {
const autoPilotOffer = await this.collection.loadAutopilotOfferWithRetry();
this.setState({ this.setState({
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput, autoPilotThroughput: autoPilotOffer.content.offerAutopilotSettings.maxThroughput,
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput autoPilotThroughputBaseline: autoPilotOffer.content.offerAutopilotSettings.maxThroughput
}); });
} else { } else {
this.setState({ this.setState({
@@ -512,6 +545,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
throughputBaseline: updatedOffer.content.offerThroughput throughputBaseline: updatedOffer.content.offerThroughput
}); });
} }
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
} }
} }
this.container.isRefreshingExplorer(false); this.container.isRefreshingExplorer(false);
@@ -529,10 +564,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}, },
startKey startKey
); );
} catch (reason) { } catch (error) {
this.container.isRefreshingExplorer(false); this.container.isRefreshingExplorer(false);
this.props.settingsTab.isExecutionError(true); this.props.settingsTab.isExecutionError(true);
console.error(reason); console.error(error);
traceFailure( traceFailure(
Action.SettingsV2Updated, Action.SettingsV2Updated,
{ {
@@ -542,7 +577,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
defaultExperience: this.container.defaultExperience(), defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab, dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.props.settingsTab.tabTitle(), tabTitle: this.props.settingsTab.tabTitle(),
error: reason error: getErrorMessage(error),
errorStack: getErrorStack(error)
}, },
startKey startKey
); );
@@ -867,7 +903,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
collection: this.collection, collection: this.collection,
container: this.container, container: this.container,
isFixedContainer: this.isFixedContainer, isFixedContainer: this.isFixedContainer,
autoPilotTiersList: this.autoPilotTiersList,
onThroughputChange: this.onThroughputChange, onThroughputChange: this.onThroughputChange,
throughput: this.state.throughput, throughput: this.state.throughput,
throughputBaseline: this.state.throughputBaseline, throughputBaseline: this.state.throughputBaseline,
@@ -916,6 +951,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
indexingPolicyContentBaseline: this.state.indexingPolicyContentBaseline, indexingPolicyContentBaseline: this.state.indexingPolicyContentBaseline,
onIndexingPolicyContentChange: this.onIndexingPolicyContentChange, onIndexingPolicyContentChange: this.onIndexingPolicyContentChange,
logIndexingPolicySuccessMessage: this.logIndexingPolicySuccessMessage, logIndexingPolicySuccessMessage: this.logIndexingPolicySuccessMessage,
indexTransformationProgress: this.state.indexTransformationProgress,
refreshIndexTransformationProgress: this.refreshIndexTransformationProgress,
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange
}; };

View File

@@ -6,7 +6,7 @@ import {
getEstimatedAutoscaleSpendElement, getEstimatedAutoscaleSpendElement,
manualToAutoscaleDisclaimerElement, manualToAutoscaleDisclaimerElement,
ttlWarning, ttlWarning,
indexingPolicyTTLWarningMessage, indexingPolicynUnsavedWarningMessage,
updateThroughputBeyondLimitWarningMessage, updateThroughputBeyondLimitWarningMessage,
updateThroughputDelayedApplyWarningMessage, updateThroughputDelayedApplyWarningMessage,
getThroughputApplyDelayedMessage, getThroughputApplyDelayedMessage,
@@ -37,7 +37,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
{manualToAutoscaleDisclaimerElement} {manualToAutoscaleDisclaimerElement}
{ttlWarning} {ttlWarning}
{indexingPolicyTTLWarningMessage} {indexingPolicynUnsavedWarningMessage}
{updateThroughputBeyondLimitWarningMessage} {updateThroughputBeyondLimitWarningMessage}
{updateThroughputDelayedApplyWarningMessage} {updateThroughputDelayedApplyWarningMessage}

View File

@@ -36,7 +36,7 @@ import {
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import { isDirtyTypes, isDirty } from "./SettingsUtils"; import { isDirtyTypes, isDirty } from "./SettingsUtils";
const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } }; export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = { export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
label: { label: {
@@ -245,15 +245,9 @@ export const ttlWarning: JSX.Element = (
</Text> </Text>
); );
export const indexingPolicyTTLWarningMessage: JSX.Element = ( export const indexingPolicynUnsavedWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}> <Text styles={infoAndToolTipTextStyle}>
Changing the Indexing Policy impacts query results while the index transformation occurs. When a change is made and You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
the indexing mode is set to consistent or lazy, queries return eventual results until the operation completes. For
more information see,{" "}
<Link target="_blank" href="https://aka.ms/cosmosdb/modify-index-policy">
Modifying Indexing Policies
</Link>
.
</Text> </Text>
); );
@@ -410,8 +404,8 @@ export const mongoIndexingPolicyAADError: JSX.Element = (
export const mongoIndexTransformationRefreshingMessage: JSX.Element = ( export const mongoIndexTransformationRefreshingMessage: JSX.Element = (
<Stack horizontal {...mongoWarningStackProps}> <Stack horizontal {...mongoWarningStackProps}>
<Text>Refreshing index transformation progress</Text> <Text styles={infoAndToolTipTextStyle}>Refreshing index transformation progress</Text>
<Spinner size={SpinnerSize.medium} /> <Spinner size={SpinnerSize.small} />
</Stack> </Stack>
); );
@@ -421,14 +415,14 @@ export const renderMongoIndexTransformationRefreshMessage = (
): JSX.Element => { ): JSX.Element => {
if (progress === 0) { if (progress === 0) {
return ( return (
<Text> <Text styles={infoAndToolTipTextStyle}>
{"You can make more indexing changes once the current index transformation is complete. "} {"You can make more indexing changes once the current index transformation is complete. "}
<Link onClick={performRefresh}>{"Refresh to check if it has completed."}</Link> <Link onClick={performRefresh}>{"Refresh to check if it has completed."}</Link>
</Text> </Text>
); );
} else { } else {
return ( return (
<Text> <Text styles={infoAndToolTipTextStyle}>
{`You can make more indexing changes once the current index transformation has completed. It is ${progress}% complete. `} {`You can make more indexing changes once the current index transformation has completed. It is ${progress}% complete. `}
<Link onClick={performRefresh}>{"Refresh to check the progress."}</Link> <Link onClick={performRefresh}>{"Refresh to check the progress."}</Link>
</Text> </Text>

View File

@@ -25,7 +25,9 @@ describe("IndexingPolicyComponent", () => {
}, },
onIndexingPolicyDirtyChange: () => { onIndexingPolicyDirtyChange: () => {
return; return;
} },
indexTransformationProgress: undefined,
refreshIndexTransformationProgress: () => new Promise(jest.fn())
}; };
it("renders", () => { it("renders", () => {

View File

@@ -1,9 +1,10 @@
import * as React from "react"; import * as React from "react";
import * as DataModels from "../../../../Contracts/DataModels"; import * as DataModels from "../../../../Contracts/DataModels";
import * as monaco from "monaco-editor"; import * as monaco from "monaco-editor";
import { isDirty } from "../SettingsUtils"; import { isDirty, isIndexTransforming } from "../SettingsUtils";
import { MessageBar, MessageBarType, Stack } from "office-ui-fabric-react"; import { MessageBar, MessageBarType, Stack } from "office-ui-fabric-react";
import { indexingPolicyTTLWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils"; import { indexingPolicynUnsavedWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils";
import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
export interface IndexingPolicyComponentProps { export interface IndexingPolicyComponentProps {
shouldDiscardIndexingPolicy: boolean; shouldDiscardIndexingPolicy: boolean;
@@ -12,6 +13,8 @@ export interface IndexingPolicyComponentProps {
indexingPolicyContentBaseline: DataModels.IndexingPolicy; indexingPolicyContentBaseline: DataModels.IndexingPolicy;
onIndexingPolicyContentChange: (newIndexingPolicy: DataModels.IndexingPolicy) => void; onIndexingPolicyContentChange: (newIndexingPolicy: DataModels.IndexingPolicy) => void;
logIndexingPolicySuccessMessage: () => void; logIndexingPolicySuccessMessage: () => void;
indexTransformationProgress: number;
refreshIndexTransformationProgress: () => Promise<void>;
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void; onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
} }
@@ -51,6 +54,9 @@ export class IndexingPolicyComponent extends React.Component<
if (!this.indexingPolicyEditor) { if (!this.indexingPolicyEditor) {
this.createIndexingPolicyEditor(); this.createIndexingPolicyEditor();
} else { } else {
this.indexingPolicyEditor.updateOptions({
readOnly: isIndexTransforming(this.props.indexTransformationProgress)
});
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel(); const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4); const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4);
indexingPolicyEditorModel.setValue(value); indexingPolicyEditorModel.setValue(value);
@@ -84,7 +90,7 @@ export class IndexingPolicyComponent extends React.Component<
this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, { this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, {
value: value, value: value,
language: "json", language: "json",
readOnly: false, readOnly: isIndexTransforming(this.props.indexTransformationProgress),
ariaLabel: "Indexing Policy" ariaLabel: "Indexing Policy"
}); });
if (this.indexingPolicyEditor) { if (this.indexingPolicyEditor) {
@@ -108,8 +114,12 @@ export class IndexingPolicyComponent extends React.Component<
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<Stack {...titleAndInputStackProps}> <Stack {...titleAndInputStackProps}>
<IndexingPolicyRefreshComponent
indexTransformationProgress={this.props.indexTransformationProgress}
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
/>
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && ( {isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicyTTLWarningMessage}</MessageBar> <MessageBar messageBarType={MessageBarType.warning}>{indexingPolicynUnsavedWarningMessage}</MessageBar>
)} )}
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div> <div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
</Stack> </Stack>

View File

@@ -0,0 +1,15 @@
import { shallow } from "enzyme";
import React from "react";
import { IndexingPolicyRefreshComponentProps, IndexingPolicyRefreshComponent } from "./IndexingPolicyRefreshComponent";
describe("IndexingPolicyRefreshComponent", () => {
it("renders", () => {
const props: IndexingPolicyRefreshComponentProps = {
indexTransformationProgress: 90,
refreshIndexTransformationProgress: () => new Promise(jest.fn())
};
const wrapper = shallow(<IndexingPolicyRefreshComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,62 @@
import * as React from "react";
import { MessageBar, MessageBarType } from "office-ui-fabric-react";
import {
mongoIndexTransformationRefreshingMessage,
renderMongoIndexTransformationRefreshMessage
} from "../../SettingsRenderUtils";
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
import { isIndexTransforming } from "../../SettingsUtils";
export interface IndexingPolicyRefreshComponentProps {
indexTransformationProgress: number;
refreshIndexTransformationProgress: () => Promise<void>;
}
interface IndexingPolicyRefreshComponentState {
isRefreshing: boolean;
}
export class IndexingPolicyRefreshComponent extends React.Component<
IndexingPolicyRefreshComponentProps,
IndexingPolicyRefreshComponentState
> {
constructor(props: IndexingPolicyRefreshComponentProps) {
super(props);
this.state = {
isRefreshing: false
};
}
private onClickRefreshIndexingTransformationLink = async () => await this.refreshIndexTransformationProgress();
private renderIndexTransformationWarning = (): JSX.Element => {
if (this.state.isRefreshing) {
return mongoIndexTransformationRefreshingMessage;
} else if (isIndexTransforming(this.props.indexTransformationProgress)) {
return renderMongoIndexTransformationRefreshMessage(
this.props.indexTransformationProgress,
this.onClickRefreshIndexingTransformationLink
);
}
return undefined;
};
private refreshIndexTransformationProgress = async () => {
this.setState({ isRefreshing: true });
try {
await this.props.refreshIndexTransformationProgress();
} catch (error) {
handleError(error, "RefreshIndexTransformationProgress", "Refreshing index transformation progress failed");
} finally {
this.setState({ isRefreshing: false });
}
};
public render(): JSX.Element {
return this.renderIndexTransformationWarning() ? (
<MessageBar messageBarType={MessageBarType.warning}>{this.renderIndexTransformationWarning()}</MessageBar>
) : (
<></>
);
}
}

View File

@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`IndexingPolicyRefreshComponent renders 1`] = `
<StyledMessageBarBase
messageBarType={5}
>
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
<StyledLinkBase
onClick={[Function]}
>
Refresh to check the progress.
</StyledLinkBase>
</Text>
</StyledMessageBarBase>
`;

View File

@@ -2,6 +2,7 @@ import { shallow } from "enzyme";
import React from "react"; import React from "react";
import { MongoIndexTypes, MongoNotificationMessage, MongoNotificationType } from "../../SettingsUtils"; import { MongoIndexTypes, MongoNotificationMessage, MongoNotificationType } from "../../SettingsUtils";
import { MongoIndexingPolicyComponent, MongoIndexingPolicyComponentProps } from "./MongoIndexingPolicyComponent"; import { MongoIndexingPolicyComponent, MongoIndexingPolicyComponentProps } from "./MongoIndexingPolicyComponent";
import { renderToString } from "react-dom/server";
describe("MongoIndexingPolicyComponent", () => { describe("MongoIndexingPolicyComponent", () => {
const baseProps: MongoIndexingPolicyComponentProps = { const baseProps: MongoIndexingPolicyComponentProps = {
@@ -21,10 +22,7 @@ describe("MongoIndexingPolicyComponent", () => {
return; return;
}, },
indexTransformationProgress: undefined, indexTransformationProgress: undefined,
refreshIndexTransformationProgress: () => refreshIndexTransformationProgress: () => new Promise(jest.fn()),
new Promise(() => {
return;
}),
onMongoIndexingPolicySaveableChange: () => { onMongoIndexingPolicySaveableChange: () => {
return; return;
}, },
@@ -38,16 +36,6 @@ describe("MongoIndexingPolicyComponent", () => {
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("isIndexingTransforming", () => {
const wrapper = shallow(<MongoIndexingPolicyComponent {...baseProps} />);
const mongoIndexingPolicyComponent = wrapper.instance() as MongoIndexingPolicyComponent;
expect(mongoIndexingPolicyComponent.isIndexingTransforming()).toEqual(false);
wrapper.setProps({ indexTransformationProgress: 50 });
expect(mongoIndexingPolicyComponent.isIndexingTransforming()).toEqual(true);
wrapper.setProps({ indexTransformationProgress: 100 });
expect(mongoIndexingPolicyComponent.isIndexingTransforming()).toEqual(false);
});
describe("AddMongoIndexProps test", () => { describe("AddMongoIndexProps test", () => {
const wrapper = shallow(<MongoIndexingPolicyComponent {...baseProps} />); const wrapper = shallow(<MongoIndexingPolicyComponent {...baseProps} />);
const mongoIndexingPolicyComponent = wrapper.instance() as MongoIndexingPolicyComponent; const mongoIndexingPolicyComponent = wrapper.instance() as MongoIndexingPolicyComponent;
@@ -55,7 +43,7 @@ describe("MongoIndexingPolicyComponent", () => {
it("defaults", () => { it("defaults", () => {
expect(mongoIndexingPolicyComponent.isMongoIndexingPolicySaveable()).toEqual(false); expect(mongoIndexingPolicyComponent.isMongoIndexingPolicySaveable()).toEqual(false);
expect(mongoIndexingPolicyComponent.isMongoIndexingPolicyDiscardable()).toEqual(false); expect(mongoIndexingPolicyComponent.isMongoIndexingPolicyDiscardable()).toEqual(false);
expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toEqual(undefined); expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toBeUndefined();
}); });
const sampleWarning = "sampleWarning"; const sampleWarning = "sampleWarning";
@@ -113,9 +101,12 @@ describe("MongoIndexingPolicyComponent", () => {
expect(mongoIndexingPolicyComponent.isMongoIndexingPolicyDiscardable()).toEqual( expect(mongoIndexingPolicyComponent.isMongoIndexingPolicyDiscardable()).toEqual(
isMongoIndexingPolicyDiscardable isMongoIndexingPolicyDiscardable
); );
expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toEqual( if (mongoWarningNotificationMessage) {
mongoWarningNotificationMessage const elementAsString = renderToString(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage());
); expect(elementAsString).toContain(mongoWarningNotificationMessage);
} else {
expect(mongoIndexingPolicyComponent.getMongoWarningNotificationMessage()).toBeUndefined();
}
} }
); );
}); });

View File

@@ -25,8 +25,8 @@ import {
createAndAddMongoIndexStackProps, createAndAddMongoIndexStackProps,
separatorStyles, separatorStyles,
mongoIndexingPolicyAADError, mongoIndexingPolicyAADError,
mongoIndexTransformationRefreshingMessage, indexingPolicynUnsavedWarningMessage,
renderMongoIndexTransformationRefreshMessage infoAndToolTipTextStyle
} from "../../SettingsRenderUtils"; } from "../../SettingsRenderUtils";
import { MongoIndex } from "../../../../../Utils/arm/generatedClients/2020-04-01/types"; import { MongoIndex } from "../../../../../Utils/arm/generatedClients/2020-04-01/types";
import { import {
@@ -35,12 +35,13 @@ import {
MongoIndexIdField, MongoIndexIdField,
MongoNotificationType, MongoNotificationType,
getMongoIndexType, getMongoIndexType,
getMongoIndexTypeText getMongoIndexTypeText,
isIndexTransforming
} from "../../SettingsUtils"; } from "../../SettingsUtils";
import { AddMongoIndexComponent } from "./AddMongoIndexComponent"; import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent"; import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
import { AuthType } from "../../../../../AuthType"; import { AuthType } from "../../../../../AuthType";
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
export interface MongoIndexingPolicyComponentProps { export interface MongoIndexingPolicyComponentProps {
mongoIndexes: MongoIndex[]; mongoIndexes: MongoIndex[];
@@ -56,20 +57,13 @@ export interface MongoIndexingPolicyComponentProps {
onMongoIndexingPolicyDiscardableChange: (isMongoIndexingPolicyDiscardable: boolean) => void; onMongoIndexingPolicyDiscardableChange: (isMongoIndexingPolicyDiscardable: boolean) => void;
} }
interface MongoIndexingPolicyComponentState {
isRefreshingIndexTransformationProgress: boolean;
}
interface MongoIndexDisplayProps { interface MongoIndexDisplayProps {
definition: JSX.Element; definition: JSX.Element;
type: JSX.Element; type: JSX.Element;
actionButton: JSX.Element; actionButton: JSX.Element;
} }
export class MongoIndexingPolicyComponent extends React.Component< export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingPolicyComponentProps> {
MongoIndexingPolicyComponentProps,
MongoIndexingPolicyComponentState
> {
private shouldCheckComponentIsDirty = true; private shouldCheckComponentIsDirty = true;
private addMongoIndexComponentRefs: React.RefObject<AddMongoIndexComponent>[] = []; private addMongoIndexComponentRefs: React.RefObject<AddMongoIndexComponent>[] = [];
private initialIndexesColumns: IColumn[] = [ private initialIndexesColumns: IColumn[] = [
@@ -98,13 +92,6 @@ export class MongoIndexingPolicyComponent extends React.Component<
} }
]; ];
constructor(props: MongoIndexingPolicyComponentProps) {
super(props);
this.state = {
isRefreshingIndexTransformationProgress: false
};
}
componentDidUpdate(prevProps: MongoIndexingPolicyComponentProps): void { componentDidUpdate(prevProps: MongoIndexingPolicyComponentProps): void {
if (this.props.indexesToAdd.length > prevProps.indexesToAdd.length) { if (this.props.indexesToAdd.length > prevProps.indexesToAdd.length) {
this.addMongoIndexComponentRefs[prevProps.indexesToAdd.length]?.current?.focus(); this.addMongoIndexComponentRefs[prevProps.indexesToAdd.length]?.current?.focus();
@@ -144,10 +131,15 @@ export class MongoIndexingPolicyComponent extends React.Component<
return this.props.indexesToAdd.length > 0 || this.props.indexesToDrop.length > 0; return this.props.indexesToAdd.length > 0 || this.props.indexesToDrop.length > 0;
}; };
public getMongoWarningNotificationMessage = (): string => { public getMongoWarningNotificationMessage = (): JSX.Element => {
return this.props.indexesToAdd.find( const warningMessage = this.props.indexesToAdd.find(
addMongoIndexProps => addMongoIndexProps.notification?.type === MongoNotificationType.Warning addMongoIndexProps => addMongoIndexProps.notification?.type === MongoNotificationType.Warning
)?.notification.message; )?.notification.message;
if (warningMessage) {
return <Text styles={infoAndToolTipTextStyle}>{warningMessage}</Text>;
}
return undefined;
}; };
private onRenderRow = (props: IDetailsRowProps): JSX.Element => { private onRenderRow = (props: IDetailsRowProps): JSX.Element => {
@@ -159,7 +151,7 @@ export class MongoIndexingPolicyComponent extends React.Component<
<IconButton <IconButton
ariaLabel="Delete index Button" ariaLabel="Delete index Button"
iconProps={{ iconName: "Delete" }} iconProps={{ iconName: "Delete" }}
disabled={this.isIndexingTransforming()} disabled={isIndexTransforming(this.props.indexTransformationProgress)}
onClick={() => { onClick={() => {
this.props.onIndexDrop(arrayPosition); this.props.onIndexDrop(arrayPosition);
}} }}
@@ -230,7 +222,7 @@ export class MongoIndexingPolicyComponent extends React.Component<
<AddMongoIndexComponent <AddMongoIndexComponent
ref={this.addMongoIndexComponentRefs[indexesToAddLength]} ref={this.addMongoIndexComponentRefs[indexesToAddLength]}
disabled={this.isIndexingTransforming()} disabled={isIndexTransforming(this.props.indexTransformationProgress)}
position={indexesToAddLength} position={indexesToAddLength}
key={indexesToAddLength} key={indexesToAddLength}
description={undefined} description={undefined}
@@ -298,55 +290,21 @@ export class MongoIndexingPolicyComponent extends React.Component<
); );
}; };
private refreshIndexTransformationProgress = async () => {
this.setState({ isRefreshingIndexTransformationProgress: true });
try {
await this.props.refreshIndexTransformationProgress();
} catch (error) {
handleError(error, "Refreshing index transformation progress failed.", "RefreshIndexTransformationProgress");
} finally {
this.setState({ isRefreshingIndexTransformationProgress: false });
}
};
public isIndexingTransforming = (): boolean =>
// index transformation progress can be 0
this.props.indexTransformationProgress !== undefined && this.props.indexTransformationProgress !== 100;
private onClickRefreshIndexingTransformationLink = async () => await this.refreshIndexTransformationProgress();
private renderIndexTransformationWarning = (): JSX.Element => {
if (this.state.isRefreshingIndexTransformationProgress) {
return mongoIndexTransformationRefreshingMessage;
} else if (this.isIndexingTransforming()) {
return renderMongoIndexTransformationRefreshMessage(
this.props.indexTransformationProgress,
this.onClickRefreshIndexingTransformationLink
);
}
return undefined;
};
private renderWarningMessage = (): JSX.Element => { private renderWarningMessage = (): JSX.Element => {
let warningMessage: string; let warningMessage: JSX.Element;
if (this.getMongoWarningNotificationMessage()) { if (this.getMongoWarningNotificationMessage()) {
warningMessage = this.getMongoWarningNotificationMessage(); warningMessage = this.getMongoWarningNotificationMessage();
} else if (this.isMongoIndexingPolicySaveable()) { } else if (this.isMongoIndexingPolicySaveable()) {
warningMessage = warningMessage = indexingPolicynUnsavedWarningMessage;
"You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.";
} }
return ( return (
<> <>
{this.renderIndexTransformationWarning() && ( <IndexingPolicyRefreshComponent
<MessageBar messageBarType={MessageBarType.warning}>{this.renderIndexTransformationWarning()}</MessageBar> indexTransformationProgress={this.props.indexTransformationProgress}
)} refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
/>
{warningMessage && ( {warningMessage && <MessageBar messageBarType={MessageBarType.warning}>{warningMessage}</MessageBar>}
<MessageBar messageBarType={MessageBarType.warning}>
<Text>{warningMessage}</Text>
</MessageBar>
)}
</> </>
); );
}; };

View File

@@ -8,6 +8,9 @@ exports[`MongoIndexingPolicyComponent renders 1`] = `
} }
} }
> >
<IndexingPolicyRefreshComponent
refreshIndexTransformationProgress={[Function]}
/>
<Text> <Text>
For queries that filter on multiple properties, create multiple single field indexes instead of a compound index. For queries that filter on multiple properties, create multiple single field indexes instead of a compound index.
<StyledLinkBase <StyledLinkBase

View File

@@ -20,7 +20,6 @@ describe("ScaleComponent", () => {
collection: collection, collection: collection,
container: container, container: container,
isFixedContainer: false, isFixedContainer: false,
autoPilotTiersList: [],
onThroughputChange: () => { onThroughputChange: () => {
return; return;
}, },

View File

@@ -24,7 +24,6 @@ export interface ScaleComponentProps {
collection: ViewModels.Collection; collection: ViewModels.Collection;
container: Explorer; container: Explorer;
isFixedContainer: boolean; isFixedContainer: boolean;
autoPilotTiersList: ViewModels.DropdownOption<DataModels.AutopilotTier>[];
onThroughputChange: (newThroughput: number) => void; onThroughputChange: (newThroughput: number) => void;
throughput: number; throughput: number;
throughputBaseline: number; throughputBaseline: number;
@@ -86,7 +85,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
public getThroughputTitle = (): string => { public getThroughputTitle = (): string => {
if (this.props.isAutoPilotSelected) { if (this.props.isAutoPilotSelected) {
return AutoPilotUtils.getAutoPilotHeaderText(false); return AutoPilotUtils.getAutoPilotHeaderText();
} }
const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString(); const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString();

View File

@@ -8,6 +8,9 @@ exports[`IndexingPolicyComponent renders 1`] = `
} }
} }
> >
<IndexingPolicyRefreshComponent
refreshIndexTransformationProgress={[Function]}
/>
<div <div
className="settingsV2IndexingPolicyEditor" className="settingsV2IndexingPolicyEditor"
tabIndex={0} tabIndex={0}

View File

@@ -15,7 +15,8 @@ import {
MongoWildcardPlaceHolder, MongoWildcardPlaceHolder,
getMongoIndexTypeText, getMongoIndexTypeText,
SingleFieldText, SingleFieldText,
WildcardText WildcardText,
isIndexTransforming
} from "./SettingsUtils"; } from "./SettingsUtils";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
@@ -139,3 +140,10 @@ describe("SettingsUtils", () => {
expect(notification.type).toEqual(MongoNotificationType.Error); expect(notification.type).toEqual(MongoNotificationType.Error);
}); });
}); });
it("isIndexingTransforming", () => {
expect(isIndexTransforming(undefined)).toBeFalsy();
expect(isIndexTransforming(0)).toBeTruthy();
expect(isIndexTransforming(90)).toBeTruthy();
expect(isIndexTransforming(100)).toBeFalsy();
});

View File

@@ -250,3 +250,7 @@ export const getMongoIndexTypeText = (index: MongoIndexTypes): string => {
} }
return WildcardText; return WildcardText;
}; };
export const isIndexTransforming = (indexTransformationProgress: number): boolean =>
// index transformation progress can be 0
indexTransformationProgress !== undefined && indexTransformationProgress !== 100;

View File

@@ -40,7 +40,6 @@ exports[`SettingsComponent renders 1`] = `
"_openShareDialog": [Function], "_openShareDialog": [Function],
"_panes": Array [ "_panes": Array [
AddDatabasePane { AddDatabasePane {
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -56,7 +55,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "adddatabasepane", "id": "adddatabasepane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -69,7 +67,6 @@ exports[`SettingsComponent renders 1`] = `
"onMoreDetailsKeyPress": [Function], "onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"selectedAutoPilotTier": [Function],
"showUpsellMessage": [Function], "showUpsellMessage": [Function],
"throughput": [Function], "throughput": [Function],
"throughputRangeText": [Function], "throughputRangeText": [Function],
@@ -86,7 +83,6 @@ exports[`SettingsComponent renders 1`] = `
AddCollectionPane { AddCollectionPane {
"_isSynapseLinkEnabled": [Function], "_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function], "autoPilotThroughput": [Function],
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -108,7 +104,6 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"formWarnings": [Function], "formWarnings": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "addcollectionpane", "id": "addcollectionpane",
"isAnalyticalStorageOn": [Function], "isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
@@ -140,10 +135,7 @@ exports[`SettingsComponent renders 1`] = `
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function], "rupm": [Function],
"rupmVisible": [Function], "rupmVisible": [Function],
"selectedAutoPilotTier": [Function],
"selectedSharedAutoPilotTier": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedAutoPilotTiersList": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function], "shouldUseDatabaseThroughput": [Function],
@@ -354,7 +346,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
CassandraAddCollectionPane { CassandraAddCollectionPane {
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -366,7 +357,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "cassandraaddcollectionpane", "id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -387,10 +377,7 @@ exports[`SettingsComponent renders 1`] = `
"requestUnitsUsageCostShared": [Function], "requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function], "selectedAutoPilotThroughput": [Function],
"selectedAutoPilotTier": [Function],
"selectedSharedAutoPilotTier": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedAutoPilotTiersList": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function], "sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function], "sharedThroughputSpendAckText": [Function],
@@ -585,7 +572,6 @@ exports[`SettingsComponent renders 1`] = `
"addCollectionPane": AddCollectionPane { "addCollectionPane": AddCollectionPane {
"_isSynapseLinkEnabled": [Function], "_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function], "autoPilotThroughput": [Function],
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -607,7 +593,6 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"formWarnings": [Function], "formWarnings": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "addcollectionpane", "id": "addcollectionpane",
"isAnalyticalStorageOn": [Function], "isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
@@ -639,10 +624,7 @@ exports[`SettingsComponent renders 1`] = `
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function], "rupm": [Function],
"rupmVisible": [Function], "rupmVisible": [Function],
"selectedAutoPilotTier": [Function],
"selectedSharedAutoPilotTier": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedAutoPilotTiersList": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function], "shouldUseDatabaseThroughput": [Function],
@@ -672,7 +654,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"addCollectionText": [Function], "addCollectionText": [Function],
"addDatabasePane": AddDatabasePane { "addDatabasePane": AddDatabasePane {
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -688,7 +669,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "adddatabasepane", "id": "adddatabasepane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -701,7 +681,6 @@ exports[`SettingsComponent renders 1`] = `
"onMoreDetailsKeyPress": [Function], "onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"selectedAutoPilotTier": [Function],
"showUpsellMessage": [Function], "showUpsellMessage": [Function],
"throughput": [Function], "throughput": [Function],
"throughputRangeText": [Function], "throughputRangeText": [Function],
@@ -778,7 +757,6 @@ exports[`SettingsComponent renders 1`] = `
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane { "cassandraAddCollectionPane": CassandraAddCollectionPane {
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -790,7 +768,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "cassandraaddcollectionpane", "id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -811,10 +788,7 @@ exports[`SettingsComponent renders 1`] = `
"requestUnitsUsageCostShared": [Function], "requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function], "selectedAutoPilotThroughput": [Function],
"selectedAutoPilotTier": [Function],
"selectedSharedAutoPilotTier": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedAutoPilotTiersList": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function], "sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function], "sharedThroughputSpendAckText": [Function],
@@ -969,7 +943,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"hasAutoPilotV2FeatureFlag": [Function],
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function], "hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
@@ -1347,7 +1320,6 @@ exports[`SettingsComponent renders 1`] = `
"_openShareDialog": [Function], "_openShareDialog": [Function],
"_panes": Array [ "_panes": Array [
AddDatabasePane { AddDatabasePane {
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -1363,7 +1335,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "adddatabasepane", "id": "adddatabasepane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -1376,7 +1347,6 @@ exports[`SettingsComponent renders 1`] = `
"onMoreDetailsKeyPress": [Function], "onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"selectedAutoPilotTier": [Function],
"showUpsellMessage": [Function], "showUpsellMessage": [Function],
"throughput": [Function], "throughput": [Function],
"throughputRangeText": [Function], "throughputRangeText": [Function],
@@ -1393,7 +1363,6 @@ exports[`SettingsComponent renders 1`] = `
AddCollectionPane { AddCollectionPane {
"_isSynapseLinkEnabled": [Function], "_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function], "autoPilotThroughput": [Function],
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -1415,7 +1384,6 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"formWarnings": [Function], "formWarnings": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "addcollectionpane", "id": "addcollectionpane",
"isAnalyticalStorageOn": [Function], "isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
@@ -1447,10 +1415,7 @@ exports[`SettingsComponent renders 1`] = `
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function], "rupm": [Function],
"rupmVisible": [Function], "rupmVisible": [Function],
"selectedAutoPilotTier": [Function],
"selectedSharedAutoPilotTier": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedAutoPilotTiersList": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function], "shouldUseDatabaseThroughput": [Function],
@@ -1661,7 +1626,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
CassandraAddCollectionPane { CassandraAddCollectionPane {
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -1673,7 +1637,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "cassandraaddcollectionpane", "id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -1694,10 +1657,7 @@ exports[`SettingsComponent renders 1`] = `
"requestUnitsUsageCostShared": [Function], "requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function], "selectedAutoPilotThroughput": [Function],
"selectedAutoPilotTier": [Function],
"selectedSharedAutoPilotTier": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedAutoPilotTiersList": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function], "sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function], "sharedThroughputSpendAckText": [Function],
@@ -1892,7 +1852,6 @@ exports[`SettingsComponent renders 1`] = `
"addCollectionPane": AddCollectionPane { "addCollectionPane": AddCollectionPane {
"_isSynapseLinkEnabled": [Function], "_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function], "autoPilotThroughput": [Function],
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -1914,7 +1873,6 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"formWarnings": [Function], "formWarnings": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "addcollectionpane", "id": "addcollectionpane",
"isAnalyticalStorageOn": [Function], "isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
@@ -1946,10 +1904,7 @@ exports[`SettingsComponent renders 1`] = `
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function], "rupm": [Function],
"rupmVisible": [Function], "rupmVisible": [Function],
"selectedAutoPilotTier": [Function],
"selectedSharedAutoPilotTier": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedAutoPilotTiersList": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function], "shouldUseDatabaseThroughput": [Function],
@@ -1979,7 +1934,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"addCollectionText": [Function], "addCollectionText": [Function],
"addDatabasePane": AddDatabasePane { "addDatabasePane": AddDatabasePane {
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -1995,7 +1949,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "adddatabasepane", "id": "adddatabasepane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -2008,7 +1961,6 @@ exports[`SettingsComponent renders 1`] = `
"onMoreDetailsKeyPress": [Function], "onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"selectedAutoPilotTier": [Function],
"showUpsellMessage": [Function], "showUpsellMessage": [Function],
"throughput": [Function], "throughput": [Function],
"throughputRangeText": [Function], "throughputRangeText": [Function],
@@ -2085,7 +2037,6 @@ exports[`SettingsComponent renders 1`] = `
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane { "cassandraAddCollectionPane": CassandraAddCollectionPane {
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -2097,7 +2048,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "cassandraaddcollectionpane", "id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -2118,10 +2068,7 @@ exports[`SettingsComponent renders 1`] = `
"requestUnitsUsageCostShared": [Function], "requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function], "selectedAutoPilotThroughput": [Function],
"selectedAutoPilotTier": [Function],
"selectedSharedAutoPilotTier": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedAutoPilotTiersList": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function], "sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function], "sharedThroughputSpendAckText": [Function],
@@ -2276,7 +2223,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"hasAutoPilotV2FeatureFlag": [Function],
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function], "hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
@@ -2667,7 +2613,6 @@ exports[`SettingsComponent renders 1`] = `
"_openShareDialog": [Function], "_openShareDialog": [Function],
"_panes": Array [ "_panes": Array [
AddDatabasePane { AddDatabasePane {
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -2683,7 +2628,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "adddatabasepane", "id": "adddatabasepane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -2696,7 +2640,6 @@ exports[`SettingsComponent renders 1`] = `
"onMoreDetailsKeyPress": [Function], "onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"selectedAutoPilotTier": [Function],
"showUpsellMessage": [Function], "showUpsellMessage": [Function],
"throughput": [Function], "throughput": [Function],
"throughputRangeText": [Function], "throughputRangeText": [Function],
@@ -2713,7 +2656,6 @@ exports[`SettingsComponent renders 1`] = `
AddCollectionPane { AddCollectionPane {
"_isSynapseLinkEnabled": [Function], "_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function], "autoPilotThroughput": [Function],
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -2735,7 +2677,6 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"formWarnings": [Function], "formWarnings": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "addcollectionpane", "id": "addcollectionpane",
"isAnalyticalStorageOn": [Function], "isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
@@ -2767,10 +2708,7 @@ exports[`SettingsComponent renders 1`] = `
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function], "rupm": [Function],
"rupmVisible": [Function], "rupmVisible": [Function],
"selectedAutoPilotTier": [Function],
"selectedSharedAutoPilotTier": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedAutoPilotTiersList": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function], "shouldUseDatabaseThroughput": [Function],
@@ -2981,7 +2919,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
CassandraAddCollectionPane { CassandraAddCollectionPane {
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -2993,7 +2930,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "cassandraaddcollectionpane", "id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -3014,10 +2950,7 @@ exports[`SettingsComponent renders 1`] = `
"requestUnitsUsageCostShared": [Function], "requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function], "selectedAutoPilotThroughput": [Function],
"selectedAutoPilotTier": [Function],
"selectedSharedAutoPilotTier": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedAutoPilotTiersList": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function], "sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function], "sharedThroughputSpendAckText": [Function],
@@ -3212,7 +3145,6 @@ exports[`SettingsComponent renders 1`] = `
"addCollectionPane": AddCollectionPane { "addCollectionPane": AddCollectionPane {
"_isSynapseLinkEnabled": [Function], "_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function], "autoPilotThroughput": [Function],
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -3234,7 +3166,6 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"formWarnings": [Function], "formWarnings": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "addcollectionpane", "id": "addcollectionpane",
"isAnalyticalStorageOn": [Function], "isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
@@ -3266,10 +3197,7 @@ exports[`SettingsComponent renders 1`] = `
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function], "rupm": [Function],
"rupmVisible": [Function], "rupmVisible": [Function],
"selectedAutoPilotTier": [Function],
"selectedSharedAutoPilotTier": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedAutoPilotTiersList": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function], "shouldUseDatabaseThroughput": [Function],
@@ -3299,7 +3227,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"addCollectionText": [Function], "addCollectionText": [Function],
"addDatabasePane": AddDatabasePane { "addDatabasePane": AddDatabasePane {
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -3315,7 +3242,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "adddatabasepane", "id": "adddatabasepane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -3328,7 +3254,6 @@ exports[`SettingsComponent renders 1`] = `
"onMoreDetailsKeyPress": [Function], "onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"selectedAutoPilotTier": [Function],
"showUpsellMessage": [Function], "showUpsellMessage": [Function],
"throughput": [Function], "throughput": [Function],
"throughputRangeText": [Function], "throughputRangeText": [Function],
@@ -3405,7 +3330,6 @@ exports[`SettingsComponent renders 1`] = `
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane { "cassandraAddCollectionPane": CassandraAddCollectionPane {
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -3417,7 +3341,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "cassandraaddcollectionpane", "id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -3438,10 +3361,7 @@ exports[`SettingsComponent renders 1`] = `
"requestUnitsUsageCostShared": [Function], "requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function], "selectedAutoPilotThroughput": [Function],
"selectedAutoPilotTier": [Function],
"selectedSharedAutoPilotTier": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedAutoPilotTiersList": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function], "sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function], "sharedThroughputSpendAckText": [Function],
@@ -3596,7 +3516,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"hasAutoPilotV2FeatureFlag": [Function],
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function], "hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
@@ -3974,7 +3893,6 @@ exports[`SettingsComponent renders 1`] = `
"_openShareDialog": [Function], "_openShareDialog": [Function],
"_panes": Array [ "_panes": Array [
AddDatabasePane { AddDatabasePane {
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -3990,7 +3908,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "adddatabasepane", "id": "adddatabasepane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -4003,7 +3920,6 @@ exports[`SettingsComponent renders 1`] = `
"onMoreDetailsKeyPress": [Function], "onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"selectedAutoPilotTier": [Function],
"showUpsellMessage": [Function], "showUpsellMessage": [Function],
"throughput": [Function], "throughput": [Function],
"throughputRangeText": [Function], "throughputRangeText": [Function],
@@ -4020,7 +3936,6 @@ exports[`SettingsComponent renders 1`] = `
AddCollectionPane { AddCollectionPane {
"_isSynapseLinkEnabled": [Function], "_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function], "autoPilotThroughput": [Function],
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -4042,7 +3957,6 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"formWarnings": [Function], "formWarnings": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "addcollectionpane", "id": "addcollectionpane",
"isAnalyticalStorageOn": [Function], "isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
@@ -4074,10 +3988,7 @@ exports[`SettingsComponent renders 1`] = `
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function], "rupm": [Function],
"rupmVisible": [Function], "rupmVisible": [Function],
"selectedAutoPilotTier": [Function],
"selectedSharedAutoPilotTier": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedAutoPilotTiersList": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function], "shouldUseDatabaseThroughput": [Function],
@@ -4288,7 +4199,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
CassandraAddCollectionPane { CassandraAddCollectionPane {
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -4300,7 +4210,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "cassandraaddcollectionpane", "id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -4321,10 +4230,7 @@ exports[`SettingsComponent renders 1`] = `
"requestUnitsUsageCostShared": [Function], "requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function], "selectedAutoPilotThroughput": [Function],
"selectedAutoPilotTier": [Function],
"selectedSharedAutoPilotTier": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedAutoPilotTiersList": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function], "sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function], "sharedThroughputSpendAckText": [Function],
@@ -4519,7 +4425,6 @@ exports[`SettingsComponent renders 1`] = `
"addCollectionPane": AddCollectionPane { "addCollectionPane": AddCollectionPane {
"_isSynapseLinkEnabled": [Function], "_isSynapseLinkEnabled": [Function],
"autoPilotThroughput": [Function], "autoPilotThroughput": [Function],
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -4541,7 +4446,6 @@ exports[`SettingsComponent renders 1`] = `
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"formWarnings": [Function], "formWarnings": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "addcollectionpane", "id": "addcollectionpane",
"isAnalyticalStorageOn": [Function], "isAnalyticalStorageOn": [Function],
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
@@ -4573,10 +4477,7 @@ exports[`SettingsComponent renders 1`] = `
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function], "rupm": [Function],
"rupmVisible": [Function], "rupmVisible": [Function],
"selectedAutoPilotTier": [Function],
"selectedSharedAutoPilotTier": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedAutoPilotTiersList": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
"shouldUseDatabaseThroughput": [Function], "shouldUseDatabaseThroughput": [Function],
@@ -4606,7 +4507,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"addCollectionText": [Function], "addCollectionText": [Function],
"addDatabasePane": AddDatabasePane { "addDatabasePane": AddDatabasePane {
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -4622,7 +4522,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "adddatabasepane", "id": "adddatabasepane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -4635,7 +4534,6 @@ exports[`SettingsComponent renders 1`] = `
"onMoreDetailsKeyPress": [Function], "onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"selectedAutoPilotTier": [Function],
"showUpsellMessage": [Function], "showUpsellMessage": [Function],
"throughput": [Function], "throughput": [Function],
"throughputRangeText": [Function], "throughputRangeText": [Function],
@@ -4712,7 +4610,6 @@ exports[`SettingsComponent renders 1`] = `
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
"canSaveQueries": [Function], "canSaveQueries": [Function],
"cassandraAddCollectionPane": CassandraAddCollectionPane { "cassandraAddCollectionPane": CassandraAddCollectionPane {
"autoPilotTiersList": [Function],
"autoPilotUsageCost": [Function], "autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function], "canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function], "canExceedMaximumValue": [Function],
@@ -4724,7 +4621,6 @@ exports[`SettingsComponent renders 1`] = `
"firstFieldHasFocus": [Function], "firstFieldHasFocus": [Function],
"formErrors": [Function], "formErrors": [Function],
"formErrorsDetails": [Function], "formErrorsDetails": [Function],
"hasAutoPilotV2FeatureFlag": [Function],
"id": "cassandraaddcollectionpane", "id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function], "isAutoPilotSelected": [Function],
"isExecuting": [Function], "isExecuting": [Function],
@@ -4745,10 +4641,7 @@ exports[`SettingsComponent renders 1`] = `
"requestUnitsUsageCostShared": [Function], "requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function], "selectedAutoPilotThroughput": [Function],
"selectedAutoPilotTier": [Function],
"selectedSharedAutoPilotTier": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedAutoPilotTiersList": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function], "sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function], "sharedThroughputSpendAckText": [Function],
@@ -4903,7 +4796,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function], "title": [Function],
"visible": [Function], "visible": [Function],
}, },
"hasAutoPilotV2FeatureFlag": [Function],
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"hasWriteAccess": [Function], "hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
@@ -5297,6 +5189,7 @@ exports[`SettingsComponent renders 1`] = `
logIndexingPolicySuccessMessage={[Function]} logIndexingPolicySuccessMessage={[Function]}
onIndexingPolicyContentChange={[Function]} onIndexingPolicyContentChange={[Function]}
onIndexingPolicyDirtyChange={[Function]} onIndexingPolicyDirtyChange={[Function]}
refreshIndexTransformationProgress={[Function]}
resetShouldDiscardIndexingPolicy={[Function]} resetShouldDiscardIndexingPolicy={[Function]}
shouldDiscardIndexingPolicy={false} shouldDiscardIndexingPolicy={false}
/> />

View File

@@ -166,15 +166,7 @@ exports[`SettingsUtils functions render 1`] = `
} }
} }
> >
Changing the Indexing Policy impacts query results while the index transformation occurs. When a change is made and the indexing mode is set to consistent or lazy, queries return eventual results until the operation completes. For more information see, You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
<StyledLinkBase
href="https://aka.ms/cosmosdb/modify-index-policy"
target="_blank"
>
Modifying Indexing Policies
</StyledLinkBase>
.
</Text> </Text>
<Text <Text
id="updateThroughputBeyondLimitWarningMessage" id="updateThroughputBeyondLimitWarningMessage"
@@ -341,14 +333,30 @@ exports[`SettingsUtils functions render 1`] = `
} }
} }
> >
<Text> <Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
Refreshing index transformation progress Refreshing index transformation progress
</Text> </Text>
<StyledSpinnerBase <StyledSpinnerBase
size={2} size={1}
/> />
</Stack> </Stack>
<Text> <Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
You can make more indexing changes once the current index transformation is complete. You can make more indexing changes once the current index transformation is complete.
<StyledLinkBase <StyledLinkBase
onClick={[Function]} onClick={[Function]}
@@ -356,7 +364,15 @@ exports[`SettingsUtils functions render 1`] = `
Refresh to check if it has completed. Refresh to check if it has completed.
</StyledLinkBase> </StyledLinkBase>
</Text> </Text>
<Text> <Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
You can make more indexing changes once the current index transformation has completed. It is 90% complete. You can make more indexing changes once the current index transformation has completed. It is 90% complete.
<StyledLinkBase <StyledLinkBase
onClick={[Function]} onClick={[Function]}

View File

@@ -1,222 +0,0 @@
import * as ko from "knockout";
import * as ViewModels from "../../../Contracts/ViewModels";
import editable from "../../../Common/EditableUtility";
import { ThroughputInputComponent, ThroughputInputParams, ThroughputInputViewModel } from "./ThroughputInputComponent";
const $ = (selector: string) => document.querySelector(selector) as HTMLElement;
describe.skip("Throughput Input Component", () => {
let component: any;
let vm: ThroughputInputViewModel;
const testId: string = "ThroughputValue";
const value: ViewModels.Editable<number> = editable.observable(500);
const minimum: ko.Observable<number> = ko.observable(400);
const maximum: ko.Observable<number> = ko.observable(2000);
function buildListOptions(
value: ViewModels.Editable<number>,
minimum: ko.Observable<number>,
maxium: ko.Observable<number>,
canExceedMaximumValue?: boolean
): ThroughputInputParams {
return {
testId,
value,
minimum,
maximum,
canExceedMaximumValue: ko.computed<boolean>(() => Boolean(canExceedMaximumValue)),
costsVisible: ko.observable(false),
isFixed: false,
label: ko.observable("Label"),
requestUnitsUsageCost: ko.observable("requestUnitsUsageCost"),
showAsMandatory: false,
autoPilotTiersList: null,
autoPilotUsageCost: null,
isAutoPilotSelected: null,
selectedAutoPilotTier: null,
throughputAutoPilotRadioId: null,
throughputProvisionedRadioId: null,
throughputModeRadioName: null
};
}
function simulateKeyPressSpace(target: HTMLElement): Promise<boolean> {
const event = new KeyboardEvent("keydown", {
key: "space"
});
const result = target.dispatchEvent(event);
return new Promise(resolve => {
setTimeout(() => {
resolve(result);
}, 1000);
});
}
beforeEach(() => {
component = ThroughputInputComponent;
document.body.innerHTML = component.template as any;
});
afterEach(async () => {
await ko.cleanNode(document);
});
describe("Rendering", () => {
it("should display value text", async () => {
vm = new component.viewModel(buildListOptions(value, minimum, maximum));
await ko.applyBindings(vm);
expect(($("input") as HTMLInputElement).value).toContain(value().toString());
});
});
describe("Behavior", () => {
it("should decrease value", async () => {
vm = new component.viewModel(buildListOptions(value, minimum, maximum));
await ko.applyBindings(vm);
value(450);
$(".testhook-decreaseThroughput").click();
expect(value()).toBe(400);
$(".testhook-decreaseThroughput").click();
expect(value()).toBe(400);
});
it("should increase value", async () => {
vm = new component.viewModel(buildListOptions(value, minimum, maximum));
await ko.applyBindings(vm);
value(1950);
$(".test-increaseThroughput").click();
expect(value()).toBe(2000);
$(".test-increaseThroughput").click();
expect(value()).toBe(2000);
});
it("should respect lower bound limits", async () => {
vm = new component.viewModel(buildListOptions(value, minimum, maximum));
await ko.applyBindings(vm);
value(minimum());
$(".testhook-decreaseThroughput").click();
expect(value()).toBe(minimum());
});
it("should respect upper bound limits", async () => {
vm = new component.viewModel(buildListOptions(value, minimum, maximum));
await ko.applyBindings(vm);
value(maximum());
$(".test-increaseThroughput").click();
expect(value()).toBe(maximum());
});
it("should allow throughput to exceed upper bound limit when canExceedMaximumValue is set", async () => {
vm = new component.viewModel(buildListOptions(value, minimum, maximum, true));
await ko.applyBindings(vm);
value(maximum());
$(".test-increaseThroughput").click();
expect(value()).toBe(maximum() + 100);
});
});
describe("Accessibility", () => {
it.skip("should decrease value with keypress", async () => {
vm = new component.viewModel(buildListOptions(value, minimum, maximum));
await ko.applyBindings(vm);
const target = $(".testhook-decreaseThroughput");
value(500);
expect(value()).toBe(500);
const result = await simulateKeyPressSpace(target);
expect(value()).toBe(400);
});
it.skip("should increase value with keypress", async () => {
vm = new component.viewModel(buildListOptions(value, minimum, maximum));
await ko.applyBindings(vm);
const target = $(".test-increaseThroughput");
value(400);
expect(value()).toBe(400);
const result = await simulateKeyPressSpace(target);
// expect(value()).toBe(500);
});
it("should set the decreaseButtonAriaLabel using the default step value", async () => {
vm = new component.viewModel(buildListOptions(value, minimum, maximum, true));
await ko.applyBindings(vm);
expect(vm.decreaseButtonAriaLabel).toBe("Decrease throughput by 100");
});
it("should set the increaseButtonAriaLabel using the default step value", async () => {
vm = new component.viewModel(buildListOptions(value, minimum, maximum, true));
await ko.applyBindings(vm);
expect(vm.increaseButtonAriaLabel).toBe("Increase throughput by 100");
});
it("should set the increaseButtonAriaLabel using the params step value", async () => {
const options = buildListOptions(value, minimum, maximum, true);
options.step = 10;
vm = new component.viewModel(options);
await ko.applyBindings(vm);
expect(vm.increaseButtonAriaLabel).toBe("Increase throughput by 10");
});
it("should set the decreaseButtonAriaLabel using the params step value", async () => {
const options = buildListOptions(value, minimum, maximum, true);
options.step = 10;
vm = new component.viewModel(options);
await ko.applyBindings(vm);
expect(vm.decreaseButtonAriaLabel).toBe("Decrease throughput by 10");
});
it("should set the decreaseButtonAriaLabel using the params step value", async () => {
const options = buildListOptions(value, minimum, maximum, true);
options.step = 10;
vm = new component.viewModel(options);
await ko.applyBindings(vm);
});
it("should have aria-label attribute on increase button", async () => {
vm = new component.viewModel(buildListOptions(value, minimum, maximum, true));
await ko.applyBindings(vm);
const ariaLabel = $(".test-increaseThroughput").attributes.getNamedItem("aria-label").value;
expect(ariaLabel).toBe("Increase throughput by 100");
});
it("should have aria-label attribute on increase button", async () => {
vm = new component.viewModel(buildListOptions(value, minimum, maximum, true));
await ko.applyBindings(vm);
const ariaLabel = $(".testhook-decreaseThroughput").attributes.getNamedItem("aria-label").value;
expect(ariaLabel).toBe("Decrease throughput by 100");
});
it("should have role on increase button", async () => {
vm = new component.viewModel(buildListOptions(value, minimum, maximum, true));
await ko.applyBindings(vm);
const role = $(".test-increaseThroughput").attributes.getNamedItem("role").value;
expect(role).toBe("button");
});
it("should have role on decrease button", async () => {
vm = new component.viewModel(buildListOptions(value, minimum, maximum, true));
await ko.applyBindings(vm);
const role = $(".testhook-decreaseThroughput").attributes.getNamedItem("role").value;
expect(role).toBe("button");
});
it("should have tabindex 0 on increase button", async () => {
vm = new component.viewModel(buildListOptions(value, minimum, maximum, true));
await ko.applyBindings(vm);
const role = $(".testhook-decreaseThroughput").attributes.getNamedItem("tabindex").value;
expect(role).toBe("0");
});
it("should have tabindex 0 on decrease button", async () => {
vm = new component.viewModel(buildListOptions(value, minimum, maximum, true));
await ko.applyBindings(vm);
const role = $(".testhook-decreaseThroughput").attributes.getNamedItem("tabindex").value;
expect(role).toBe("0");
});
});
});

View File

@@ -1,145 +0,0 @@
<div>
<div>
<p class="pkPadding">
<!-- ko if: showAsMandatory -->
<span class="mandatoryStar">*</span>
<!-- /ko -->
<span class="addCollectionLabel" data-bind="text: label"></span>
<!-- ko if: infoBubbleText -->
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="../../../../images/info-bubble.svg" alt="More information" />
<span data-bind="text: infoBubbleText" class="tooltiptext throughputRuInfo"></span>
</span>
<!-- /ko -->
</p>
</div>
<!-- ko if: !isFixed -->
<div data-bind="visible: showAutoPilot" class="throughputModeContainer">
<input
class="throughputModeRadio"
aria-label="Autopilot mode"
data-test="throughput-autoPilot"
type="radio"
role="radio"
tabindex="0"
data-bind="
checked: isAutoPilotSelected,
checkedValue: true,
attr: {
id: throughputAutoPilotRadioId,
name: throughputModeRadioName,
'aria-checked': isAutoPilotSelected() ? 'true' : 'false'
}"
/>
<span
class="throughputModeSpace"
data-bind="
attr: {
for: throughputAutoPilotRadioId
}"
>Autopilot (preview)
</span>
<input
class="throughputModeRadio nonFirstRadio"
aria-label="Manual mode"
type="radio"
role="radio"
tabindex="0"
data-bind="
checked: isAutoPilotSelected,
checkedValue: false,
attr: {
id: throughputProvisionedRadioId,
name: throughputModeRadioName,
'aria-checked': !isAutoPilotSelected() ? 'true' : 'false'
}"
/>
<span
class="throughputModeSpace"
data-bind="
attr: {
for: throughputProvisionedRadioId
}"
>Manual
</span>
</div>
<!-- /ko -->
<div data-bind="visible: isAutoPilotSelected">
<select
name="autoPilotTiers"
class="collid select-font-size"
aria-label="Autopilot Max RU/s"
data-bind="
options: autoPilotTiersList,
optionsText: 'text',
optionsValue: 'value',
value: selectedAutoPilotTier,
optionsCaption: 'Choose Max RU/s'"
>
</select>
<p>
<span
data-bind="
html: autoPilotUsageCost,
visible: selectedAutoPilotTier"
>
</span>
</p>
</div>
<div data-bind="visible: !isAutoPilotSelected()">
<div data-bind="setTemplateReady: true">
<p class="addContainerThroughputInput">
<input
type="number"
required
data-bind="
textInput: value,
css: {
dirty: value.editableIsDirty
},
enable: isEnabled,
attr:{
'data-test': testId,
'class': cssClass,
step: step,
min: minimum,
max: canExceedMaximumValue() ? null : maximum,
'aria-label': ariaLabel
}"
/>
</p>
</div>
<p data-bind="visible: costsVisible">
<span data-bind="html: requestUnitsUsageCost"></span>
</p>
<!-- ko if: spendAckVisible -->
<p class="pkPadding">
<input
type="checkbox"
aria-label="acknowledge spend throughput"
data-bind="
attr: {
title: spendAckText,
id: spendAckId
},
checked: spendAckChecked"
/>
<span data-bind="text: spendAckText, attr: { for: spendAckId }"></span>
</p>
<!-- /ko -->
<!-- ko if: isFixed -->
<p>
Choose unlimited storage capacity for more than 10,000 RU/s.
</p>
<!-- /ko -->
</div>
</div>

View File

@@ -1,261 +0,0 @@
import * as DataModels from "../../../Contracts/DataModels";
import * as ko from "knockout";
import * as ViewModels from "../../../Contracts/ViewModels";
import { KeyCodes } from "../../../Common/Constants";
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
import ThroughputInputComponentTemplate from "./ThroughputInputComponent.html";
/**
* Throughput Input:
*
* Creates a set of controls to input, sanitize and increase/decrease throughput
*
* How to use in your markup:
* <throughput-input params="{ value: anObservableToHoldTheValue, minimum: anObservableWithMinimum, maximum: anObservableWithMaximum }">
* </throughput-input>
*
*/
/**
* Parameters for this component
*/
export interface ThroughputInputParams {
/**
* Callback triggered when the template is bound to the component (for testing purposes)
*/
onTemplateReady?: () => void;
/**
* Observable to bind the Throughput value to
*/
value: ViewModels.Editable<number>;
/**
* Text to use as id for testing
*/
testId: string;
/**
* Text to use as aria-label
*/
ariaLabel?: ko.Observable<string>;
/**
* Minimum value in the range
*/
minimum: ko.Observable<number>;
/**
* Maximum value in the range
*/
maximum: ko.Observable<number>;
/**
* Step value for increase/decrease
*/
step?: number;
/**
* Observable to bind the Throughput enabled status
*/
isEnabled?: ko.Observable<boolean>;
/**
* Should show pricing controls
*/
costsVisible: ko.Observable<boolean>;
/**
* RU price
*/
requestUnitsUsageCost: ko.Subscribable<string>; // Our code assigns to ko.Computed, but unit test assigns to ko.Observable
/**
* State of the spending acknowledge checkbox
*/
spendAckChecked?: ko.Observable<boolean>;
/**
* id of the spending acknowledge checkbox
*/
spendAckId?: ko.Observable<string>;
/**
* spending acknowledge text
*/
spendAckText?: ko.Observable<string>;
/**
* Show spending acknowledge controls
*/
spendAckVisible?: ko.Observable<boolean>;
/**
* Display * to the left of the label
*/
showAsMandatory: boolean;
/**
* If true, it will display a text to prompt users to use unlimited collections to go beyond max for fixed
*/
isFixed: boolean;
/**
* Label of the provisioned throughut control
*/
label: ko.Observable<string>;
/**
* Text of the info bubble for provisioned throughut control
*/
infoBubbleText?: ko.Observable<string>;
/**
* Computed value that decides if value can exceed maximum allowable value
*/
canExceedMaximumValue?: ko.Computed<boolean>;
/**
* CSS classes to apply on input element
*/
cssClass?: string;
isAutoPilotSelected: ko.Observable<boolean>;
throughputAutoPilotRadioId: string;
throughputProvisionedRadioId: string;
throughputModeRadioName: string;
autoPilotTiersList: ko.ObservableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>;
selectedAutoPilotTier: ko.Observable<DataModels.AutopilotTier>;
autoPilotUsageCost: ko.Computed<string>;
showAutoPilot?: ko.Observable<boolean>;
}
export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
public ariaLabel: ko.Observable<string>;
public canExceedMaximumValue: ko.Computed<boolean>;
public step: number;
public testId: string;
public value: ViewModels.Editable<number>;
public minimum: ko.Observable<number>;
public maximum: ko.Observable<number>;
public isEnabled: ko.Observable<boolean>;
public cssClass: string;
public decreaseButtonAriaLabel: string;
public increaseButtonAriaLabel: string;
public costsVisible: ko.Observable<boolean>;
public requestUnitsUsageCost: ko.Subscribable<string>;
public spendAckChecked: ko.Observable<boolean>;
public spendAckId: ko.Observable<string>;
public spendAckText: ko.Observable<string>;
public spendAckVisible: ko.Observable<boolean>;
public showAsMandatory: boolean;
public infoBubbleText: string | ko.Observable<string>;
public label: ko.Observable<string>;
public isFixed: boolean;
public showAutoPilot: ko.Observable<boolean>;
public isAutoPilotSelected: ko.Observable<boolean>;
public throughputAutoPilotRadioId: string;
public throughputProvisionedRadioId: string;
public throughputModeRadioName: string;
public autoPilotTiersList: ko.ObservableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>;
public selectedAutoPilotTier: ko.Observable<DataModels.AutopilotTier>;
public autoPilotUsageCost: ko.Computed<string>;
public constructor(options: ThroughputInputParams) {
super();
super.onTemplateReady((isTemplateReady: boolean) => {
if (isTemplateReady && options.onTemplateReady) {
options.onTemplateReady();
}
});
const params: ThroughputInputParams = options;
this.testId = params.testId || "ThroughputValue";
this.ariaLabel = ko.observable((params.ariaLabel && params.ariaLabel()) || "");
this.canExceedMaximumValue = params.canExceedMaximumValue || ko.computed(() => false);
this.step = params.step || ThroughputInputViewModel._defaultStep;
this.isEnabled = params.isEnabled || ko.observable(true);
this.cssClass = params.cssClass || "textfontclr collid";
this.minimum = params.minimum;
this.maximum = params.maximum;
this.value = params.value;
this.decreaseButtonAriaLabel = "Decrease throughput by " + this.step.toString();
this.increaseButtonAriaLabel = "Increase throughput by " + this.step.toString();
this.costsVisible = options.costsVisible;
this.requestUnitsUsageCost = options.requestUnitsUsageCost;
this.spendAckChecked = options.spendAckChecked || ko.observable<boolean>(false);
this.spendAckId = options.spendAckId || ko.observable<string>();
this.spendAckText = options.spendAckText || ko.observable<string>();
this.spendAckVisible = options.spendAckVisible || ko.observable<boolean>(false);
this.showAsMandatory = !!options.showAsMandatory;
this.isFixed = !!options.isFixed;
this.infoBubbleText = options.infoBubbleText || ko.observable<string>();
this.label = options.label || ko.observable<string>();
this.showAutoPilot = options.showAutoPilot !== undefined ? options.showAutoPilot : ko.observable<boolean>(true);
this.isAutoPilotSelected = options.isAutoPilotSelected || ko.observable<boolean>(false);
this.throughputAutoPilotRadioId = options.throughputAutoPilotRadioId;
this.throughputProvisionedRadioId = options.throughputProvisionedRadioId;
this.throughputModeRadioName = options.throughputModeRadioName;
this.autoPilotTiersList = options.autoPilotTiersList;
this.selectedAutoPilotTier = options.selectedAutoPilotTier;
this.autoPilotUsageCost = options.autoPilotUsageCost;
}
public decreaseThroughput() {
let offerThroughput: number = this._getSanitizedValue();
if (offerThroughput > this.minimum()) {
offerThroughput -= this.step;
if (offerThroughput < this.minimum()) {
offerThroughput = this.minimum();
}
this.value(offerThroughput);
}
}
public increaseThroughput() {
let offerThroughput: number = this._getSanitizedValue();
if (offerThroughput < this.maximum() || this.canExceedMaximumValue()) {
offerThroughput += this.step;
if (offerThroughput > this.maximum() && !this.canExceedMaximumValue()) {
offerThroughput = this.maximum();
}
this.value(offerThroughput);
}
}
public onIncreaseKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.increaseThroughput();
event.stopPropagation();
return false;
}
return true;
};
public onDecreaseKeyDown = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
this.decreaseThroughput();
event.stopPropagation();
return false;
}
return true;
};
private _getSanitizedValue(): number {
const throughput = this.value();
return isNaN(throughput) ? 0 : Number(throughput);
}
private static _defaultStep: number = 100;
}
export const ThroughputInputComponent = {
viewModel: ThroughputInputViewModel,
template: ThroughputInputComponentTemplate
};

View File

@@ -19,7 +19,6 @@ describe("ContainerSampleGenerator", () => {
explorerStub.isPreferredApiTable = ko.computed<boolean>(() => false); explorerStub.isPreferredApiTable = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiCassandra = ko.computed<boolean>(() => false); explorerStub.isPreferredApiCassandra = ko.computed<boolean>(() => false);
explorerStub.canExceedMaximumValue = ko.computed<boolean>(() => false); explorerStub.canExceedMaximumValue = ko.computed<boolean>(() => false);
explorerStub.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
explorerStub.findDatabaseWithId = () => database; explorerStub.findDatabaseWithId = () => database;
explorerStub.refreshAllDatabases = () => Q.resolve(); explorerStub.refreshAllDatabases = () => Q.resolve();
return explorerStub; return explorerStub;

View File

@@ -86,6 +86,7 @@ import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandBut
import { updateUserContext, userContext } from "../UserContext"; import { updateUserContext, userContext } from "../UserContext";
import { stringToBlob } from "../Utils/BlobUtils"; import { stringToBlob } from "../Utils/BlobUtils";
import { IChoiceGroupProps } from "office-ui-fabric-react"; import { IChoiceGroupProps } from "office-ui-fabric-react";
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
BindingHandlersRegisterer.registerBindingHandlers(); BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import // Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@@ -212,7 +213,6 @@ export default class Explorer {
public isHostedDataExplorerEnabled: ko.Computed<boolean>; public isHostedDataExplorerEnabled: ko.Computed<boolean>;
public isRightPanelV2Enabled: ko.Computed<boolean>; public isRightPanelV2Enabled: ko.Computed<boolean>;
public canExceedMaximumValue: ko.Computed<boolean>; public canExceedMaximumValue: ko.Computed<boolean>;
public hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
public shouldShowShareDialogContents: ko.Observable<boolean>; public shouldShowShareDialogContents: ko.Observable<boolean>;
public shareAccessData: ko.Observable<AdHocAccessData>; public shareAccessData: ko.Observable<AdHocAccessData>;
@@ -422,13 +422,6 @@ export default class Explorer {
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue) this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
); );
this.hasAutoPilotV2FeatureFlag = ko.computed(() => {
if (this.isFeatureEnabled(Constants.Features.enableAutoPilotV2)) {
return true;
}
return false;
});
this.isNotificationConsoleExpanded = ko.observable<boolean>(false); this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
this.databases = ko.observableArray<ViewModels.Database>(); this.databases = ko.observableArray<ViewModels.Database>();
@@ -1047,11 +1040,11 @@ export default class Explorer {
); );
TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, startTime); TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, startTime);
this.databaseAccount(databaseAccount); this.databaseAccount(databaseAccount);
} catch (e) { } catch (error) {
NotificationConsoleUtils.clearInProgressMessageWithId(logId); NotificationConsoleUtils.clearInProgressMessageWithId(logId);
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error, ConsoleDataType.Error,
`Enabling Azure Synapse Link for this account failed. ${e.message || JSON.stringify(e)}` `Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`
); );
TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, startTime); TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, startTime);
} finally { } finally {
@@ -1120,7 +1113,7 @@ export default class Explorer {
); );
this.renewExplorerShareAccess(this, this.tokenForRenewal()) this.renewExplorerShareAccess(this, this.tokenForRenewal())
.fail((error: any) => { .fail((error: any) => {
const stringifiedError: string = error.message; const stringifiedError: string = getErrorMessage(error);
this.renewTokenError("Invalid connection string specified"); this.renewTokenError("Invalid connection string specified");
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error, ConsoleDataType.Error,
@@ -1149,7 +1142,7 @@ export default class Explorer {
NotificationConsoleUtils.clearInProgressMessageWithId(id); NotificationConsoleUtils.clearInProgressMessageWithId(id);
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error, ConsoleDataType.Error,
`Failed to generate share url: ${error.message}` `Failed to generate share url: ${getErrorMessage(error)}`
); );
console.error(error); console.error(error);
} }
@@ -1174,7 +1167,10 @@ export default class Explorer {
deferred.resolve(); deferred.resolve();
}, },
(error: any) => { (error: any) => {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to connect: ${error.message}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to connect: ${getErrorMessage(error)}`
);
deferred.reject(error); deferred.reject(error);
} }
) )
@@ -1448,19 +1444,21 @@ export default class Explorer {
this._setLoadingStatusText("Failed to fetch databases."); this._setLoadingStatusText("Failed to fetch databases.");
this.isRefreshingExplorer(false); this.isRefreshingExplorer(false);
deferred.reject(error); deferred.reject(error);
const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.LoadDatabases, Action.LoadDatabases,
{ {
databaseAccountName: this.databaseAccount().name, databaseAccountName: this.databaseAccount().name,
defaultExperience: this.defaultExperience(), defaultExperience: this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
error: error.message error: errorMessage,
errorStack: getErrorStack(error)
}, },
startKey startKey
); );
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error, ConsoleDataType.Error,
`Error while refreshing databases: ${error.message}` `Error while refreshing databases: ${errorMessage}`
); );
} }
); );
@@ -1479,7 +1477,7 @@ export default class Explorer {
); );
} }
}, },
reason => { error => {
if (resourceTreeStartKey != null) { if (resourceTreeStartKey != null) {
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.LoadResourceTree, Action.LoadResourceTree,
@@ -1487,7 +1485,8 @@ export default class Explorer {
databaseAccountName: this.databaseAccount() && this.databaseAccount().name, databaseAccountName: this.databaseAccount() && this.databaseAccount().name,
defaultExperience: this.defaultExperience && this.defaultExperience(), defaultExperience: this.defaultExperience && this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
error: reason error: getErrorMessage(error),
errorStack: getErrorStack(error)
}, },
resourceTreeStartKey resourceTreeStartKey
); );
@@ -1536,7 +1535,7 @@ export default class Explorer {
resolve(token); resolve(token);
}, },
(error: any) => { (error: any) => {
Logger.logError(error, "Explorer/getArcadiaToken"); Logger.logError(getErrorMessage(error), "Explorer/getArcadiaToken");
resolve(undefined); resolve(undefined);
} }
); );
@@ -1554,7 +1553,7 @@ export default class Explorer {
workspaceItems[i] = { ...workspace, sparkPools: sparkpools }; workspaceItems[i] = { ...workspace, sparkPools: sparkpools };
}, },
error => { error => {
Logger.logError(error, "Explorer/this._arcadiaManager.listSparkPoolsAsync"); Logger.logError(getErrorMessage(error), "Explorer/this._arcadiaManager.listSparkPoolsAsync");
} }
); );
sparkPromises.push(promise); sparkPromises.push(promise);
@@ -1562,8 +1561,7 @@ export default class Explorer {
return Promise.all(sparkPromises).then(() => workspaceItems); return Promise.all(sparkPromises).then(() => workspaceItems);
} catch (error) { } catch (error) {
Logger.logError(error, "Explorer/this._arcadiaManager.listWorkspacesAsync"); handleError(error, "Explorer/this._arcadiaManager.listWorkspacesAsync", "Get Arcadia workspaces failed");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error.message);
return Promise.resolve([]); return Promise.resolve([]);
} }
} }
@@ -1598,10 +1596,10 @@ export default class Explorer {
); );
} catch (error) { } catch (error) {
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
Logger.logError(error, "initNotebooks/getNotebookConnectionInfoAsync"); handleError(
NotificationConsoleUtils.logConsoleMessage( error,
ConsoleDataType.Error, "initNotebooks/getNotebookConnectionInfoAsync",
`Failed to get notebook workspace connection info: ${error.message}` `Failed to get notebook workspace connection info: ${getErrorMessage(error)}`
); );
throw error; throw error;
} finally { } finally {
@@ -1624,9 +1622,10 @@ export default class Explorer {
public resetNotebookWorkspace() { public resetNotebookWorkspace() {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookClient) {
const error = "Attempt to reset notebook workspace, but notebook is not enabled"; handleError(
Logger.logError(error, "Explorer/resetNotebookWorkspace"); "Attempt to reset notebook workspace, but notebook is not enabled",
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error); "Explorer/resetNotebookWorkspace"
);
return; return;
} }
const resetConfirmationDialogProps: DialogProps = { const resetConfirmationDialogProps: DialogProps = {
@@ -1651,7 +1650,7 @@ export default class Explorer {
const workspaces = await this.notebookWorkspaceManager.getNotebookWorkspacesAsync(databaseAccount?.id); const workspaces = await this.notebookWorkspaceManager.getNotebookWorkspacesAsync(databaseAccount?.id);
return workspaces && workspaces.length > 0 && workspaces.some(workspace => workspace.name === "default"); return workspaces && workspaces.length > 0 && workspaces.some(workspace => workspace.name === "default");
} catch (error) { } catch (error) {
Logger.logError(error, "Explorer/_containsDefaultNotebookWorkspace"); Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace");
return false; return false;
} }
} }
@@ -1677,8 +1676,7 @@ export default class Explorer {
await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(this.databaseAccount().id, "default"); await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(this.databaseAccount().id, "default");
} }
} catch (error) { } catch (error) {
Logger.logError(error, "Explorer/ensureNotebookWorkspaceRunning"); handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace");
NotificationConsoleUtils.logConsoleError(`Failed to initialize notebook workspace: ${error.message}`);
} finally { } finally {
clearMessage && clearMessage(); clearMessage && clearMessage();
} }
@@ -1693,7 +1691,10 @@ export default class Explorer {
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace); TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
} catch (error) { } catch (error) {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to reset notebook workspace: ${error}`); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to reset notebook workspace: ${error}`);
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, error); TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, {
error: getErrorMessage(error),
errorStack: getErrorStack(error)
});
throw error; throw error;
} finally { } finally {
NotificationConsoleUtils.clearInProgressMessageWithId(id); NotificationConsoleUtils.clearInProgressMessageWithId(id);
@@ -2060,7 +2061,8 @@ export default class Explorer {
databaseAccountName: this.databaseAccount() && this.databaseAccount().name, databaseAccountName: this.databaseAccount() && this.databaseAccount().name,
defaultExperience: this.defaultExperience && this.defaultExperience(), defaultExperience: this.defaultExperience && this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
trace: error.message error: getErrorMessage(error),
errorStack: getErrorStack(error)
}, },
startKey startKey
); );
@@ -2226,8 +2228,7 @@ export default class Explorer {
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> { public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to upload notebook, but notebook is not enabled"; const error = "Attempt to upload notebook, but notebook is not enabled";
Logger.logError(error, "Explorer/uploadFile"); handleError(error, "Explorer/uploadFile");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
throw new Error(error); throw new Error(error);
} }
@@ -2408,8 +2409,7 @@ export default class Explorer {
public renameNotebook(notebookFile: NotebookContentItem): Q.Promise<NotebookContentItem> { public renameNotebook(notebookFile: NotebookContentItem): Q.Promise<NotebookContentItem> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to rename notebook, but notebook is not enabled"; const error = "Attempt to rename notebook, but notebook is not enabled";
Logger.logError(error, "Explorer/renameNotebook"); handleError(error, "Explorer/renameNotebook");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
throw new Error(error); throw new Error(error);
} }
@@ -2457,8 +2457,7 @@ export default class Explorer {
public onCreateDirectory(parent: NotebookContentItem): Q.Promise<NotebookContentItem> { public onCreateDirectory(parent: NotebookContentItem): Q.Promise<NotebookContentItem> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create notebook directory, but notebook is not enabled"; const error = "Attempt to create notebook directory, but notebook is not enabled";
Logger.logError(error, "Explorer/onCreateDirectory"); handleError(error, "Explorer/onCreateDirectory");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
throw new Error(error); throw new Error(error);
} }
@@ -2479,8 +2478,7 @@ export default class Explorer {
public readFile(notebookFile: NotebookContentItem): Promise<string> { public readFile(notebookFile: NotebookContentItem): Promise<string> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to read file, but notebook is not enabled"; const error = "Attempt to read file, but notebook is not enabled";
Logger.logError(error, "Explorer/downloadFile"); handleError(error, "Explorer/downloadFile");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
throw new Error(error); throw new Error(error);
} }
@@ -2490,8 +2488,7 @@ export default class Explorer {
public downloadFile(notebookFile: NotebookContentItem): Promise<void> { public downloadFile(notebookFile: NotebookContentItem): Promise<void> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to download file, but notebook is not enabled"; const error = "Attempt to download file, but notebook is not enabled";
Logger.logError(error, "Explorer/downloadFile"); handleError(error, "Explorer/downloadFile");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
throw new Error(error); throw new Error(error);
} }
@@ -2522,7 +2519,7 @@ export default class Explorer {
(error: any) => { (error: any) => {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error, ConsoleDataType.Error,
`Could not download notebook ${error.message}` `Could not download notebook ${getErrorMessage(error)}`
); );
clearMessage(); clearMessage();
@@ -2572,7 +2569,7 @@ export default class Explorer {
); );
this.isNotebooksEnabledForAccount(isAccountInAllowedLocation); this.isNotebooksEnabledForAccount(isAccountInAllowedLocation);
} catch (error) { } catch (error) {
Logger.logError(error, "Explorer/isNotebooksEnabledForAccount"); Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
this.isNotebooksEnabledForAccount(false); this.isNotebooksEnabledForAccount(false);
} }
} }
@@ -2601,7 +2598,7 @@ export default class Explorer {
false; false;
this.isSparkEnabledForAccount(isEnabled); this.isSparkEnabledForAccount(isEnabled);
} catch (error) { } catch (error) {
Logger.logError(error, "Explorer/isSparkEnabledForAccount"); Logger.logError(getErrorMessage(error), "Explorer/isSparkEnabledForAccount");
this.isSparkEnabledForAccount(false); this.isSparkEnabledForAccount(false);
} }
}; };
@@ -2626,7 +2623,7 @@ export default class Explorer {
(featureStatus && featureStatus.properties && featureStatus.properties.state === "Registered") || false; (featureStatus && featureStatus.properties && featureStatus.properties.state === "Registered") || false;
return isEnabled; return isEnabled;
} catch (error) { } catch (error) {
Logger.logError(error, "Explorer/isSparkEnabledForAccount"); Logger.logError(getErrorMessage(error), "Explorer/isSparkEnabledForAccount");
return false; return false;
} }
}; };
@@ -2645,8 +2642,7 @@ export default class Explorer {
public deleteNotebookFile(item: NotebookContentItem): Promise<void> { public deleteNotebookFile(item: NotebookContentItem): Promise<void> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to delete notebook file, but notebook is not enabled"; const error = "Attempt to delete notebook file, but notebook is not enabled";
Logger.logError(error, "Explorer/deleteNotebookFile"); handleError(error, "Explorer/deleteNotebookFile");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
throw new Error(error); throw new Error(error);
} }
@@ -2695,8 +2691,7 @@ export default class Explorer {
public onNewNotebookClicked(parent?: NotebookContentItem): void { public onNewNotebookClicked(parent?: NotebookContentItem): void {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create new notebook, but notebook is not enabled"; const error = "Attempt to create new notebook, but notebook is not enabled";
Logger.logError(error, "Explorer/onNewNotebookClicked"); handleError(error, "Explorer/onNewNotebookClicked");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
throw new Error(error); throw new Error(error);
} }
@@ -2729,16 +2724,17 @@ export default class Explorer {
return this.openNotebook(newFile); return this.openNotebook(newFile);
}) })
.then(() => this.resourceTree.triggerRender()) .then(() => this.resourceTree.triggerRender())
.catch((reason: any) => { .catch((error: any) => {
const error = `Failed to create a new notebook: ${reason}`; const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.CreateNewNotebook, Action.CreateNewNotebook,
{ {
databaseAccountName: this.databaseAccount().name, databaseAccountName: this.databaseAccount().name,
defaultExperience: this.defaultExperience(), defaultExperience: this.defaultExperience(),
dataExplorerArea: Constants.Areas.Notebook, dataExplorerArea: Constants.Areas.Notebook,
error error: errorMessage,
errorStack: getErrorStack(error)
}, },
startKey startKey
); );
@@ -2781,8 +2777,7 @@ export default class Explorer {
public refreshContentItem(item: NotebookContentItem): Promise<void> { public refreshContentItem(item: NotebookContentItem): Promise<void> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to refresh notebook list, but notebook is not enabled"; const error = "Attempt to refresh notebook list, but notebook is not enabled";
Logger.logError(error, "Explorer/refreshContentItem"); handleError(error, "Explorer/refreshContentItem");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
return Promise.reject(new Error(error)); return Promise.reject(new Error(error));
} }
@@ -2968,7 +2963,7 @@ export default class Explorer {
} }
return tokenRefreshInterval; return tokenRefreshInterval;
} catch (error) { } catch (error) {
Logger.logError(error, "Explorer/getTokenRefreshInterval"); Logger.logError(getErrorMessage(error), "Explorer/getTokenRefreshInterval");
return tokenRefreshInterval; return tokenRefreshInterval;
} }
} }

View File

@@ -29,6 +29,7 @@ import { InputProperty } from "../../../Contracts/ViewModels";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif"; import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase"; import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
export interface GraphAccessor { export interface GraphAccessor {
applyFilter: () => void; applyFilter: () => void;
@@ -892,7 +893,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
backendPromise.then( backendPromise.then(
(result: UserQueryResult) => (this.queryTotalRequestCharge = result.requestCharge), (result: UserQueryResult) => (this.queryTotalRequestCharge = result.requestCharge),
(error: any) => { (error: any) => {
const errorMsg = `Failure in submitting query: ${query}: ${error.message}`; const errorMsg = `Failure in submitting query: ${query}: ${getErrorMessage(error)}`;
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
this.setState({ this.setState({
filterQueryError: errorMsg filterQueryError: errorMsg
@@ -1826,7 +1827,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
promise promise
.then((result: GremlinClient.GremlinRequestResult) => this.processGremlinQueryResults(result)) .then((result: GremlinClient.GremlinRequestResult) => this.processGremlinQueryResults(result))
.catch((error: any) => { .catch((error: any) => {
const errorMsg = `Failed to process query result: ${error.message}`; const errorMsg = `Failed to process query result: ${getErrorMessage(error)}`;
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
this.setState({ this.setState({
filterQueryError: errorMsg filterQueryError: errorMsg

View File

@@ -94,7 +94,7 @@ describe("Gremlin Client", () => {
it("should log and display error out on unknown requestId", () => { it("should log and display error out on unknown requestId", () => {
const gremlinClient = new GremlinClient(); const gremlinClient = new GremlinClient();
const logConsoleSpy = sinon.spy(NotificationConsoleUtils, "logConsoleMessage"); const logConsoleSpy = sinon.spy(NotificationConsoleUtils, "logConsoleError");
const logErrorSpy = sinon.spy(Logger, "logError"); const logErrorSpy = sinon.spy(Logger, "logError");
gremlinClient.initialize(emptyParams); gremlinClient.initialize(emptyParams);
@@ -122,7 +122,7 @@ describe("Gremlin Client", () => {
}); });
it("should not aggregate RU if not a number and reset totalRequestCharge to undefined", done => { it("should not aggregate RU if not a number and reset totalRequestCharge to undefined", done => {
const logConsoleSpy = sinon.spy(NotificationConsoleUtils, "logConsoleMessage"); const logConsoleSpy = sinon.spy(NotificationConsoleUtils, "logConsoleError");
const logErrorSpy = sinon.spy(Logger, "logError"); const logErrorSpy = sinon.spy(Logger, "logError");
const gremlinClient = new GremlinClient(); const gremlinClient = new GremlinClient();
@@ -165,7 +165,7 @@ describe("Gremlin Client", () => {
}); });
it("should not aggregate RU if undefined and reset totalRequestCharge to undefined", done => { it("should not aggregate RU if undefined and reset totalRequestCharge to undefined", done => {
const logConsoleSpy = sinon.spy(NotificationConsoleUtils, "logConsoleMessage"); const logConsoleSpy = sinon.spy(NotificationConsoleUtils, "logConsoleError");
const logErrorSpy = sinon.spy(Logger, "logError"); const logErrorSpy = sinon.spy(Logger, "logError");
const gremlinClient = new GremlinClient(); const gremlinClient = new GremlinClient();

View File

@@ -7,7 +7,7 @@ import { GremlinSimpleClient, Result } from "./GremlinSimpleClient";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { HashMap } from "../../../Common/HashMap"; import { HashMap } from "../../../Common/HashMap";
import * as Logger from "../../../Common/Logger"; import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
export interface GremlinClientParameters { export interface GremlinClientParameters {
endpoint: string; endpoint: string;
@@ -58,26 +58,23 @@ export class GremlinClient {
} }
}, },
failureCallback: (result: Result, error: any) => { failureCallback: (result: Result, error: any) => {
if (typeof error !== "string") { const errorMessage = getErrorMessage(error);
error = error.message;
}
const requestId = result.requestId; const requestId = result.requestId;
if (!requestId || !this.pendingResults.has(requestId)) { if (!requestId || !this.pendingResults.has(requestId)) {
const msg = `Error: ${error}, unknown requestId:${requestId} ${GremlinClient.getRequestChargeString( const errorMsg = `Error: ${errorMessage}, unknown requestId:${requestId} ${GremlinClient.getRequestChargeString(
result.requestCharge result.requestCharge
)}`; )}`;
GremlinClient.reportError(msg); handleError(errorMsg, GremlinClient.LOG_AREA);
// Fail all pending requests if no request id (fatal) // Fail all pending requests if no request id (fatal)
if (!requestId) { if (!requestId) {
this.pendingResults.keys().forEach((reqId: string) => { this.pendingResults.keys().forEach((reqId: string) => {
this.abortPendingRequest(reqId, error, null); this.abortPendingRequest(reqId, errorMessage, null);
}); });
} }
} else { } else {
this.abortPendingRequest(requestId, error, result.requestCharge); this.abortPendingRequest(requestId, errorMessage, result.requestCharge);
} }
}, },
infoCallback: (msg: string) => { infoCallback: (msg: string) => {
@@ -132,15 +129,16 @@ export class GremlinClient {
deferred.reject(error); deferred.reject(error);
this.pendingResults.delete(requestId); this.pendingResults.delete(requestId);
GremlinClient.reportError( const errorMsg = `Aborting pending request ${requestId}. Error:${error} ${GremlinClient.getRequestChargeString(
`Aborting pending request ${requestId}. Error:${error} ${GremlinClient.getRequestChargeString(requestCharge)}` requestCharge
); )}`;
handleError(errorMsg, GremlinClient.LOG_AREA);
} }
private flushResult(requestId: string) { private flushResult(requestId: string) {
if (!this.pendingResults.has(requestId)) { if (!this.pendingResults.has(requestId)) {
const msg = `Unknown requestId:${requestId}`; const errorMsg = `Unknown requestId:${requestId}`;
GremlinClient.reportError(msg); handleError(errorMsg, GremlinClient.LOG_AREA);
return; return;
} }
@@ -158,8 +156,8 @@ export class GremlinClient {
*/ */
private storePendingResult(result: Result): boolean { private storePendingResult(result: Result): boolean {
if (!this.pendingResults.has(result.requestId)) { if (!this.pendingResults.has(result.requestId)) {
const msg = `Dropping result for unknown requestId:${result.requestId}`; const errorMsg = `Dropping result for unknown requestId:${result.requestId}`;
GremlinClient.reportError(msg); handleError(errorMsg, GremlinClient.LOG_AREA);
return false; return false;
} }
const pendingResults = this.pendingResults.get(result.requestId).result; const pendingResults = this.pendingResults.get(result.requestId).result;
@@ -179,9 +177,8 @@ export class GremlinClient {
if (result.requestCharge === undefined || typeof result.requestCharge !== "number") { if (result.requestCharge === undefined || typeof result.requestCharge !== "number") {
// Clear totalRequestCharge, even if it was a valid number as the total might be incomplete therefore incorrect // Clear totalRequestCharge, even if it was a valid number as the total might be incomplete therefore incorrect
pendingResults.totalRequestCharge = undefined; pendingResults.totalRequestCharge = undefined;
GremlinClient.reportError( const errorMsg = `Unable to perform RU aggregation calculation with non numbers. Result request charge: ${result.requestCharge}. RequestId: ${result.requestId}`;
`Unable to perform RU aggregation calculation with non numbers. Result request charge: ${result.requestCharge}. RequestId: ${result.requestId}` handleError(errorMsg, GremlinClient.LOG_AREA);
);
} else { } else {
if (pendingResults.totalRequestCharge === undefined) { if (pendingResults.totalRequestCharge === undefined) {
pendingResults.totalRequestCharge = 0; pendingResults.totalRequestCharge = 0;
@@ -190,9 +187,4 @@ export class GremlinClient {
} }
return pendingResults.isIncomplete; return pendingResults.isIncomplete;
} }
private static reportError(msg: string): void {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
Logger.logError(msg, GremlinClient.LOG_AREA);
}
} }

View File

@@ -20,7 +20,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false); mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isNotebookEnabled = ko.observable(false); mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false); mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
@@ -62,7 +62,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false); mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false); mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
}); });
@@ -126,7 +126,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false); mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false); mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
}); });
@@ -208,7 +208,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false); mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false); mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
}); });
@@ -289,7 +289,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isPreferredApiTable = ko.computed(() => true); mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false); mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
@@ -348,7 +348,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isAuthWithResourceToken = ko.observable(true); mockExplorer.isAuthWithResourceToken = ko.observable(true);
mockExplorer.isPreferredApiDocumentDB = ko.computed(() => true); mockExplorer.isPreferredApiDocumentDB = ko.computed(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true; mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true); mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false); mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
}); });

View File

@@ -6,6 +6,7 @@ import * as Constants from "../../Common/Constants";
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 * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
export class NotebookContainerClient { export class NotebookContainerClient {
private reconnectingNotificationId: string; private reconnectingNotificationId: string;
@@ -74,7 +75,7 @@ export class NotebookContainerClient {
} }
return undefined; return undefined;
} catch (error) { } catch (error) {
Logger.logError(error, "NotebookContainerClient/getMemoryUsage"); Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage");
if (!this.reconnectingNotificationId) { if (!this.reconnectingNotificationId) {
this.reconnectingNotificationId = NotificationConsoleUtils.logConsoleMessage( this.reconnectingNotificationId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
@@ -110,7 +111,7 @@ export class NotebookContainerClient {
headers: { Authorization: authToken } headers: { Authorization: authToken }
}); });
} catch (error) { } catch (error) {
Logger.logError(error, "NotebookContainerClient/resetWorkspace"); Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
await this.recreateNotebookWorkspaceAsync(); await this.recreateNotebookWorkspaceAsync();
} }
} }
@@ -140,7 +141,7 @@ export class NotebookContainerClient {
await notebookWorkspaceManager.deleteNotebookWorkspaceAsync(explorer.databaseAccount().id, "default"); await notebookWorkspaceManager.deleteNotebookWorkspaceAsync(explorer.databaseAccount().id, "default");
await notebookWorkspaceManager.createNotebookWorkspaceAsync(explorer.databaseAccount().id, "default"); await notebookWorkspaceManager.createNotebookWorkspaceAsync(explorer.databaseAccount().id, "default");
} catch (error) { } catch (error) {
Logger.logError(error, "NotebookContainerClient/recreateNotebookWorkspaceAsync"); Logger.logError(getErrorMessage(error), "NotebookContainerClient/recreateNotebookWorkspaceAsync");
return Promise.reject(error); return Promise.reject(error);
} }
} }

View File

@@ -26,6 +26,7 @@ import { ImmutableNotebook } from "@nteract/commutable";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { ContextualPaneBase } from "../Panes/ContextualPaneBase"; import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
import { CopyNotebookPaneAdapter } from "../Panes/CopyNotebookPane"; import { CopyNotebookPaneAdapter } from "../Panes/CopyNotebookPane";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
export interface NotebookManagerOptions { export interface NotebookManagerOptions {
container: Explorer; container: Explorer;
@@ -147,7 +148,7 @@ export default class NotebookManager {
// Octokit's error handler uses any // Octokit's error handler uses any
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
private onGitHubClientError = (error: any): void => { private onGitHubClientError = (error: any): void => {
Logger.logError(error, "NotebookManager/onGitHubClientError"); Logger.logError(getErrorMessage(error), "NotebookManager/onGitHubClientError");
if (error.status === HttpStatusCodes.Unauthorized) { if (error.status === HttpStatusCodes.Unauthorized) {
this.gitHubOAuthService.resetToken(); this.gitHubOAuthService.resetToken();

View File

@@ -127,7 +127,6 @@
be shared across all containers within the database.</span> be shared across all containers within the database.</span>
</span> </span>
</div> </div>
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: databaseCreateNewShared() && databaseCreateNew()"> <div data-bind="visible: databaseCreateNewShared() && databaseCreateNew()">
<!-- 1 --> <!-- 1 -->
<throughput-input-autopilot-v3 params="{ <throughput-input-autopilot-v3 params="{
@@ -158,38 +157,6 @@
</throughput-input-autopilot-v3> </throughput-input-autopilot-v3>
</div> </div>
<!-- /ko --> <!-- /ko -->
<!-- ko if: hasAutoPilotV2FeatureFlag && hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: databaseCreateNewShared() && databaseCreateNew()">
<!-- 1 -->
<throughput-input params="{
testId: 'databaseThroughputValue',
value: throughputDatabase,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: databaseCreateNewShared() && databaseCreateNew(),
label: sharedThroughputRangeText,
ariaLabel: sharedThroughputRangeText,
costsVisible: costsVisible,
requestUnitsUsageCost: requestUnitsUsageCost,
spendAckChecked: throughputSpendAck,
spendAckId: 'throughputSpendAck',
spendAckText: throughputSpendAckText,
spendAckVisible: throughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newContainer-databaseThroughput-autoPilotRadio',
throughputProvisionedRadioId: 'newContainer-databaseThroughput-manualRadio',
throughputModeRadioName: 'sharedThroughputModeRadio',
isAutoPilotSelected: isSharedAutoPilotSelected,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue,
autoPilotTiersList: sharedAutoPilotTiersList,
selectedAutoPilotTier: selectedSharedAutoPilotTier
}">
</throughput-input>
</div>
<!-- /ko -->
<!-- /ko -->
<!-- Database provisioned throughput - End --> <!-- Database provisioned throughput - End -->
</div> </div>
@@ -211,19 +178,6 @@
data-bind="value: collectionId, attr: { 'aria-label': collectionIdTitle }"> data-bind="value: collectionId, attr: { 'aria-label': collectionIdTitle }">
</div> </div>
<!-- <p class="seconddivpadding" data-bind="visible:(container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) && !databaseHasSharedOffer()">
Where did 'fixed' containers go?
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext noFixedCollectionsTooltipWidth">
We lowered the minimum throughput for partitioned containers to 400 RU/s, removing the only drawback partitioned containers had. <br/><br/>
We are planning to deprecate ability to create non-partitioned containers, as they do not allow you to scale elastically.
If for some reason you still need a container without partition key, you can use our SDKs to create one. <br/><br/>
Please <a href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback">contact us</a> if you have questions or concerns.
</span>
</span>
</p> -->
<!-- Indexing For Shared Throughput - start --> <!-- Indexing For Shared Throughput - start -->
<div class="seconddivpadding" <div class="seconddivpadding"
data-bind="visible: showIndexingOptionsForSharedThroughput() && !container.isPreferredApiMongoDB()"> data-bind="visible: showIndexingOptionsForSharedThroughput() && !container.isPreferredApiMongoDB()">
@@ -287,62 +241,6 @@
</div> </div>
<!-- Unlimited option button - End --> <!-- Unlimited option button - End -->
</div> </div>
<!-- Fixed Button Content - Start -->
<div class="tabcontent" data-bind="visible: isFixedStorageSelected() && !databaseHasSharedOffer()">
<!-- 2 -->
<!-- note: this is used when creating a fixed collection without shared throughput. only manual throughput is available. -->
<throughput-input params="{
testId: 'fixedThroughputValue',
value: throughputSinglePartition,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: isFixedStorageSelected() && !databaseHasSharedOffer(),
label: throughputRangeText,
ariaLabel: throughputRangeText,
costsVisible: costsVisible,
requestUnitsUsageCost: requestUnitsUsageCost
showAsMandatory: true,
isFixed: true,
infoBubbleText: ruToolTipText,
canExceedMaximumValue: canExceedMaximumValue
}">
</throughput-input>
<div data-bind="visible: rupmVisible">
<div class="tabs">
<p class="pkPadding">
<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="rupmOn" name="rupmcoll" value="on" class="radio"
data-bind="checked: rupm">
<label for="rupmOn">ON</label>
</div>
<div class="tab">
<input type="radio" id="rupmOff" name="rupmcoll" value="off" class="radio"
data-bind="checked: rupm">
<label for="rupmOff">OFF</label>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed Button Content - End -->
<!-- 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 data-bind="visible: rupmVisible">
@@ -390,7 +288,7 @@
range of values and is likely to have evenly distributed access patterns.</span> range of values and is likely to have evenly distributed access patterns.</span>
</span> </span>
</p> </p>
<input type="text" data-test="addCollection-partitionKeyValue" aria-required="true" size="40" <input type="text" id="partitionKeyValue" data-test="addCollection-partitionKeyValue" aria-required="true" size="40"
class="textfontclr collid" data-bind="textInput: partitionKey, class="textfontclr collid" data-bind="textInput: partitionKey,
attr: { attr: {
placeholder: partitionKeyPlaceholder, placeholder: partitionKeyPlaceholder,
@@ -442,7 +340,6 @@
<!-- Provision collection throughput checkbox - end --> <!-- Provision collection throughput checkbox - end -->
<!-- Provision collection throughput spinner - start --> <!-- Provision collection throughput spinner - start -->
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: displayCollectionThroughput" data-test="addCollection-displayCollectionThroughput"> <div data-bind="visible: displayCollectionThroughput" data-test="addCollection-displayCollectionThroughput">
<!-- 3 --> <!-- 3 -->
<throughput-input-autopilot-v3 params="{ <throughput-input-autopilot-v3 params="{
@@ -472,40 +369,6 @@
}"> }">
</throughput-input-autopilot-v3> </throughput-input-autopilot-v3>
</div> </div>
<!-- /ko -->
<!-- ko if: hasAutoPilotV2FeatureFlag && hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: displayCollectionThroughput" data-test="addCollection-displayCollectionThroughput">
<!-- 3 -->
<throughput-input params="{
testId: 'collectionThroughputValue',
value: throughputMultiPartition,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: displayCollectionThroughput,
label: throughputRangeText,
ariaLabel: throughputRangeText,
costsVisible: costsVisible,
requestUnitsUsageCost: dedicatedRequestUnitsUsageCost,
spendAckChecked: throughputSpendAck,
spendAckId: 'throughputSpendAckCollection',
spendAckText: throughputSpendAckText,
spendAckVisible: throughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newContainer-containerThroughput-autoPilotRadio',
throughputProvisionedRadioId: 'newContainer-containerThroughput-manualRadio',
throughputModeRadioName: 'throughputModeRadioName',
isAutoPilotSelected: isAutoPilotSelected,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue,
autoPilotTiersList: autoPilotTiersList,
selectedAutoPilotTier: selectedAutoPilotTier,
showAutoPilot: !isFixedStorageSelected()
}">
</throughput-input>
</div>
<!-- /ko -->
<!-- Provision collection throughput spinner - end --> <!-- Provision collection throughput spinner - end -->
<!-- /ko --> <!-- /ko -->
<!-- Provision collection throughput - end --> <!-- Provision collection throughput - end -->

View File

@@ -1,8 +1,7 @@
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import AddCollectionPane from "./AddCollectionPane"; import AddCollectionPane from "./AddCollectionPane";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import ko from "knockout"; import { DatabaseAccount } from "../../Contracts/DataModels";
import { AutopilotTier, DatabaseAccount } from "../../Contracts/DataModels";
describe("Add Collection Pane", () => { describe("Add Collection Pane", () => {
describe("isValid()", () => { describe("isValid()", () => {
@@ -41,25 +40,6 @@ describe("Add Collection Pane", () => {
beforeEach(() => { beforeEach(() => {
explorer = new Explorer(); explorer = new Explorer();
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
});
it("should be true if autopilot enabled and select valid tier", () => {
explorer.databaseAccount(mockDatabaseAccount);
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
addCollectionPane.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
addCollectionPane.isAutoPilotSelected(true);
addCollectionPane.selectedAutoPilotTier(AutopilotTier.Tier2);
expect(addCollectionPane.isValid()).toBe(true);
});
it("should be false if autopilot enabled and select invalid tier", () => {
explorer.databaseAccount(mockDatabaseAccount);
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
addCollectionPane.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
addCollectionPane.isAutoPilotSelected(true);
addCollectionPane.selectedAutoPilotTier(0);
expect(addCollectionPane.isValid()).toBe(false);
}); });
it("should be true if graph API and partition key is not /id nor /label", () => { it("should be true if graph API and partition key is not /id nor /label", () => {

View File

@@ -3,7 +3,6 @@ import * as AddCollectionUtility from "../../Shared/AddCollectionUtility";
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 ErrorParserUtility from "../../Common/ErrorParserUtility";
import * as ko from "knockout"; import * as ko from "knockout";
import * as PricingUtils from "../../Utils/PricingUtils"; import * as PricingUtils from "../../Utils/PricingUtils";
import * as SharedConstants from "../../Shared/Constants"; import * as SharedConstants from "../../Shared/Constants";
@@ -15,6 +14,7 @@ import { configContext, Platform } from "../../ConfigContext";
import { ContextualPaneBase } from "./ContextualPaneBase"; 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";
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions { export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
isPreferredApiTable: ko.Computed<boolean>; isPreferredApiTable: ko.Computed<boolean>;
@@ -75,10 +75,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
public debugstring: ko.Computed<string>; public debugstring: ko.Computed<string>;
public displayCollectionThroughput: ko.Computed<boolean>; public displayCollectionThroughput: ko.Computed<boolean>;
public isAutoPilotSelected: ko.Observable<boolean>; public isAutoPilotSelected: ko.Observable<boolean>;
public selectedAutoPilotTier: ko.Observable<DataModels.AutopilotTier>;
public selectedSharedAutoPilotTier: ko.Observable<DataModels.AutopilotTier>;
public autoPilotTiersList: ko.ObservableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>;
public sharedAutoPilotTiersList: ko.ObservableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>;
public isSharedAutoPilotSelected: ko.Observable<boolean>; public isSharedAutoPilotSelected: ko.Observable<boolean>;
public autoPilotThroughput: ko.Observable<number>; public autoPilotThroughput: ko.Observable<number>;
public sharedAutoPilotThroughput: ko.Observable<number>; public sharedAutoPilotThroughput: ko.Observable<number>;
@@ -92,7 +88,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
public isAnalyticalStorageOn: ko.Observable<boolean>; public isAnalyticalStorageOn: ko.Observable<boolean>;
public isSynapseLinkUpdating: ko.Computed<boolean>; public isSynapseLinkUpdating: ko.Computed<boolean>;
public canExceedMaximumValue: ko.PureComputed<boolean>; public canExceedMaximumValue: ko.PureComputed<boolean>;
public hasAutoPilotV2FeatureFlag: ko.PureComputed<boolean>;
public ruToolTipText: ko.Computed<string>; public ruToolTipText: ko.Computed<string>;
public canConfigureThroughput: ko.PureComputed<boolean>; public canConfigureThroughput: ko.PureComputed<boolean>;
public showUpsellMessage: ko.PureComputed<boolean>; public showUpsellMessage: ko.PureComputed<boolean>;
@@ -102,8 +97,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
constructor(options: AddCollectionPaneOptions) { constructor(options: AddCollectionPaneOptions) {
super(options); super(options);
this.hasAutoPilotV2FeatureFlag = ko.pureComputed(() => this.container.hasAutoPilotV2FeatureFlag()); this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText());
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText(this.hasAutoPilotV2FeatureFlag()));
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled()); this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled()); this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.formWarnings = ko.observable<string>(); this.formWarnings = ko.observable<string>();
@@ -171,13 +165,13 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.databaseHasSharedOffer = ko.observable<boolean>(true); this.databaseHasSharedOffer = ko.observable<boolean>(true);
this.throughputRangeText = ko.pureComputed<string>(() => { this.throughputRangeText = ko.pureComputed<string>(() => {
if (this.isAutoPilotSelected()) { if (this.isAutoPilotSelected()) {
return AutoPilotUtils.getAutoPilotHeaderText(this.hasAutoPilotV2FeatureFlag()); return AutoPilotUtils.getAutoPilotHeaderText();
} }
return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`; return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`;
}); });
this.sharedThroughputRangeText = ko.pureComputed<string>(() => { this.sharedThroughputRangeText = ko.pureComputed<string>(() => {
if (this.isSharedAutoPilotSelected()) { if (this.isSharedAutoPilotSelected()) {
return AutoPilotUtils.getAutoPilotHeaderText(this.hasAutoPilotV2FeatureFlag()); return AutoPilotUtils.getAutoPilotHeaderText();
} }
return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`; return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`;
}); });
@@ -447,7 +441,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => { this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {
const autoscaleThroughput = this.autoPilotThroughput() * 1; const autoscaleThroughput = this.autoPilotThroughput() * 1;
if (!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected()) { if (this.isAutoPilotSelected()) {
return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
} }
const selectedThroughput: number = this._getThroughput(); const selectedThroughput: number = this._getThroughput();
@@ -490,14 +484,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.isAutoPilotSelected = ko.observable<boolean>(false); this.isAutoPilotSelected = ko.observable<boolean>(false);
this.isSharedAutoPilotSelected = ko.observable<boolean>(false); this.isSharedAutoPilotSelected = ko.observable<boolean>(false);
this.selectedAutoPilotTier = ko.observable<DataModels.AutopilotTier>();
this.selectedSharedAutoPilotTier = ko.observable<DataModels.AutopilotTier>();
this.autoPilotTiersList = ko.observableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>(
AutoPilotUtils.getAvailableAutoPilotTiersOptions()
);
this.sharedAutoPilotTiersList = ko.observableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>(
AutoPilotUtils.getAvailableAutoPilotTiersOptions()
);
this.autoPilotThroughput = ko.observable<number>(AutoPilotUtils.minAutoPilotThroughput); this.autoPilotThroughput = ko.observable<number>(AutoPilotUtils.minAutoPilotThroughput);
this.sharedAutoPilotThroughput = ko.observable<number>(AutoPilotUtils.minAutoPilotThroughput); this.sharedAutoPilotThroughput = ko.observable<number>(AutoPilotUtils.minAutoPilotThroughput);
this.autoPilotUsageCost = ko.pureComputed<string>(() => { this.autoPilotUsageCost = ko.pureComputed<string>(() => {
@@ -506,9 +492,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return ""; return "";
} }
const isDatabaseThroughput: boolean = this.databaseCreateNewShared(); const isDatabaseThroughput: boolean = this.databaseCreateNewShared();
return !this.hasAutoPilotV2FeatureFlag() return PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, isDatabaseThroughput);
? PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, isDatabaseThroughput)
: PricingUtils.getAutoPilotV2SpendHtml(autoPilot.autopilotTier, isDatabaseThroughput);
}); });
this.resetData(); this.resetData();
@@ -897,10 +881,9 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.resetData(); this.resetData();
this.container.refreshAllDatabases(); this.container.refreshAllDatabases();
}, },
(reason: any) => { (error: any) => {
this.isExecuting(false); this.isExecuting(false);
const message = ErrorParserUtility.parse(reason); const errorMessage: string = getErrorMessage(error);
const errorMessage = ErrorParserUtility.replaceKnownError(message[0].message);
this.formErrors(errorMessage); this.formErrors(errorMessage);
this.formErrorsDetails(errorMessage); this.formErrorsDetails(errorMessage);
const addCollectionPaneFailedMessage = { const addCollectionPaneFailedMessage = {
@@ -928,7 +911,8 @@ export default class AddCollectionPane extends ContextualPaneBase {
flight: this.container.flight() flight: this.container.flight()
}, },
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
error: reason error: errorMessage,
errorStack: getErrorStack(error)
}; };
TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey); TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey);
} }
@@ -942,13 +926,9 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.throughputSpendAck(false); this.throughputSpendAck(false);
this.isAutoPilotSelected(false); this.isAutoPilotSelected(false);
this.isSharedAutoPilotSelected(false); this.isSharedAutoPilotSelected(false);
if (!this.hasAutoPilotV2FeatureFlag()) { this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
} else {
this.selectedAutoPilotTier(undefined);
this.selectedSharedAutoPilotTier(undefined);
}
this.uniqueKeys([]); this.uniqueKeys([]);
this.useIndexingForSharedThroughput(true); this.useIndexingForSharedThroughput(true);
@@ -1027,17 +1007,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
if ((this.databaseCreateNewShared() && this.isSharedAutoPilotSelected()) || this.isAutoPilotSelected()) { if ((this.databaseCreateNewShared() && this.isSharedAutoPilotSelected()) || this.isAutoPilotSelected()) {
const autoPilot = this._getAutoPilot(); const autoPilot = this._getAutoPilot();
if ( if (
(!this.hasAutoPilotV2FeatureFlag() && !autoPilot ||
(!autoPilot || !autoPilot.maxThroughput ||
!autoPilot.maxThroughput || !AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput)
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput))) ||
(this.hasAutoPilotV2FeatureFlag() &&
(!autoPilot || !autoPilot.autopilotTier || !AutoPilotUtils.isValidAutoPilotTier(autoPilot.autopilotTier)))
) { ) {
this.formErrors( this.formErrors(
!this.hasAutoPilotV2FeatureFlag() `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
? `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
: "Please select an Autopilot tier from the list."
); );
return false; return false;
} }
@@ -1067,7 +1042,6 @@ export default class AddCollectionPane extends ContextualPaneBase {
const autoscaleThroughput = this.autoPilotThroughput() * 1; const autoscaleThroughput = this.autoPilotThroughput() * 1;
if ( if (
!this.hasAutoPilotV2FeatureFlag() &&
this.isAutoPilotSelected() && this.isAutoPilotSelected() &&
autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!this.throughputSpendAck() !this.throughputSpendAck()
@@ -1114,31 +1088,15 @@ export default class AddCollectionPane extends ContextualPaneBase {
} }
private _getAutoPilot(): DataModels.AutoPilotCreationSettings { private _getAutoPilot(): DataModels.AutoPilotCreationSettings {
if ( if (this.databaseCreateNewShared() && this.isSharedAutoPilotSelected() && this.sharedAutoPilotThroughput()) {
(!this.hasAutoPilotV2FeatureFlag() && return {
this.databaseCreateNewShared() && maxThroughput: this.sharedAutoPilotThroughput() * 1
this.isSharedAutoPilotSelected() && };
this.sharedAutoPilotThroughput()) ||
(this.hasAutoPilotV2FeatureFlag() &&
this.databaseCreateNewShared() &&
this.isSharedAutoPilotSelected() &&
this.selectedSharedAutoPilotTier())
) {
return !this.hasAutoPilotV2FeatureFlag()
? {
maxThroughput: this.sharedAutoPilotThroughput() * 1
}
: { autopilotTier: this.selectedSharedAutoPilotTier() };
} }
if ( if (this.isAutoPilotSelected() && this.autoPilotThroughput()) {
(!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.autoPilotThroughput()) || return {
(this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.selectedAutoPilotTier()) maxThroughput: this.autoPilotThroughput() * 1
) { };
return !this.hasAutoPilotV2FeatureFlag()
? {
maxThroughput: this.autoPilotThroughput() * 1
}
: { autopilotTier: this.selectedAutoPilotTier() };
} }
return undefined; return undefined;

View File

@@ -89,7 +89,6 @@
data-bind="text: databaseLevelThroughputTooltipText"></span> data-bind="text: databaseLevelThroughputTooltipText"></span>
</span> </span>
</div> </div>
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: databaseCreateNewShared"> <div data-bind="visible: databaseCreateNewShared">
<throughput-input-autopilot-v3 params="{ <throughput-input-autopilot-v3 params="{
step: 100, step: 100,
@@ -124,42 +123,6 @@
support</a> for more than <span data-bind="text: maxThroughputRUText"></span> RU/s.</p> support</a> for more than <span data-bind="text: maxThroughputRUText"></span> RU/s.</p>
</div> </div>
<!-- /ko --> <!-- /ko -->
<!-- ko if: hasAutoPilotV2FeatureFlag && hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: databaseCreateNewShared">
<throughput-input params="{
step: 100,
value: throughput,
testId: 'sharedThroughputValue',
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: databaseCreateNewShared,
label: throughputRangeText,
ariaLabel: throughputRangeText,
costsVisible: costsVisible,
requestUnitsUsageCost: requestUnitsUsageCost,
spendAckChecked: throughputSpendAck,
spendAckId: 'throughputSpendAckDatabase',
spendAckText: throughputSpendAckText,
spendAckVisible: throughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newDatabase-databaseThroughput-autoPilotRadio',
throughputProvisionedRadioId: 'newDatabase-databaseThroughput-manualRadio',
throughputModeRadioName: 'throughputModeRadioName',
isAutoPilotSelected: isAutoPilotSelected,
autoPilotTiersList: autoPilotTiersList,
selectedAutoPilotTier: selectedAutoPilotTier,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue
}">
</throughput-input>
<p data-bind="visible: canRequestSupport">
<!-- TODO: Replace link with call to the Azure Support blade --><a
href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request">Contact
support</a> for more than <span data-bind="text: maxThroughputRUText"></span> RU/s.</p>
</div>
<!-- /ko -->
<!-- /ko -->
<!-- Database provisioned throughput - End --> <!-- Database provisioned throughput - End -->
</div> </div>
</div> </div>

View File

@@ -1,7 +1,6 @@
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 ErrorParserUtility from "../../Common/ErrorParserUtility";
import * as ko from "knockout"; import * as ko from "knockout";
import * as PricingUtils from "../../Utils/PricingUtils"; import * as PricingUtils from "../../Utils/PricingUtils";
import * as SharedConstants from "../../Shared/Constants"; import * as SharedConstants from "../../Shared/Constants";
@@ -12,6 +11,7 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import { createDatabase } from "../../Common/dataAccess/createDatabase"; import { createDatabase } from "../../Common/dataAccess/createDatabase";
import { configContext, Platform } from "../../ConfigContext"; import { configContext, Platform } from "../../ConfigContext";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class AddDatabasePane extends ContextualPaneBase { export default class AddDatabasePane extends ContextualPaneBase {
public defaultExperience: ko.Computed<string>; public defaultExperience: ko.Computed<string>;
@@ -38,12 +38,9 @@ export default class AddDatabasePane extends ContextualPaneBase {
public upsellAnchorUrl: ko.PureComputed<string>; public upsellAnchorUrl: ko.PureComputed<string>;
public upsellAnchorText: ko.PureComputed<string>; public upsellAnchorText: ko.PureComputed<string>;
public isAutoPilotSelected: ko.Observable<boolean>; public isAutoPilotSelected: ko.Observable<boolean>;
public selectedAutoPilotTier: ko.Observable<DataModels.AutopilotTier>;
public autoPilotTiersList: ko.ObservableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>;
public maxAutoPilotThroughputSet: ko.Observable<number>; public maxAutoPilotThroughputSet: ko.Observable<number>;
public autoPilotUsageCost: ko.Computed<string>; public autoPilotUsageCost: ko.Computed<string>;
public canExceedMaximumValue: ko.PureComputed<boolean>; public canExceedMaximumValue: ko.PureComputed<boolean>;
public hasAutoPilotV2FeatureFlag: ko.PureComputed<boolean>;
public ruToolTipText: ko.Computed<string>; public ruToolTipText: ko.Computed<string>;
public isFreeTierAccount: ko.Computed<boolean>; public isFreeTierAccount: ko.Computed<boolean>;
public canConfigureThroughput: ko.PureComputed<boolean>; public canConfigureThroughput: ko.PureComputed<boolean>;
@@ -53,8 +50,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
super(options); super(options);
this.title((this.container && this.container.addDatabaseText()) || "New Database"); this.title((this.container && this.container.addDatabaseText()) || "New Database");
this.databaseId = ko.observable<string>(); this.databaseId = ko.observable<string>();
this.hasAutoPilotV2FeatureFlag = ko.pureComputed(() => this.container.hasAutoPilotV2FeatureFlag()); this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText());
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText(this.hasAutoPilotV2FeatureFlag()));
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled()); this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled()); this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled());
@@ -94,10 +90,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.minThroughputRU = ko.observable<number>(); this.minThroughputRU = ko.observable<number>();
this.throughputSpendAckText = ko.observable<string>(); this.throughputSpendAckText = ko.observable<string>();
this.throughputSpendAck = ko.observable<boolean>(false); this.throughputSpendAck = ko.observable<boolean>(false);
this.selectedAutoPilotTier = ko.observable<DataModels.AutopilotTier>();
this.autoPilotTiersList = ko.observableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>(
AutoPilotUtils.getAvailableAutoPilotTiersOptions()
);
this.isAutoPilotSelected = ko.observable<boolean>(false); this.isAutoPilotSelected = ko.observable<boolean>(false);
this.maxAutoPilotThroughputSet = ko.observable<number>(AutoPilotUtils.minAutoPilotThroughput); this.maxAutoPilotThroughputSet = ko.observable<number>(AutoPilotUtils.minAutoPilotThroughput);
this.autoPilotUsageCost = ko.pureComputed<string>(() => { this.autoPilotUsageCost = ko.pureComputed<string>(() => {
@@ -105,13 +97,11 @@ export default class AddDatabasePane extends ContextualPaneBase {
if (!autoPilot) { if (!autoPilot) {
return ""; return "";
} }
return !this.hasAutoPilotV2FeatureFlag() return PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, true /* isDatabaseThroughput */);
? PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, true /* isDatabaseThroughput */)
: PricingUtils.getAutoPilotV2SpendHtml(autoPilot.autopilotTier, true /* isDatabaseThroughput */);
}); });
this.throughputRangeText = ko.pureComputed<string>(() => { this.throughputRangeText = ko.pureComputed<string>(() => {
if (this.isAutoPilotSelected()) { if (this.isAutoPilotSelected()) {
return AutoPilotUtils.getAutoPilotHeaderText(this.hasAutoPilotV2FeatureFlag()); return AutoPilotUtils.getAutoPilotHeaderText();
} }
return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`; return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`;
}); });
@@ -208,7 +198,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => { this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {
const autoscaleThroughput = this.maxAutoPilotThroughputSet() * 1; const autoscaleThroughput = this.maxAutoPilotThroughputSet() * 1;
if (!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected()) { if (this.isAutoPilotSelected()) {
return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
} }
@@ -306,18 +296,22 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.isExecuting(true); this.isExecuting(true);
const createDatabaseParams: DataModels.CreateDatabaseParams = { const createDatabaseParams: DataModels.CreateDatabaseParams = {
autoPilotMaxThroughput: this.maxAutoPilotThroughputSet(),
databaseId: addDatabasePaneStartMessage.database.id, databaseId: addDatabasePaneStartMessage.database.id,
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared, databaseLevelThroughput: addDatabasePaneStartMessage.database.shared
offerThroughput: addDatabasePaneStartMessage.offerThroughput
}; };
if (this.isAutoPilotSelected()) {
createDatabaseParams.autoPilotMaxThroughput = this.maxAutoPilotThroughputSet();
} else {
createDatabaseParams.offerThroughput = addDatabasePaneStartMessage.offerThroughput;
}
createDatabase(createDatabaseParams).then( createDatabase(createDatabaseParams).then(
(database: DataModels.Database) => { (database: DataModels.Database) => {
this._onCreateDatabaseSuccess(offerThroughput, startKey); this._onCreateDatabaseSuccess(offerThroughput, startKey);
}, },
(reason: any) => { (error: any) => {
this._onCreateDatabaseFailure(reason, offerThroughput, reason); this._onCreateDatabaseFailure(error, offerThroughput, startKey);
} }
); );
} }
@@ -325,7 +319,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
public resetData() { public resetData() {
this.databaseId(""); this.databaseId("");
this.databaseCreateNewShared(this.getSharedThroughputDefault()); this.databaseCreateNewShared(this.getSharedThroughputDefault());
this.selectedAutoPilotTier(undefined);
this.isAutoPilotSelected(false); this.isAutoPilotSelected(false);
this.maxAutoPilotThroughputSet(AutoPilotUtils.minAutoPilotThroughput); this.maxAutoPilotThroughputSet(AutoPilotUtils.minAutoPilotThroughput);
this._updateThroughputLimitByDatabase(); this._updateThroughputLimitByDatabase();
@@ -367,11 +360,11 @@ export default class AddDatabasePane extends ContextualPaneBase {
this.resetData(); this.resetData();
} }
private _onCreateDatabaseFailure(reason: any, offerThroughput: number, startKey: number): void { private _onCreateDatabaseFailure(error: any, offerThroughput: number, startKey: number): void {
this.isExecuting(false); this.isExecuting(false);
const message = ErrorParserUtility.parse(reason); const errorMessage = getErrorMessage(error);
this.formErrors(message[0].message); this.formErrors(errorMessage);
this.formErrorsDetails(message[0].message); this.formErrorsDetails(errorMessage);
const addDatabasePaneFailedMessage = { const addDatabasePaneFailedMessage = {
databaseAccountName: this.container.databaseAccount().name, databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(), defaultExperience: this.container.defaultExperience(),
@@ -386,7 +379,8 @@ export default class AddDatabasePane extends ContextualPaneBase {
flight: this.container.flight() flight: this.container.flight()
}, },
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
error: reason error: errorMessage,
errorStack: getErrorStack(error)
}; };
TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey); TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey);
} }
@@ -413,17 +407,12 @@ export default class AddDatabasePane extends ContextualPaneBase {
if (this.isAutoPilotSelected()) { if (this.isAutoPilotSelected()) {
const autoPilot = this._isAutoPilotSelectedAndWhatTier(); const autoPilot = this._isAutoPilotSelectedAndWhatTier();
if ( if (
(!this.hasAutoPilotV2FeatureFlag() && !autoPilot ||
(!autoPilot || !autoPilot.maxThroughput ||
!autoPilot.maxThroughput || !AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput)
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput))) ||
(this.hasAutoPilotV2FeatureFlag() &&
(!autoPilot || !autoPilot.autopilotTier || !AutoPilotUtils.isValidAutoPilotTier(autoPilot.autopilotTier)))
) { ) {
this.formErrors( this.formErrors(
!this.hasAutoPilotV2FeatureFlag() `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
? `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
: "Please select an Autopilot tier from the list."
); );
return false; return false;
} }
@@ -438,7 +427,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
const autoscaleThroughput = this.maxAutoPilotThroughputSet() * 1; const autoscaleThroughput = this.maxAutoPilotThroughputSet() * 1;
if ( if (
!this.hasAutoPilotV2FeatureFlag() &&
this.isAutoPilotSelected() && this.isAutoPilotSelected() &&
autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!this.throughputSpendAck() !this.throughputSpendAck()
@@ -451,15 +439,10 @@ export default class AddDatabasePane extends ContextualPaneBase {
} }
private _isAutoPilotSelectedAndWhatTier(): DataModels.AutoPilotCreationSettings { private _isAutoPilotSelectedAndWhatTier(): DataModels.AutoPilotCreationSettings {
if ( if (this.isAutoPilotSelected() && this.maxAutoPilotThroughputSet()) {
(!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.maxAutoPilotThroughputSet()) || return {
(this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.selectedAutoPilotTier()) maxThroughput: this.maxAutoPilotThroughputSet() * 1
) { };
return !this.hasAutoPilotV2FeatureFlag()
? {
maxThroughput: this.maxAutoPilotThroughputSet() * 1
}
: { autopilotTier: this.selectedAutoPilotTier() };
} }
return undefined; return undefined;
} }

View File

@@ -7,6 +7,7 @@ import * as Logger from "../../Common/Logger";
import { QueriesGridComponentAdapter } from "../Controls/QueriesGridReactComponent/QueriesGridComponentAdapter"; import { QueriesGridComponentAdapter } from "../Controls/QueriesGridReactComponent/QueriesGridComponentAdapter";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import QueryTab from "../Tabs/QueryTab"; import QueryTab from "../Tabs/QueryTab";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export class BrowseQueriesPane extends ContextualPaneBase { export class BrowseQueriesPane extends ContextualPaneBase {
public queriesGridComponentAdapter: QueriesGridComponentAdapter; public queriesGridComponentAdapter: QueriesGridComponentAdapter;
@@ -60,17 +61,20 @@ export class BrowseQueriesPane extends ContextualPaneBase {
startKey startKey
); );
} catch (error) { } catch (error) {
const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.SetupSavedQueries, Action.SetupSavedQueries,
{ {
databaseAccountName: this.container && this.container.databaseAccount().name, databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(), defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane, dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title() paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error)
}, },
startKey startKey
); );
this.formErrors(`Failed to setup a collection for saved queries: ${error.message}`); this.formErrors(`Failed to setup a collection for saved queries: ${errorMessage}`);
} finally { } finally {
this.isExecuting(false); this.isExecuting(false);
} }

View File

@@ -142,7 +142,6 @@
</span> </span>
</div> </div>
<!-- 1 --> <!-- 1 -->
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: keyspaceCreateNew() && keyspaceHasSharedOffer()"> <div data-bind="visible: keyspaceCreateNew() && keyspaceHasSharedOffer()">
<throughput-input-autopilot-v3 <throughput-input-autopilot-v3
params="{ params="{
@@ -173,38 +172,6 @@
</throughput-input-autopilot-v3> </throughput-input-autopilot-v3>
</div> </div>
<!-- /ko --> <!-- /ko -->
<!-- ko if: hasAutoPilotV2FeatureFlag && hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: keyspaceCreateNew() && keyspaceHasSharedOffer()">
<throughput-input
params="{
testId: 'cassandraThroughputValue-v2-shared',
value: keyspaceThroughput,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: keyspaceCreateNew() && keyspaceHasSharedOffer(),
label: sharedThroughputRangeText,
ariaLabel: sharedThroughputRangeText,
requestUnitsUsageCost: requestUnitsUsageCostShared,
spendAckChecked: sharedThroughputSpendAck,
spendAckId: 'sharedThroughputSpendAck-v2-shared',
spendAckText: sharedThroughputSpendAckText,
spendAckVisible: sharedThroughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newKeyspace-databaseThroughput-autoPilotRadio-v2-shared',
throughputProvisionedRadioId: 'newKeyspace-databaseThroughput-manualRadio-v2-shared',
isAutoPilotSelected: isSharedAutoPilotSelected,
autoPilotTiersList: sharedAutoPilotTiersList,
costsVisible: costsVisible,
selectedAutoPilotTier: selectedSharedAutoPilotTier,
autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue
}"
>
</throughput-input>
</div>
<!-- /ko -->
<!-- /ko -->
<!-- Database provisioned throughput - End --> <!-- Database provisioned throughput - End -->
</div> </div>
<div class="seconddivpadding"> <div class="seconddivpadding">
@@ -257,7 +224,6 @@
</span> </span>
</div> </div>
<!-- 2 --> <!-- 2 -->
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: !keyspaceHasSharedOffer() || dedicateTableThroughput()"> <div data-bind="visible: !keyspaceHasSharedOffer() || dedicateTableThroughput()">
<throughput-input-autopilot-v3 <throughput-input-autopilot-v3
params="{ params="{
@@ -289,40 +255,6 @@
</throughput-input-autopilot-v3> </throughput-input-autopilot-v3>
</div> </div>
<!-- /ko --> <!-- /ko -->
<!-- ko if: hasAutoPilotV2FeatureFlag && hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: !keyspaceHasSharedOffer() || dedicateTableThroughput()">
<throughput-input
params="{
testId: 'cassandraSharedThroughputValue-v2-dedicated',
value: throughput,
minimum: minThroughputRU,
maximum: maxThroughputRU,
isEnabled: !keyspaceHasSharedOffer() || dedicateTableThroughput(),
label: throughputRangeText,
ariaLabel: throughputRangeText,
costsVisible: costsVisible,
requestUnitsUsageCost: requestUnitsUsageCostDedicated,
spendAckChecked: throughputSpendAck,
spendAckId: 'throughputSpendAckCassandra-v2-dedicated',
spendAckText: throughputSpendAckText,
spendAckVisible: throughputSpendAckVisible,
showAsMandatory: true,
infoBubbleText: ruToolTipText,
throughputAutoPilotRadioId: 'newKeyspace-containerThroughput-autoPilotRadio-v2-dedicated',
throughputProvisionedRadioId: 'newKeyspace-containerThroughput-manualRadio-v2-dedicated',
isAutoPilotSelected: isAutoPilotSelected,
autoPilotTiersList: autoPilotTiersList,
selectedAutoPilotTier: selectedAutoPilotTier,
autoPilotUsageCost: autoPilotUsageCost,
showAutoPilot: false,
canExceedMaximumValue: canExceedMaximumValue
}"
>
</throughput-input>
</div>
<!-- /ko -->
<!-- /ko -->
<!-- Provision table throughput - end --> <!-- Provision table throughput - end -->
</div> </div>
<div class="paneFooter"> <div class="paneFooter">

View File

@@ -13,6 +13,7 @@ import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import { HashMap } from "../../Common/HashMap"; import { HashMap } from "../../Common/HashMap";
import { configContext, Platform } from "../../ConfigContext"; import { configContext, Platform } from "../../ConfigContext";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class CassandraAddCollectionPane extends ContextualPaneBase { export default class CassandraAddCollectionPane extends ContextualPaneBase {
public createTableQuery: ko.Observable<string>; public createTableQuery: ko.Observable<string>;
@@ -38,10 +39,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
public sharedThroughputSpendAck: ko.Observable<boolean>; public sharedThroughputSpendAck: ko.Observable<boolean>;
public sharedThroughputSpendAckText: ko.Observable<string>; public sharedThroughputSpendAckText: ko.Observable<string>;
public isAutoPilotSelected: ko.Observable<boolean>; public isAutoPilotSelected: ko.Observable<boolean>;
public selectedAutoPilotTier: ko.Observable<DataModels.AutopilotTier>;
public selectedSharedAutoPilotTier: ko.Observable<DataModels.AutopilotTier>;
public autoPilotTiersList: ko.ObservableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>;
public sharedAutoPilotTiersList: ko.ObservableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>;
public isSharedAutoPilotSelected: ko.Observable<boolean>; public isSharedAutoPilotSelected: ko.Observable<boolean>;
public selectedAutoPilotThroughput: ko.Observable<number>; public selectedAutoPilotThroughput: ko.Observable<number>;
public sharedAutoPilotThroughput: ko.Observable<number>; public sharedAutoPilotThroughput: ko.Observable<number>;
@@ -49,7 +46,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
public sharedThroughputSpendAckVisible: ko.Computed<boolean>; public sharedThroughputSpendAckVisible: ko.Computed<boolean>;
public throughputSpendAckVisible: ko.Computed<boolean>; public throughputSpendAckVisible: ko.Computed<boolean>;
public canExceedMaximumValue: ko.PureComputed<boolean>; public canExceedMaximumValue: ko.PureComputed<boolean>;
public hasAutoPilotV2FeatureFlag: ko.PureComputed<boolean>;
public isFreeTierAccount: ko.Computed<boolean>; public isFreeTierAccount: ko.Computed<boolean>;
public ruToolTipText: ko.Computed<string>; public ruToolTipText: ko.Computed<string>;
public canConfigureThroughput: ko.PureComputed<boolean>; public canConfigureThroughput: ko.PureComputed<boolean>;
@@ -61,8 +57,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
this.title("Add Table"); this.title("Add Table");
this.createTableQuery = ko.observable<string>("CREATE TABLE "); this.createTableQuery = ko.observable<string>("CREATE TABLE ");
this.keyspaceCreateNew = ko.observable<boolean>(true); this.keyspaceCreateNew = ko.observable<boolean>(true);
this.hasAutoPilotV2FeatureFlag = ko.pureComputed(() => this.container.hasAutoPilotV2FeatureFlag()); this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText());
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText(this.hasAutoPilotV2FeatureFlag()));
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled()); this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.keyspaceOffers = new HashMap<DataModels.Offer>(); this.keyspaceOffers = new HashMap<DataModels.Offer>();
this.keyspaceIds = ko.observableArray<string>(); this.keyspaceIds = ko.observableArray<string>();
@@ -90,8 +85,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
}); });
this.tableId = ko.observable<string>(""); this.tableId = ko.observable<string>("");
this.selectedAutoPilotTier = ko.observable<DataModels.AutopilotTier>();
this.selectedSharedAutoPilotTier = ko.observable<DataModels.AutopilotTier>();
this.isAutoPilotSelected = ko.observable<boolean>(false); this.isAutoPilotSelected = ko.observable<boolean>(false);
this.isSharedAutoPilotSelected = ko.observable<boolean>(false); this.isSharedAutoPilotSelected = ko.observable<boolean>(false);
this.selectedAutoPilotThroughput = ko.observable<number>(); this.selectedAutoPilotThroughput = ko.observable<number>();
@@ -102,11 +95,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
if (!enableAutoPilot) { if (!enableAutoPilot) {
return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`; return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`;
} }
return AutoPilotUtils.getAutoPilotHeaderText(this.hasAutoPilotV2FeatureFlag()); return AutoPilotUtils.getAutoPilotHeaderText();
}); });
this.sharedThroughputRangeText = ko.pureComputed<string>(() => { this.sharedThroughputRangeText = ko.pureComputed<string>(() => {
if (this.isSharedAutoPilotSelected()) { if (this.isSharedAutoPilotSelected()) {
return AutoPilotUtils.getAutoPilotHeaderText(this.hasAutoPilotV2FeatureFlag()); return AutoPilotUtils.getAutoPilotHeaderText();
} }
return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`; return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`;
}); });
@@ -246,7 +239,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
this.sharedThroughputSpendAckVisible = ko.computed<boolean>(() => { this.sharedThroughputSpendAckVisible = ko.computed<boolean>(() => {
const autoscaleThroughput = this.sharedAutoPilotThroughput() * 1; const autoscaleThroughput = this.sharedAutoPilotThroughput() * 1;
if (!this.hasAutoPilotV2FeatureFlag() && this.isSharedAutoPilotSelected()) { if (this.isSharedAutoPilotSelected()) {
return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
} }
@@ -255,7 +248,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => { this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {
const autoscaleThroughput = this.selectedAutoPilotThroughput() * 1; const autoscaleThroughput = this.selectedAutoPilotThroughput() * 1;
if (!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected()) { if (this.isAutoPilotSelected()) {
return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K; return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
} }
@@ -280,22 +273,13 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
updateKeyspaceIds(this.container.nonSystemDatabases()); updateKeyspaceIds(this.container.nonSystemDatabases());
} }
this.autoPilotTiersList = ko.observableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>(
AutoPilotUtils.getAvailableAutoPilotTiersOptions()
);
this.sharedAutoPilotTiersList = ko.observableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>(
AutoPilotUtils.getAvailableAutoPilotTiersOptions()
);
this.autoPilotUsageCost = ko.pureComputed<string>(() => { this.autoPilotUsageCost = ko.pureComputed<string>(() => {
const autoPilot = this._getAutoPilot(); const autoPilot = this._getAutoPilot();
if (!autoPilot) { if (!autoPilot) {
return ""; return "";
} }
const isDatabaseThroughput: boolean = this.keyspaceCreateNew(); const isDatabaseThroughput: boolean = this.keyspaceCreateNew();
return !this.hasAutoPilotV2FeatureFlag() return PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, isDatabaseThroughput);
? PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, isDatabaseThroughput)
: PricingUtils.getAutoPilotV2SpendHtml(autoPilot.autopilotTier, isDatabaseThroughput);
}); });
} }
@@ -352,15 +336,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
const autoPilotCommand = `cosmosdb_autoscale_max_throughput`; const autoPilotCommand = `cosmosdb_autoscale_max_throughput`;
let createTableAndKeyspacePromise: Q.Promise<any>; let createTableAndKeyspacePromise: Q.Promise<any>;
const toCreateKeyspace: boolean = this.keyspaceCreateNew(); const toCreateKeyspace: boolean = this.keyspaceCreateNew();
const useAutoPilotForKeyspace: boolean = const useAutoPilotForKeyspace: boolean = this.isSharedAutoPilotSelected() && !!this.sharedAutoPilotThroughput();
(!this.hasAutoPilotV2FeatureFlag() && this.isSharedAutoPilotSelected() && !!this.sharedAutoPilotThroughput()) ||
(this.hasAutoPilotV2FeatureFlag() && this.isSharedAutoPilotSelected() && !!this.selectedSharedAutoPilotTier());
const createKeyspaceQueryPrefix: string = `CREATE KEYSPACE ${this.keyspaceId().trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }`; const createKeyspaceQueryPrefix: string = `CREATE KEYSPACE ${this.keyspaceId().trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 }`;
const createKeyspaceQuery: string = this.keyspaceHasSharedOffer() const createKeyspaceQuery: string = this.keyspaceHasSharedOffer()
? useAutoPilotForKeyspace ? useAutoPilotForKeyspace
? !this.hasAutoPilotV2FeatureFlag() ? `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${this.sharedAutoPilotThroughput()};`
? `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${this.sharedAutoPilotThroughput()};`
: `${createKeyspaceQueryPrefix} AND ${autoPilotCommand}=${this.selectedSharedAutoPilotTier()};`
: `${createKeyspaceQueryPrefix} AND cosmosdb_provisioned_throughput=${this.keyspaceThroughput()};` : `${createKeyspaceQueryPrefix} AND cosmosdb_provisioned_throughput=${this.keyspaceThroughput()};`
: `${createKeyspaceQueryPrefix};`; : `${createKeyspaceQueryPrefix};`;
const createTableQueryPrefix: string = `${this.createTableQuery()}${this.tableId().trim()} ${this.userTableQuery()}`; const createTableQueryPrefix: string = `${this.createTableQuery()}${this.tableId().trim()} ${this.userTableQuery()}`;
@@ -450,8 +430,9 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
}; };
TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneSuccessMessage, startKey); TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneSuccessMessage, startKey);
}, },
reason => { error => {
this.formErrors(reason.exceptionMessage); const errorMessage = getErrorMessage(error);
this.formErrors(errorMessage);
this.isExecuting(false); this.isExecuting(false);
const addCollectionPaneFailedMessage = { const addCollectionPaneFailedMessage = {
databaseAccountName: this.container.databaseAccount().name, databaseAccountName: this.container.databaseAccount().name,
@@ -477,7 +458,8 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
toCreateKeyspace: toCreateKeyspace, toCreateKeyspace: toCreateKeyspace,
createKeyspaceQuery: createKeyspaceQuery, createKeyspaceQuery: createKeyspaceQuery,
createTableQuery: createTableQuery, createTableQuery: createTableQuery,
error: reason error: errorMessage,
errorStack: getErrorStack(error)
}; };
TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey); TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey);
} }
@@ -489,8 +471,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
const throughputDefaults = this.container.collectionCreationDefaults.throughput; const throughputDefaults = this.container.collectionCreationDefaults.throughput;
this.isAutoPilotSelected(false); this.isAutoPilotSelected(false);
this.isSharedAutoPilotSelected(false); this.isSharedAutoPilotSelected(false);
this.selectedAutoPilotTier(null);
this.selectedSharedAutoPilotTier(null);
this.selectedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.selectedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput); this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.throughput(AddCollectionUtility.getMaxThroughput(this.container.collectionCreationDefaults, this.container)); this.throughput(AddCollectionUtility.getMaxThroughput(this.container.collectionCreationDefaults, this.container));
@@ -512,7 +492,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
const sharedAutoscaleThroughput = this.sharedAutoPilotThroughput() * 1; const sharedAutoscaleThroughput = this.sharedAutoPilotThroughput() * 1;
if ( if (
!this.hasAutoPilotV2FeatureFlag() &&
this.isSharedAutoPilotSelected() && this.isSharedAutoPilotSelected() &&
sharedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && sharedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!this.sharedThroughputSpendAck() !this.sharedThroughputSpendAck()
@@ -523,7 +502,6 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
const dedicatedAutoscaleThroughput = this.selectedAutoPilotThroughput() * 1; const dedicatedAutoscaleThroughput = this.selectedAutoPilotThroughput() * 1;
if ( if (
!this.hasAutoPilotV2FeatureFlag() &&
this.isAutoPilotSelected() && this.isAutoPilotSelected() &&
dedicatedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && dedicatedAutoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!this.throughputSpendAck() !this.throughputSpendAck()
@@ -538,17 +516,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
) { ) {
const autoPilot = this._getAutoPilot(); const autoPilot = this._getAutoPilot();
if ( if (
(!this.hasAutoPilotV2FeatureFlag() && !autoPilot ||
(!autoPilot || !autoPilot.maxThroughput ||
!autoPilot.maxThroughput || !AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput)
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput))) ||
(this.hasAutoPilotV2FeatureFlag() &&
(!autoPilot || !autoPilot.autopilotTier || !AutoPilotUtils.isValidAutoPilotTier(autoPilot.autopilotTier)))
) { ) {
this.formErrors( this.formErrors(
!this.hasAutoPilotV2FeatureFlag() `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
? `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
: "Please select an Autopilot tier from the list."
); );
return false; return false;
} }
@@ -575,33 +548,20 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
private _getAutoPilot(): DataModels.AutoPilotCreationSettings { private _getAutoPilot(): DataModels.AutoPilotCreationSettings {
if ( if (
(!this.hasAutoPilotV2FeatureFlag() && this.keyspaceCreateNew() &&
this.keyspaceCreateNew() && this.keyspaceHasSharedOffer() &&
this.keyspaceHasSharedOffer() && this.isSharedAutoPilotSelected() &&
this.isSharedAutoPilotSelected() && this.sharedAutoPilotThroughput()
this.sharedAutoPilotThroughput()) ||
(this.hasAutoPilotV2FeatureFlag() &&
this.keyspaceCreateNew() &&
this.keyspaceHasSharedOffer() &&
this.isSharedAutoPilotSelected() &&
this.selectedSharedAutoPilotTier())
) { ) {
return !this.hasAutoPilotV2FeatureFlag() return {
? { maxThroughput: this.sharedAutoPilotThroughput() * 1
maxThroughput: this.sharedAutoPilotThroughput() * 1 };
}
: { autopilotTier: this.selectedSharedAutoPilotTier() };
} }
if ( if (this.selectedAutoPilotThroughput()) {
(!this.hasAutoPilotV2FeatureFlag() && this.selectedAutoPilotThroughput()) || return {
(this.hasAutoPilotV2FeatureFlag() && this.selectedAutoPilotTier()) maxThroughput: this.selectedAutoPilotThroughput() * 1
) { };
return !this.hasAutoPilotV2FeatureFlag()
? {
maxThroughput: this.selectedAutoPilotThroughput() * 1
}
: { autopilotTier: this.selectedAutoPilotTier() };
} }
return undefined; return undefined;

View File

@@ -1,7 +1,6 @@
import ko from "knockout"; import ko from "knockout";
import * as React from "react"; import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import * as Logger from "../../Common/Logger";
import { JunoClient, IPinnedRepo } from "../../Juno/JunoClient"; import { JunoClient, IPinnedRepo } from "../../Juno/JunoClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
@@ -13,6 +12,7 @@ import { HttpStatusCodes } from "../../Common/Constants";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem"; import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils";
interface Location { interface Location {
type: "MyNotebooks" | "GitHub"; type: "MyNotebooks" | "GitHub";
@@ -90,9 +90,7 @@ export class CopyNotebookPaneAdapter implements ReactAdapter {
if (this.gitHubOAuthService.isLoggedIn()) { if (this.gitHubOAuthService.isLoggedIn()) {
const response = await this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope); const response = await this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
const message = `Received HTTP ${response.status} when fetching pinned repos`; handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit");
Logger.logError(message, "CopyNotebookPaneAdapter/submit");
NotificationConsoleUtils.logConsoleError(message);
} }
if (response.data?.length > 0) { if (response.data?.length > 0) {
@@ -134,12 +132,10 @@ export class CopyNotebookPaneAdapter implements ReactAdapter {
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${this.name} to ${destination}`); NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${this.name} to ${destination}`);
} catch (error) { } catch (error) {
const errorMessage = getErrorMessage(error);
this.formError = `Failed to copy ${this.name} to ${destination}`; this.formError = `Failed to copy ${this.name} to ${destination}`;
this.formErrorDetail = `${error}`; this.formErrorDetail = `${errorMessage}`;
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", this.formError);
const message = `${this.formError}: ${this.formErrorDetail}`;
Logger.logError(message, "CopyNotebookPaneAdapter/submit");
NotificationConsoleUtils.logConsoleError(message);
return; return;
} finally { } finally {
clearMessage && clearMessage(); clearMessage && clearMessage();

View File

@@ -3,7 +3,6 @@ import Q from "q";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
import { CassandraAPIDataClient } from "../Tables/TableDataClient"; import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
@@ -12,6 +11,7 @@ import DeleteFeedback from "../../Common/DeleteFeedback";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { deleteCollection } from "../../Common/dataAccess/deleteCollection"; import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class DeleteCollectionConfirmationPane extends ContextualPaneBase { export default class DeleteCollectionConfirmationPane extends ContextualPaneBase {
public collectionIdConfirmationText: ko.Observable<string>; public collectionIdConfirmationText: ko.Observable<string>;
@@ -99,11 +99,11 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase
this.containerDeleteFeedback(""); this.containerDeleteFeedback("");
} }
}, },
(reason: any) => { (error: any) => {
this.isExecuting(false); this.isExecuting(false);
const message = ErrorParserUtility.parse(reason); const errorMessage = getErrorMessage(error);
this.formErrors(message[0].message); this.formErrors(errorMessage);
this.formErrorsDetails(message[0].message); this.formErrorsDetails(errorMessage);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.DeleteCollection, Action.DeleteCollection,
{ {
@@ -111,7 +111,9 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase
defaultExperience: this.container.defaultExperience(), defaultExperience: this.container.defaultExperience(),
collectionId: selectedCollection.id(), collectionId: selectedCollection.id(),
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title() paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error)
}, },
startKey startKey
); );

View File

@@ -3,7 +3,6 @@ import Q from "q";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
import { CassandraAPIDataClient } from "../Tables/TableDataClient"; import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
@@ -14,6 +13,7 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase"; import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
import { ARMError } from "../../Utils/arm/request"; import { ARMError } from "../../Utils/arm/request";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase { export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
public databaseIdConfirmationText: ko.Observable<string>; public databaseIdConfirmationText: ko.Observable<string>;
@@ -108,12 +108,11 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
this.databaseDeleteFeedback(""); this.databaseDeleteFeedback("");
} }
}, },
(reason: unknown) => { (error: any) => {
this.isExecuting(false); this.isExecuting(false);
const errorMessage = getErrorMessage(error);
const message = reason instanceof ARMError ? reason.message : ErrorParserUtility.parse(reason)[0].message; this.formErrors(errorMessage);
this.formErrors(message); this.formErrorsDetails(errorMessage);
this.formErrorsDetails(message);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.DeleteDatabase, Action.DeleteDatabase,
{ {
@@ -121,7 +120,9 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
defaultExperience: this.container.defaultExperience(), defaultExperience: this.container.defaultExperience(),
databaseId: selectedDatabase.id(), databaseId: selectedDatabase.id(),
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title() paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error)
}, },
startKey startKey
); );

View File

@@ -1,6 +1,5 @@
import _ from "underscore"; import _ from "underscore";
import { Areas, HttpStatusCodes } from "../../Common/Constants"; import { Areas, HttpStatusCodes } from "../../Common/Constants";
import * as Logger from "../../Common/Logger";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { GitHubClient, IGitHubPageInfo, IGitHubRepo } from "../../GitHub/GitHubClient"; import { GitHubClient, IGitHubPageInfo, IGitHubRepo } from "../../GitHub/GitHubClient";
import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient"; import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient";
@@ -8,13 +7,12 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import { JunoUtils } from "../../Utils/JunoUtils"; import { JunoUtils } from "../../Utils/JunoUtils";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent"; import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent";
import { GitHubReposComponent, GitHubReposComponentProps, RepoListItem } from "../Controls/GitHub/GitHubReposComponent"; import { GitHubReposComponent, GitHubReposComponentProps, RepoListItem } from "../Controls/GitHub/GitHubReposComponent";
import { GitHubReposComponentAdapter } from "../Controls/GitHub/GitHubReposComponentAdapter"; import { GitHubReposComponentAdapter } from "../Controls/GitHub/GitHubReposComponentAdapter";
import { BranchesProps, PinnedReposProps, UnpinnedReposProps } from "../Controls/GitHub/ReposListComponent"; import { BranchesProps, PinnedReposProps, UnpinnedReposProps } from "../Controls/GitHub/ReposListComponent";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import { handleError } from "../../Common/ErrorHandlingUtils";
interface GitHubReposPaneOptions extends ViewModels.PaneOptions { interface GitHubReposPaneOptions extends ViewModels.PaneOptions {
gitHubClient: GitHubClient; gitHubClient: GitHubClient;
@@ -105,9 +103,7 @@ export class GitHubReposPane extends ContextualPaneBase {
throw new Error(`Received HTTP ${response.status} when saving pinned repos`); throw new Error(`Received HTTP ${response.status} when saving pinned repos`);
} }
} catch (error) { } catch (error) {
const message = `Failed to save pinned repos: ${error}`; handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos");
Logger.logError(message, "GitHubReposPane/submit");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
} }
} }
} }
@@ -206,9 +202,7 @@ export class GitHubReposPane extends ContextualPaneBase {
branchesProps.lastPageInfo = response.pageInfo; branchesProps.lastPageInfo = response.pageInfo;
} }
} catch (error) { } catch (error) {
const message = `Failed to fetch branches: ${error}`; handleError(error, "GitHubReposPane/loadMoreBranches", "Failed to fetch branches");
Logger.logError(message, "GitHubReposPane/loadMoreBranches");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
} }
branchesProps.isLoading = false; branchesProps.isLoading = false;
@@ -236,9 +230,7 @@ export class GitHubReposPane extends ContextualPaneBase {
this.unpinnedReposProps.repos = this.calculateUnpinnedRepos(); this.unpinnedReposProps.repos = this.calculateUnpinnedRepos();
} }
} catch (error) { } catch (error) {
const message = `Failed to fetch unpinned repos: ${error}`; handleError(error, "GitHubReposPane/loadMoreUnpinnedRepos", "Failed to fetch unpinned repos");
Logger.logError(message, "GitHubReposPane/loadMoreUnpinnedRepos");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
} }
this.unpinnedReposProps.isLoading = false; this.unpinnedReposProps.isLoading = false;
@@ -255,9 +247,7 @@ export class GitHubReposPane extends ContextualPaneBase {
return response.data; return response.data;
} catch (error) { } catch (error) {
const message = `Failed to fetch repo: ${error}`; handleError(error, "GitHubReposPane/getRepo", "Failed to fetch repo");
Logger.logError(message, "GitHubReposPane/getRepo");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }
} }
@@ -320,9 +310,7 @@ export class GitHubReposPane extends ContextualPaneBase {
this.triggerRender(); this.triggerRender();
} }
} catch (error) { } catch (error) {
const message = `Failed to fetch pinned repos: ${error}`; handleError(error, "GitHubReposPane/refreshPinnedReposListItems", "Failed to fetch pinned repos");
Logger.logError(message, "GitHubReposPane/refreshPinnedReposListItems");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
} }
} }

View File

@@ -1,17 +1,16 @@
import ko from "knockout"; import ko from "knockout";
import * as React from "react"; import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import * as Logger from "../../Common/Logger";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { JunoClient } from "../../Juno/JunoClient"; import { JunoClient } from "../../Juno/JunoClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent"; import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent"; import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
import { ImmutableNotebook } from "@nteract/commutable/src"; import { ImmutableNotebook } from "@nteract/commutable/src";
import { toJS } from "@nteract/commutable"; import { toJS } from "@nteract/commutable";
import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent"; import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent";
import { HttpStatusCodes } from "../../Common/Constants"; import { HttpStatusCodes } from "../../Common/Constants";
import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils";
export class PublishNotebookPaneAdapter implements ReactAdapter { export class PublishNotebookPaneAdapter implements ReactAdapter {
parameters: ko.Observable<number>; parameters: ko.Observable<number>;
@@ -111,9 +110,11 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
this.isCodeOfConductAccepted = response.data; this.isCodeOfConductAccepted = response.data;
} catch (error) { } catch (error) {
const message = `Failed to check if code of conduct was accepted: ${error}`; handleError(
Logger.logError(message, "PublishNotebookPaneAdapter/isCodeOfConductAccepted"); error,
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message); "PublishNotebookPaneAdapter/isCodeOfConductAccepted",
"Failed to check if code of conduct was accepted"
);
} }
} else { } else {
this.isCodeOfConductAccepted = true; this.isCodeOfConductAccepted = true;
@@ -170,12 +171,10 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
} }
} }
} catch (error) { } catch (error) {
const errorMessage = getErrorMessage(error);
this.formError = `Failed to publish ${this.name} to gallery`; this.formError = `Failed to publish ${this.name} to gallery`;
this.formErrorDetail = `${error}`; this.formErrorDetail = `${errorMessage}`;
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", this.formError);
const message = `${this.formError}: ${this.formErrorDetail}`;
Logger.logError(message, "PublishNotebookPaneAdapter/submit");
NotificationConsoleUtils.logConsoleError(message);
return; return;
} finally { } finally {
clearPublishingMessage(); clearPublishingMessage();
@@ -189,10 +188,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
private createFormErrorForLargeImageSelection = (formError: string, formErrorDetail: string, area: string): void => { private createFormErrorForLargeImageSelection = (formError: string, formErrorDetail: string, area: string): void => {
this.formError = formError; this.formError = formError;
this.formErrorDetail = formErrorDetail; this.formErrorDetail = formErrorDetail;
handleError(formErrorDetail, area, formError);
const message = `${this.formError}: ${this.formErrorDetail}`;
Logger.logError(message, area);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
this.triggerRender(); this.triggerRender();
}; };

View File

@@ -7,6 +7,7 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
export class RenewAdHocAccessPane extends ContextualPaneBase { export class RenewAdHocAccessPane extends ContextualPaneBase {
public accessKey: ko.Observable<string>; public accessKey: ko.Observable<string>;
@@ -82,7 +83,7 @@ export class RenewAdHocAccessPane extends ContextualPaneBase {
this.container this.container
.renewShareAccess(this.accessKey()) .renewShareAccess(this.accessKey())
.fail((error: any) => { .fail((error: any) => {
const errorMessage: string = error.message; const errorMessage: string = getErrorMessage(error);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to connect: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to connect: ${errorMessage}`);
this.formErrors(errorMessage); this.formErrors(errorMessage);
this.formErrorsDetails(errorMessage); this.formErrorsDetails(errorMessage);

View File

@@ -8,6 +8,7 @@ import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsol
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import QueryTab from "../Tabs/QueryTab"; import QueryTab from "../Tabs/QueryTab";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export class SaveQueryPane extends ContextualPaneBase { export class SaveQueryPane extends ContextualPaneBase {
public queryName: ko.Observable<string>; public queryName: ko.Observable<string>;
@@ -87,18 +88,18 @@ export class SaveQueryPane extends ContextualPaneBase {
}, },
(error: any) => { (error: any) => {
this.isExecuting(false); this.isExecuting(false);
if (typeof error != "string") { const errorMessage = getErrorMessage(error);
error = error.message;
}
this.formErrors("Failed to save query"); this.formErrors("Failed to save query");
this.formErrorsDetails(`Failed to save query: ${error}`); this.formErrorsDetails(`Failed to save query: ${errorMessage}`);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.SaveQuery, Action.SaveQuery,
{ {
databaseAccountName: this.container.databaseAccount().name, databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(), defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title() paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error)
}, },
startKey startKey
); );
@@ -132,18 +133,21 @@ export class SaveQueryPane extends ContextualPaneBase {
startKey startKey
); );
} catch (error) { } catch (error) {
const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.SetupSavedQueries, Action.SetupSavedQueries,
{ {
databaseAccountName: this.container && this.container.databaseAccount().name, databaseAccountName: this.container && this.container.databaseAccount().name,
defaultExperience: this.container && this.container.defaultExperience(), defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title() paneTitle: this.title(),
error: errorMessage,
errorStack: getErrorStack(error)
}, },
startKey startKey
); );
this.formErrors("Failed to setup a container for saved queries"); this.formErrors("Failed to setup a container for saved queries");
this.formErrors(`Failed to setup a container for saved queries: ${error.message}`); this.formErrorsDetails(`Failed to setup a container for saved queries: ${errorMessage}`);
} finally { } finally {
this.isExecuting(false); this.isExecuting(false);
} }

View File

@@ -6,6 +6,7 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as ko from "knockout"; import * as ko from "knockout";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export class SetupNotebooksPane extends ContextualPaneBase { export class SetupNotebooksPane extends ContextualPaneBase {
private description: ko.Observable<string>; private description: ko.Observable<string>;
@@ -85,7 +86,7 @@ export class SetupNotebooksPane extends ContextualPaneBase {
"Successfully created a default notebook workspace for the account" "Successfully created a default notebook workspace for the account"
); );
} catch (error) { } catch (error) {
const errorMessage = typeof error == "string" ? error : error.message; const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.CreateNotebookWorkspace, Action.CreateNotebookWorkspace,
{ {
@@ -93,7 +94,8 @@ export class SetupNotebooksPane extends ContextualPaneBase {
defaultExperience: this.container && this.container.defaultExperience(), defaultExperience: this.container && this.container.defaultExperience(),
dataExplorerArea: Areas.ContextualPane, dataExplorerArea: Areas.ContextualPane,
paneTitle: this.title(), paneTitle: this.title(),
error: errorMessage error: errorMessage,
errorStack: getErrorStack(error)
}, },
startKey startKey
); );

Some files were not shown because too many files have changed in this diff Show More