Compare commits

..

1 Commits

Author SHA1 Message Date
Jordi Bunster
6adfea03ab Q removal: the easy parts 2020-11-16 03:26:08 -08:00
172 changed files with 3371 additions and 2055 deletions

View File

@@ -1,10 +1,7 @@
# These options are only needed when if running end to end tests locally
PORTAL_RUNNER_USERNAME=
PORTAL_RUNNER_PASSWORD=
PORTAL_RUNNER_SUBSCRIPTION=
PORTAL_RUNNER_RESOURCE_GROUP=
PORTAL_RUNNER_DATABASE_ACCOUNT=
PORTAL_RUNNER_CONNECTION_STRING=
CASSANDRA_CONNECTION_STRING=
MONGO_CONNECTION_STRING=
TABLES_CONNECTION_STRING=
DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html
PORTAL_RUNNER_CONNECTION_STRING=

View File

@@ -15,6 +15,8 @@ src/Common/DeleteFeedback.ts
src/Common/DocumentClientUtilityBase.ts
src/Common/EditableUtility.ts
src/Common/EnvironmentUtility.ts
src/Common/ErrorParserUtility.test.ts
src/Common/ErrorParserUtility.ts
src/Common/HashMap.test.ts
src/Common/HashMap.ts
src/Common/HeadersUtility.test.ts

View File

@@ -150,12 +150,6 @@ jobs:
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
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:
name: Publish Nuget
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
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.
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.
`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.
1. Turn off browser SSL validation for localhost: chrome://flags/#allow-insecure-localhost OR Install valid SSL certs for localhost (on IE, follow these [instructions](https://www.technipages.com/ie-bypass-problem-with-this-websites-security-certificate) to install the localhost certificate in the right place)
2. Allowlist `https://localhost:1234` domain for CORS in the Azure Cosmos DB portal
2. Whitelist `https://localhost:1234` domain for CORS in the Azure Cosmos DB portal
3. Start the project in portal mode: `PLATFORM=Portal npm run watch`
4. Load the portal using the following link: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html
@@ -84,19 +84,16 @@ Unit tests are located adjacent to the code under test and run with [Jest](https
4. Install dependencies: `npm install`
5. Run cypress headless(`npm run test`) or in interactive mode(`npm run test:debug`)
#### End to End Production Tests
#### End to End Production Runners
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
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`
1. Copy .env.example to .env and fill in all variables
2. Run `npm run test:e2e`
### Releasing
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.
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.
# Contributing

48
package-lock.json generated
View File

@@ -2803,13 +2803,12 @@
}
},
"@nteract/monaco-editor": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@nteract/monaco-editor/-/monaco-editor-3.2.2.tgz",
"integrity": "sha512-51Pxt6v6qaAlbDY0BgEydk/Jxuu93t+uB8Geg3vJfE6VDphTEakB0wocBIfvcTKVV55Lx53/rTSp6QHqtaHiGg==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@nteract/monaco-editor/-/monaco-editor-3.2.0.tgz",
"integrity": "sha512-PGEUvy/GTBMECy4RUfh4wxO7GfA9YDBSV3hGt8MyrVz/GxUDtjB7FqrYS0ZhmVQPYl8hnV2i48F3YlypC+xIXA==",
"requires": {
"@nteract/core": "^14.0.0",
"@nteract/messaging": "^7.0.12",
"lodash.debounce": "^4.0.6",
"@nteract/messaging": "^7.0.10",
"monaco-editor": "0.18.1",
"rxjs": "^6.3.3"
},
@@ -2858,40 +2857,6 @@
"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": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/@nteract/mythic-notifications/-/mythic-notifications-0.1.9.tgz",
@@ -14457,11 +14422,6 @@
"resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz",
"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": {
"version": "4.2.0",
"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/logos": "1.0.0",
"@nteract/markdown": "4.4.0",
"@nteract/monaco-editor": "3.2.2",
"@nteract/monaco-editor": "3.2.0",
"@nteract/octicons": "2.0.0",
"@nteract/outputs": "3.0.9",
"@nteract/presentational-components": "3.0.7",

View File

@@ -1,3 +1,4 @@
import { AutopilotTier } from "../Contracts/DataModels";
import { HashMap } from "./HashMap";
export class AuthorizationEndpoints {
@@ -123,6 +124,7 @@ export class Features {
public static readonly notebookBasePath = "notebookbasepath";
public static readonly canExceedMaximumValue = "canexceedmaximumvalue";
public static readonly enableFixedCollectionWithSharedThroughput = "enablefixedcollectionwithsharedthroughput";
public static readonly enableAutoPilotV2 = "enableautopilotv2";
public static readonly ttl90Days = "ttl90days";
public static readonly enableRightPanelV2 = "enablerightpanelv2";
public static readonly enableSDKoperations = "enablesdkoperations";
@@ -260,6 +262,7 @@ export class HttpHeaders {
public static usePolygonsSmallerThanAHemisphere = "x-ms-documentdb-usepolygonssmallerthanahemisphere";
public static autoPilotThroughput = "autoscaleSettings";
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 migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
@@ -404,6 +407,54 @@ export enum ConflictOperationType {
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 =
//[SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Well known public masterKey for emulator")]
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";

View File

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

View File

@@ -7,7 +7,6 @@ import {
Resource
} from "@azure/cosmos";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import Q from "q";
import { configContext, Platform } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts";
@@ -39,18 +38,17 @@ export function getCommonQueryOptions(options: FeedOptions): any {
return options;
}
export function queryDocuments(
export async function queryDocuments(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
): Promise<QueryIterator<ItemDefinition & Resource>> {
options = getCommonQueryOptions(options);
const documentsIterator = client()
return client()
.database(databaseId)
.container(containerId)
.items.query(query, options);
return Q(documentsIterator);
}
export function getPartitionKeyHeaderForConflict(conflictId: ConflictId): Object {
@@ -76,17 +74,15 @@ export function updateDocument(
collection: ViewModels.CollectionBase,
documentId: DocumentId,
newDocument: any
): Q.Promise<any> {
): Promise<any> {
const partitionKey = documentId.partitionKeyValue;
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
.replace(newDocument)
.then(response => response.resource)
);
return client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
.replace(newDocument)
.then(response => response.resource);
}
export function executeStoredProcedure(
@@ -94,89 +90,89 @@ export function executeStoredProcedure(
storedProcedure: StoredProcedure,
partitionKeyValue: any,
params: any[]
): Q.Promise<any> {
// TODO remove this deferred. Kept it because of timeout code at bottom of function
const deferred = Q.defer<any>();
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedure(storedProcedure.id())
.execute(partitionKeyValue, params, { enableScriptLogging: true })
.then(response =>
deferred.resolve({
): Promise<any> {
return Promise.race([
client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedure(storedProcedure.id())
.execute(partitionKeyValue, params, { enableScriptLogging: true })
.then(response => ({
result: response.resource,
scriptLogs: response.headers[Constants.HttpHeaders.scriptLogResults]
})
})),
new Promise((_, reject) =>
setTimeout(
() => reject(`Request timed out while executing stored procedure ${storedProcedure.id()}`),
Constants.ClientDefaults.requestTimeoutMs
)
)
.catch(error => deferred.reject(error));
return deferred.promise.timeout(
Constants.ClientDefaults.requestTimeoutMs,
`Request timed out while executing stored procedure ${storedProcedure.id()}`
);
]);
}
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.items.create(newDocument)
.then(response => response.resource)
);
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Promise<any> {
return client()
.database(collection.databaseId)
.container(collection.id())
.items.create(newDocument)
.then(response => response.resource);
}
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Promise<any> {
const partitionKey = documentId.partitionKeyValue;
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
.read()
.then(response => response.resource)
);
return client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
.read()
.then(response => response.resource);
}
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Promise<any> {
const partitionKey = documentId.partitionKeyValue;
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
.delete()
);
return client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
.delete();
}
export function deleteConflict(
collection: ViewModels.CollectionBase,
conflictId: ConflictId,
options: any = {}
): Q.Promise<any> {
): Promise<any> {
options.partitionKey = options.partitionKey || getPartitionKeyHeaderForConflict(conflictId);
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.conflict(conflictId.id())
.delete(options)
);
return client()
.database(collection.databaseId)
.container(collection.id())
.conflict(conflictId.id())
.delete(options);
}
export function queryConflicts(
export async function refreshCachedOffers(): Promise<void> {
if (configContext.platform === Platform.Portal) {
sendCachedDataMessage(MessageTypes.RefreshOffers, []);
}
}
export async function refreshCachedResources(options?: any): Promise<void> {
if (configContext.platform === Platform.Portal) {
sendCachedDataMessage(MessageTypes.RefreshResources, []);
}
}
export async function queryConflicts(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
const documentsIterator = client()
): Promise<QueryIterator<ConflictDefinition & Resource>> {
return client()
.database(databaseId)
.container(containerId)
.conflicts.query(query, options);
return Q(documentsIterator);
}

View File

@@ -1,5 +1,4 @@
import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import Q from "q";
import * as ViewModels from "../Contracts/ViewModels";
import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId";
@@ -16,7 +15,7 @@ export function queryDocuments(
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
): Promise<QueryIterator<ItemDefinition & Resource>> {
return DataAccessUtilityBase.queryDocuments(databaseId, containerId, query, options);
}
@@ -25,7 +24,7 @@ export function queryConflicts(
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
): Promise<QueryIterator<ConflictDefinition & Resource>> {
return DataAccessUtilityBase.queryConflicts(databaseId, containerId, query, options);
}
@@ -43,32 +42,26 @@ export function executeStoredProcedure(
storedProcedure: StoredProcedure,
partitionKeyValue: any,
params: any[]
): Q.Promise<any> {
var deferred = Q.defer<any>();
): Promise<any> {
const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`);
DataAccessUtilityBase.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params)
return DataAccessUtilityBase.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params)
.then(
(response: any) => {
deferred.resolve(response);
logConsoleInfo(
`Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
);
return response;
},
(error: any) => {
handleError(
error,
"ExecuteStoredProcedure",
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`,
"ExecuteStoredProcedure"
);
deferred.reject(error);
throw error;
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
.finally(clearMessage);
}
export function queryDocumentsPage(
@@ -76,142 +69,114 @@ export function queryDocumentsPage(
documentsIterator: MinimalQueryIterator,
firstItemIndex: number,
options: any
): Q.Promise<ViewModels.QueryResults> {
var deferred = Q.defer<ViewModels.QueryResults>();
): Promise<ViewModels.QueryResults> {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
Q(nextPage(documentsIterator, firstItemIndex))
return nextPage(documentsIterator, firstItemIndex)
.then(
(result: ViewModels.QueryResults) => {
const itemCount = (result.documents && result.documents.length) || 0;
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
deferred.resolve(result);
return result;
},
(error: any) => {
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`);
deferred.reject(error);
handleError(error, `Failed to query ${entityName} for container ${resourceName}`, "QueryDocumentsPage");
throw error;
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
.finally(clearMessage);
}
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
var deferred = Q.defer<any>();
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Promise<any> {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`);
DataAccessUtilityBase.readDocument(collection, documentId)
.then(
(document: any) => {
deferred.resolve(document);
},
(error: any) => {
handleError(error, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
return DataAccessUtilityBase.readDocument(collection, documentId)
.catch((error: any) => {
handleError(error, `Failed to read ${entityName} ${documentId.id()}`, "ReadDocument");
throw error;
})
.finally(clearMessage);
}
export function updateDocument(
collection: ViewModels.CollectionBase,
documentId: DocumentId,
newDocument: any
): Q.Promise<any> {
var deferred = Q.defer<any>();
): Promise<any> {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`);
DataAccessUtilityBase.updateDocument(collection, documentId, newDocument)
return DataAccessUtilityBase.updateDocument(collection, documentId, newDocument)
.then(
(updatedDocument: any) => {
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
deferred.resolve(updatedDocument);
return updatedDocument;
},
(error: any) => {
handleError(error, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`);
deferred.reject(error);
handleError(error, `Failed to update ${entityName} ${documentId.id()}`, "UpdateDocument");
throw error;
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
.finally(clearMessage);
}
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
var deferred = Q.defer<any>();
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Promise<any> {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`);
DataAccessUtilityBase.createDocument(collection, newDocument)
return DataAccessUtilityBase.createDocument(collection, newDocument)
.then(
(savedDocument: any) => {
logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`);
deferred.resolve(savedDocument);
return savedDocument;
},
(error: any) => {
handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`);
deferred.reject(error);
handleError(error, `Error while creating new ${entityName} for container ${collection.id()}`, "CreateDocument");
throw error;
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
.finally(clearMessage);
}
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
var deferred = Q.defer<any>();
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Promise<any> {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`);
DataAccessUtilityBase.deleteDocument(collection, documentId)
return DataAccessUtilityBase.deleteDocument(collection, documentId)
.then(
(response: any) => {
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
deferred.resolve(response);
return response;
},
(error: any) => {
handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`);
deferred.reject(error);
handleError(error, `Error while deleting ${entityName} ${documentId.id()}`, "DeleteDocument");
throw error;
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
.finally(clearMessage);
}
export function deleteConflict(
collection: ViewModels.CollectionBase,
conflictId: ConflictId,
options?: any
): Q.Promise<any> {
var deferred = Q.defer<any>();
): Promise<any> {
const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`);
DataAccessUtilityBase.deleteConflict(collection, conflictId, options)
return DataAccessUtilityBase.deleteConflict(collection, conflictId, options)
.then(
(response: any) => {
logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`);
deferred.resolve(response);
return response;
},
(error: any) => {
handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`);
deferred.reject(error);
handleError(error, `Error while deleting conflict ${conflictId.id()}`, "DeleteConflict");
throw error;
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
.finally(clearMessage);
}
export function refreshCachedResources(options: any = {}): Promise<void> {
return DataAccessUtilityBase.refreshCachedResources(options);
}
export function refreshCachedOffers(): Promise<void> {
return DataAccessUtilityBase.refreshCachedOffers();
}

View File

@@ -1,56 +1,11 @@
import { ARMError } from "../Utils/arm/request";
import { HttpStatusCodes } from "./Constants";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/ViewModels";
import { CosmosError, sendNotificationForError } from "./dataAccess/sendNotificationForError";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { logError } from "./Logger";
import { sendMessage } from "./MessageHandler";
import { replaceKnownError } from "./ErrorParserUtility";
export const handleError = (error: string | ARMError | Error, area: string, consoleErrorPrefix?: string): void => {
const errorMessage = getErrorMessage(error);
const errorCode = error instanceof ARMError ? error.code : undefined;
// 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;
export const handleError = (error: CosmosError, consoleErrorPrefix: string, area: string): void => {
const sanitizedErrorMsg = replaceKnownError(error.message);
logConsoleError(`${consoleErrorPrefix}:\n ${sanitizedErrorMsg}`);
logError(sanitizedErrorMsg, area, error.code);
sendNotificationForError(error);
};

View File

@@ -0,0 +1,24 @@
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

@@ -0,0 +1,69 @@
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,8 +21,14 @@ export function logWarning(message: string, area: string, code?: number): void {
return _logEntry(entry);
}
export function logError(errorMessage: string, area: string, code?: number | string): void {
const entry: Diagnostics.LogEntry = _generateLogEntry(Diagnostics.LogEntryLevel.Error, errorMessage, area, code);
export function logError(message: string | Error, area: string, code?: number): void {
let logMessage: string;
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);
}
@@ -53,7 +59,7 @@ function _generateLogEntry(
level: Diagnostics.LogEntryLevel,
message: string,
area: string,
code?: number | string
code?: number
): Diagnostics.LogEntry {
return {
timestamp: new Date().getUTCSeconds(),

View File

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

View File

@@ -23,6 +23,7 @@ import {
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { refreshCachedResources } from "../DataAccessUtilityBase";
import { userContext } from "../../UserContext";
import { createDatabase } from "./createDatabase";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@@ -53,9 +54,10 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
}
logConsoleInfo(`Successfully created container ${params.collectionId}`);
await refreshCachedResources();
return collection;
} catch (error) {
handleError(error, "CreateCollection", `Error while creating container ${params.collectionId}`);
handleError(error, `Error while creating container ${params.collectionId}`, "CreateCollection");
throw error;
} finally {
clearMessage();

View File

@@ -26,6 +26,7 @@ import {
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { refreshCachedOffers, refreshCachedResources } from "../DataAccessUtilityBase";
import { userContext } from "../../UserContext";
export async function createDatabase(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
@@ -38,10 +39,12 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
? createDatabaseWithARM(params)
: createDatabaseWithSDK(params));
await refreshCachedResources();
await refreshCachedOffers();
logConsoleInfo(`Successfully created database ${params.databaseId}`);
return database;
} catch (error) {
handleError(error, "CreateDatabase", `Error while creating database ${params.databaseId}`);
handleError(error, `Error while creating database ${params.databaseId}`, "CreateDatabase");
throw error;
} finally {
clearMessage();

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import { AuthType } from "../../AuthType";
import { client } from "../CosmosClient";
import { updateUserContext } from "../../UserContext";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { sendCachedDataMessage } from "../MessageHandler";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
describe("deleteCollection", () => {
@@ -17,6 +18,7 @@ describe("deleteCollection", () => {
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB
});
(sendCachedDataMessage as jest.Mock).mockResolvedValue(undefined);
});
it("should call ARM if logged in with AAD", async () => {

View File

@@ -9,6 +9,7 @@ import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
import { client } from "../CosmosClient";
import { refreshCachedResources } from "../DataAccessUtilityBase";
export async function deleteCollection(databaseId: string, collectionId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
@@ -22,8 +23,9 @@ export async function deleteCollection(databaseId: string, collectionId: string)
.delete();
}
logConsoleInfo(`Successfully deleted container ${collectionId}`);
await refreshCachedResources();
} catch (error) {
handleError(error, "DeleteCollection", `Error while deleting container ${collectionId}`);
handleError(error, `Error while deleting container ${collectionId}`, "DeleteCollection");
throw error;
} finally {
clearMessage();

View File

@@ -7,6 +7,7 @@ import { AuthType } from "../../AuthType";
import { client } from "../CosmosClient";
import { updateUserContext } from "../../UserContext";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { sendCachedDataMessage } from "../MessageHandler";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
describe("deleteDatabase", () => {
@@ -17,6 +18,7 @@ describe("deleteDatabase", () => {
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB
});
(sendCachedDataMessage as jest.Mock).mockResolvedValue(undefined);
});
it("should call ARM if logged in with AAD", async () => {

View File

@@ -8,6 +8,7 @@ import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { userContext } from "../../UserContext";
import { client } from "../CosmosClient";
import { refreshCachedResources } from "../DataAccessUtilityBase";
export async function deleteDatabase(databaseId: string): Promise<void> {
const clearMessage = logConsoleProgress(`Deleting database ${databaseId}`);
@@ -24,8 +25,9 @@ export async function deleteDatabase(databaseId: string): Promise<void> {
.delete();
}
logConsoleInfo(`Successfully deleted database ${databaseId}`);
await refreshCachedResources();
} catch (error) {
handleError(error, "DeleteDatabase", `Error while deleting database ${databaseId}`);
handleError(error, `Error while deleting database ${databaseId}`, "DeleteDatabase");
throw error;
} finally {
clearMessage();

View File

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

View File

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

View File

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

View File

@@ -1,28 +0,0 @@
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();
collection = response.resource as DataModels.Collection;
} catch (error) {
handleError(error, "ReadCollection", `Error while querying container ${collectionId}`);
handleError(error, `Error while querying container ${collectionId}`, "ReadCollection");
throw error;
}
clearMessage();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,8 @@ import { userContext } from "../../UserContext";
import { getMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
import { MongoDBCollectionResource } from "../../Utils/arm/generatedClients/2020-04-01/types";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import * as Constants from "../Constants";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { AuthType } from "../../AuthType";
@@ -22,9 +24,35 @@ export async function readMongoDBCollectionThroughRP(
const response = await getMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
collection = response.properties.resource;
} catch (error) {
handleError(error, "ReadMongoDBCollection", `Error while reading container ${collectionId}`);
handleError(error, `Error while reading container ${collectionId}`, "ReadMongoDBCollection");
throw error;
}
clearMessage();
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,10 +1,28 @@
import { Offer } from "../../Contracts/DataModels";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { ClientDefaults } from "../Constants";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { Platform, configContext } from "../../ConfigContext";
import { client } from "../CosmosClient";
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { sendCachedDataMessage } from "../MessageHandler";
import { userContext } from "../../UserContext";
export const readOffers = async (): Promise<Offer[]> => {
const clearMessage = logConsoleProgress(`Querying offers`);
try {
if (configContext.platform === Platform.Portal) {
const offers = sendCachedDataMessage<Offer[]>(MessageTypes.AllOffers, [
userContext.databaseAccount.id,
ClientDefaults.portalCacheTimeoutMs
]);
clearMessage();
return offers;
}
} catch (error) {
// If error getting cached Offers, continue on and read via SDK
}
try {
const response = await client()
@@ -13,11 +31,11 @@ export const readOffers = async (): Promise<Offer[]> => {
return response?.resources;
} catch (error) {
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too.
if (getErrorMessage(error)?.includes("Reading or replacing offers is not supported for serverless accounts")) {
if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) {
return [];
}
handleError(error, "ReadOffers", `Error while querying offers`);
handleError(error, `Error while querying offers`, "ReadOffers");
throw error;
} finally {
clearMessage();

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
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

@@ -28,6 +28,7 @@ import {
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { refreshCachedResources } from "../DataAccessUtilityBase";
import { userContext } from "../../UserContext";
export async function updateCollection(
@@ -57,9 +58,10 @@ export async function updateCollection(
}
logConsoleInfo(`Successfully updated container ${collectionId}`);
await refreshCachedResources();
return collection;
} catch (error) {
handleError(error, "UpdateCollection", `Failed to update container ${collectionId}`);
handleError(error, `Failed to update container ${collectionId}`, "UpdateCollection");
throw error;
} finally {
clearMessage();

View File

@@ -10,6 +10,7 @@ import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { readCollectionOffer } from "./readCollectionOffer";
import { readDatabaseOffer } from "./readDatabaseOffer";
import { refreshCachedOffers, refreshCachedResources } from "../DataAccessUtilityBase";
import {
updateSqlDatabaseThroughput,
migrateSqlDatabaseToAutoscale,
@@ -69,10 +70,12 @@ export const updateOffer = async (params: UpdateOfferParams): Promise<Offer> =>
} else {
updatedOffer = await updateOfferWithSDK(params);
}
await refreshCachedOffers();
await refreshCachedResources();
logConsoleInfo(`Successfully updated offer for ${offerResourceText}`);
return updatedOffer;
} catch (error) {
handleError(error, "UpdateCollection", `Error updating offer for ${offerResourceText}`);
handleError(error, `Error updating offer for ${offerResourceText}`, "UpdateCollection");
throw error;
} finally {
clearMessage();

View File

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

View File

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

View File

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

View File

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

View File

@@ -216,6 +216,18 @@ export interface UniqueKey {
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 {
databaseId: string;
collectionId: string;
@@ -227,12 +239,21 @@ export interface CreateDatabaseAndCollectionRequest {
uniqueKeyPolicy?: UniqueKeyPolicy;
autoPilot?: AutoPilotCreationSettings;
analyticalStorageTtl?: number;
hasAutoPilotV2FeatureFlag?: boolean;
}
export interface AutoPilotCreationSettings {
autopilotTier?: AutopilotTier;
maxThroughput?: number;
}
export enum AutopilotTier {
Tier1 = 1,
Tier2 = 2,
Tier3 = 3,
Tier4 = 4
}
export interface Query {
id: string;
resourceId: string;
@@ -241,7 +262,9 @@ export interface Query {
}
export interface AutoPilotOfferSettings {
tier?: AutopilotTier;
maximumTierThroughput?: number;
targetTier?: AutopilotTier;
maxThroughput?: number;
targetMaxThroughput?: number;
}
@@ -468,6 +491,7 @@ export interface MongoParameters extends RpParameters {
rid?: string;
rtype?: string;
isAutoPilot?: Boolean;
autoPilotTier?: string;
autoPilotThroughput?: string;
analyticalStorageTtl?: number;
}

View File

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

View File

@@ -5,7 +5,6 @@ import {
TriggerDefinition,
UserDefinedFunctionDefinition
} from "@azure/cosmos";
import Q from "q";
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer/Explorer";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
@@ -107,7 +106,7 @@ export interface CollectionBase extends TreeNode {
onDocumentDBDocumentsClick(): void;
onNewQueryClick(source: any, event: MouseEvent, queryText?: string): void;
expandCollection(): Q.Promise<any>;
expandCollection(): Promise<any>;
collapseCollection(): void;
getDatabase(): Database;
}
@@ -162,7 +161,6 @@ export interface Collection extends CollectionBase {
loadStoredProcedures(): Promise<any>;
loadTriggers(): Promise<any>;
loadOffer(): Promise<void>;
loadAutopilotOfferWithRetry(): Promise<DataModels.Offer>;
createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure;
createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction;
@@ -173,7 +171,7 @@ export interface Collection extends CollectionBase {
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
uploadFiles(fileList: FileList): Q.Promise<UploadDetails>;
uploadFiles(fileList: FileList): Promise<UploadDetails>;
getLabel(): string;
}
@@ -291,7 +289,7 @@ export interface DocumentsTabOptions extends TabOptions {
}
export interface SettingsTabV2Options extends TabOptions {
getPendingNotification: Q.Promise<DataModels.Notification>;
getPendingNotification: Promise<DataModels.Notification>;
}
export interface ConflictsTabOptions extends TabOptions {

View File

@@ -123,4 +123,8 @@ describe("Component Registerer", () => {
it("should register dynamic-list component", () => {
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,6 +11,7 @@ import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahea
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent";
import { TabsManagerKOComponent } from "./Tabs/TabsManager";
import { ThroughputInputComponent } from "./Controls/ThroughputInput/ThroughputInputComponent";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
ko.components.register("input-typeahead", new InputTypeaheadComponent());
@@ -22,6 +23,7 @@ ko.components.register("editor", new EditorComponent());
ko.components.register("json-editor", new JsonEditorComponent());
ko.components.register("diff-editor", new DiffEditorComponent());
ko.components.register("dynamic-list", DynamicListComponent);
ko.components.register("throughput-input", ThroughputInputComponent);
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
ko.components.register("tabs-manager", TabsManagerKOComponent());

View File

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

View File

@@ -57,7 +57,7 @@ class EditorViewModel extends JsonEditorViewModel {
}
}
protected getErrorMarkers(input: string): Q.Promise<monaco.editor.IMarkerData[]> {
protected async getErrorMarkers(input: string): Promise<monaco.editor.IMarkerData[]> {
return ErrorMarkProvider.getErrorMark(input);
}
}

View File

@@ -1,4 +1,3 @@
import Q from "q";
import * as monaco from "monaco-editor";
import * as ViewModels from "../../../Contracts/ViewModels";
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
@@ -107,8 +106,8 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
protected registerCompletionItemProvider() {}
// Interface. Will be implemented in children editor view model such as EditorViewModel.
protected getErrorMarkers(input: string): Q.Promise<monaco.editor.IMarkerData[]> {
return Q.Promise(() => {});
protected getErrorMarkers(input: string): Promise<monaco.editor.IMarkerData[]> {
return new Promise(() => {});
}
protected getEditorLanguage(): string {

View File

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

View File

@@ -1,8 +1,9 @@
import * as React from "react";
import { JunoClient } from "../../../Juno/JunoClient";
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 { handleError } from "../../../Common/ErrorHandlingUtils";
export interface CodeOfConductComponentProps {
junoClient: JunoClient;
@@ -44,7 +45,9 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
this.props.onAcceptCodeOfConduct(response.data);
} catch (error) {
handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct");
const message = `Failed to accept code of conduct: ${error}`;
Logger.logError(message, "CodeOfConductComponent/acceptCodeOfConduct");
logConsoleError(message);
}
}

View File

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

View File

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

View File

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

View File

@@ -8,8 +8,8 @@ import * as DataModels from "../../../Contracts/DataModels";
import ko from "knockout";
import { TtlType, isDirty } from "./SettingsUtils";
import Explorer from "../../Explorer";
jest.mock("../../../Common/dataAccess/getIndexTransformationProgress", () => ({
getIndexTransformationProgress: jest.fn().mockReturnValue(undefined)
jest.mock("../../../Common/dataAccess/readMongoDBCollection", () => ({
getMongoDBCollectionIndexTransformationProgress: jest.fn().mockReturnValue(undefined)
}));
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
jest.mock("../../../Common/dataAccess/updateCollection", () => ({
@@ -31,7 +31,6 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
}));
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import Q from "q";
jest.mock("../../../Common/dataAccess/updateOffer", () => ({
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer)
}));
@@ -47,9 +46,7 @@ describe("SettingsComponent", () => {
hashLocation: "settings",
isActive: ko.observable(false),
onUpdateTabsButtons: undefined,
getPendingNotification: Q.Promise<DataModels.Notification>(() => {
return;
})
getPendingNotification: Promise.resolve(undefined)
})
};

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ import {
} from "office-ui-fabric-react";
import { isDirtyTypes, isDirty } from "./SettingsUtils";
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
label: {
@@ -245,9 +245,15 @@ export const ttlWarning: JSX.Element = (
</Text>
);
export const indexingPolicynUnsavedWarningMessage: JSX.Element = (
export const indexingPolicyTTLWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
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,{" "}
<Link target="_blank" href="https://aka.ms/cosmosdb/modify-index-policy">
Modifying Indexing Policies
</Link>
.
</Text>
);
@@ -404,8 +410,8 @@ export const mongoIndexingPolicyAADError: JSX.Element = (
export const mongoIndexTransformationRefreshingMessage: JSX.Element = (
<Stack horizontal {...mongoWarningStackProps}>
<Text styles={infoAndToolTipTextStyle}>Refreshing index transformation progress</Text>
<Spinner size={SpinnerSize.small} />
<Text>Refreshing index transformation progress</Text>
<Spinner size={SpinnerSize.medium} />
</Stack>
);
@@ -415,14 +421,14 @@ export const renderMongoIndexTransformationRefreshMessage = (
): JSX.Element => {
if (progress === 0) {
return (
<Text styles={infoAndToolTipTextStyle}>
<Text>
{"You can make more indexing changes once the current index transformation is complete. "}
<Link onClick={performRefresh}>{"Refresh to check if it has completed."}</Link>
</Text>
);
} else {
return (
<Text styles={infoAndToolTipTextStyle}>
<Text>
{`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>
</Text>

View File

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

View File

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

View File

@@ -1,15 +0,0 @@
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

@@ -1,62 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,8 +15,7 @@ import {
MongoWildcardPlaceHolder,
getMongoIndexTypeText,
SingleFieldText,
WildcardText,
isIndexTransforming
WildcardText
} from "./SettingsUtils";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
@@ -140,10 +139,3 @@ describe("SettingsUtils", () => {
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,7 +250,3 @@ export const getMongoIndexTypeText = (index: MongoIndexTypes): string => {
}
return WildcardText;
};
export const isIndexTransforming = (indexTransformationProgress: number): boolean =>
// index transformation progress can be 0
indexTransformationProgress !== undefined && indexTransformationProgress !== 100;

View File

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

View File

@@ -166,7 +166,15 @@ exports[`SettingsUtils functions render 1`] = `
}
}
>
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
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,
<StyledLinkBase
href="https://aka.ms/cosmosdb/modify-index-policy"
target="_blank"
>
Modifying Indexing Policies
</StyledLinkBase>
.
</Text>
<Text
id="updateThroughputBeyondLimitWarningMessage"
@@ -333,30 +341,14 @@ exports[`SettingsUtils functions render 1`] = `
}
}
>
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
<Text>
Refreshing index transformation progress
</Text>
<StyledSpinnerBase
size={1}
size={2}
/>
</Stack>
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
<Text>
You can make more indexing changes once the current index transformation is complete.
<StyledLinkBase
onClick={[Function]}
@@ -364,15 +356,7 @@ exports[`SettingsUtils functions render 1`] = `
Refresh to check if it has completed.
</StyledLinkBase>
</Text>
<Text
styles={
Object {
"root": Object {
"fontSize": 12,
},
}
}
>
<Text>
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
<StyledLinkBase
onClick={[Function]}

View File

@@ -0,0 +1,222 @@
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

@@ -0,0 +1,145 @@
<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

@@ -0,0 +1,261 @@
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

@@ -3,7 +3,6 @@ jest.mock("../Graph/GraphExplorerComponent/GremlinClient");
jest.mock("../../Common/dataAccess/createCollection");
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import Q from "q";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { createDocument } from "../../Common/DocumentClientUtilityBase";
import Explorer from "../Explorer";
@@ -19,8 +18,9 @@ describe("ContainerSampleGenerator", () => {
explorerStub.isPreferredApiTable = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiCassandra = ko.computed<boolean>(() => false);
explorerStub.canExceedMaximumValue = ko.computed<boolean>(() => false);
explorerStub.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
explorerStub.findDatabaseWithId = () => database;
explorerStub.refreshAllDatabases = () => Q.resolve();
explorerStub.refreshAllDatabases = () => Promise.resolve();
return explorerStub;
};

View File

@@ -70,7 +70,7 @@ export class ContainerSampleGenerator {
if (!collection) {
throw new Error("No container to populate");
}
const promises: Q.Promise<any>[] = [];
const promises: Promise<any>[] = [];
if (this.container.isPreferredApiGraph()) {
// For Gremlin, all queries are executed sequentially, because some queries might be dependent on other queries

View File

@@ -15,6 +15,7 @@ import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import Database from "./Tree/Database";
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
import { refreshCachedResources } from "../Common/DocumentClientUtilityBase";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
@@ -86,7 +87,6 @@ import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandBut
import { updateUserContext, userContext } from "../UserContext";
import { stringToBlob } from "../Utils/BlobUtils";
import { IChoiceGroupProps } from "office-ui-fabric-react";
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@@ -213,10 +213,11 @@ export default class Explorer {
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
public isRightPanelV2Enabled: ko.Computed<boolean>;
public canExceedMaximumValue: ko.Computed<boolean>;
public hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
public shouldShowShareDialogContents: ko.Observable<boolean>;
public shareAccessData: ko.Observable<AdHocAccessData>;
public renewExplorerShareAccess: (explorer: Explorer, token: string) => Q.Promise<void>;
public renewExplorerShareAccess: (explorer: Explorer, token: string) => Promise<void>;
public renewTokenError: ko.Observable<string>;
public tokenForRenewal: ko.Observable<string>;
public shareAccessToggleState: ko.Observable<ShareAccessToggleState>;
@@ -422,6 +423,13 @@ export default class Explorer {
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.databases = ko.observableArray<ViewModels.Database>();
@@ -1040,11 +1048,11 @@ export default class Explorer {
);
TelemetryProcessor.traceSuccess(Action.EnableAzureSynapseLink, startTime);
this.databaseAccount(databaseAccount);
} catch (error) {
} catch (e) {
NotificationConsoleUtils.clearInProgressMessageWithId(logId);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`
`Enabling Azure Synapse Link for this account failed. ${e.message || JSON.stringify(e)}`
);
TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, startTime);
} finally {
@@ -1112,8 +1120,8 @@ export default class Explorer {
"Initiating connection to account"
);
this.renewExplorerShareAccess(this, this.tokenForRenewal())
.fail((error: any) => {
const stringifiedError: string = getErrorMessage(error);
.catch((error: any) => {
const stringifiedError: string = error.message;
this.renewTokenError("Invalid connection string specified");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
@@ -1142,43 +1150,34 @@ export default class Explorer {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to generate share url: ${getErrorMessage(error)}`
`Failed to generate share url: ${error.message}`
);
console.error(error);
}
);
}
public renewShareAccess(token: string): Q.Promise<void> {
public async renewShareAccess(token: string): Promise<void> {
if (!this.renewExplorerShareAccess) {
return Q.reject("Not implemented");
throw "Not implemented";
}
const deferred: Q.Deferred<void> = Q.defer<void>();
const id: string = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
"Initiating connection to account"
);
this.renewExplorerShareAccess(this, token)
return this.renewExplorerShareAccess(this, token)
.then(
() => {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Connection successful");
this.renewAdHocAccessPane && this.renewAdHocAccessPane.close();
deferred.resolve();
},
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to connect: ${getErrorMessage(error)}`
);
deferred.reject(error);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to connect: ${error.message}`);
throw error;
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
});
return deferred.promise;
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
}
public displayGuestAccessTokenRenewalPrompt(): void {
@@ -1373,24 +1372,19 @@ export default class Explorer {
}
}
public refreshDatabaseForResourceToken(): Q.Promise<any> {
public async refreshDatabaseForResourceToken(): Promise<any> {
const databaseId = this.resourceTokenDatabaseId();
const collectionId = this.resourceTokenCollectionId();
if (!databaseId || !collectionId) {
return Q.reject();
throw new Error();
}
const deferred: Q.Deferred<void> = Q.defer();
readCollection(databaseId, collectionId).then((collection: DataModels.Collection) => {
this.resourceTokenCollection(new ResourceTokenCollection(this, databaseId, collection));
this.selectedNode(this.resourceTokenCollection());
deferred.resolve();
});
return deferred.promise;
const collection = await readCollection(databaseId, collectionId);
this.resourceTokenCollection(new ResourceTokenCollection(this, databaseId, collection));
this.selectedNode(this.resourceTokenCollection());
}
public refreshAllDatabases(isInitialLoad?: boolean): Q.Promise<any> {
public refreshAllDatabases(isInitialLoad?: boolean): Promise<any> {
this.isRefreshingExplorer(true);
const startKey: number = TelemetryProcessor.traceStart(Action.LoadDatabases, {
databaseAccountName: this.databaseAccount() && this.databaseAccount().name,
@@ -1407,92 +1401,85 @@ export default class Explorer {
}
// TODO: Refactor
const deferred: Q.Deferred<any> = Q.defer();
this._setLoadingStatusText("Fetching databases...");
readDatabases().then(
(databases: DataModels.Database[]) => {
this._setLoadingStatusText("Successfully fetched databases.");
TelemetryProcessor.traceSuccess(
Action.LoadDatabases,
{
databaseAccountName: this.databaseAccount().name,
defaultExperience: this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree
},
startKey
);
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
const deltaDatabases = this.getDeltaDatabases(databases);
this.addDatabasesToList(deltaDatabases.toAdd);
this.deleteDatabasesFromList(deltaDatabases.toDelete);
this.selectedNode(currentlySelectedNode);
this._setLoadingStatusText("Fetching containers...");
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd)
.then(
() => {
this._setLoadingStatusText("Successfully fetched containers.");
deferred.resolve();
},
reason => {
this._setLoadingStatusText("Failed to fetch containers.");
deferred.reject(reason);
}
)
.finally(() => this.isRefreshingExplorer(false));
},
error => {
this._setLoadingStatusText("Failed to fetch databases.");
this.isRefreshingExplorer(false);
deferred.reject(error);
const errorMessage = getErrorMessage(error);
TelemetryProcessor.traceFailure(
Action.LoadDatabases,
{
databaseAccountName: this.databaseAccount().name,
defaultExperience: this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree,
error: errorMessage,
errorStack: getErrorStack(error)
},
startKey
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while refreshing databases: ${errorMessage}`
);
}
);
return deferred.promise.then(
() => {
if (resourceTreeStartKey != null) {
return readDatabases()
.then(
(databases: DataModels.Database[]) => {
this._setLoadingStatusText("Successfully fetched databases.");
TelemetryProcessor.traceSuccess(
Action.LoadResourceTree,
Action.LoadDatabases,
{
databaseAccountName: this.databaseAccount() && this.databaseAccount().name,
defaultExperience: this.defaultExperience && this.defaultExperience(),
databaseAccountName: this.databaseAccount().name,
defaultExperience: this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree
},
resourceTreeStartKey
startKey
);
}
},
error => {
if (resourceTreeStartKey != null) {
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
const deltaDatabases = this.getDeltaDatabases(databases);
this.addDatabasesToList(deltaDatabases.toAdd);
this.deleteDatabasesFromList(deltaDatabases.toDelete);
this.selectedNode(currentlySelectedNode);
this._setLoadingStatusText("Fetching containers...");
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd)
.then(
() => this._setLoadingStatusText("Successfully fetched containers."),
reason => {
this._setLoadingStatusText("Failed to fetch containers.");
throw reason;
}
)
.finally(() => this.isRefreshingExplorer(false));
},
error => {
this._setLoadingStatusText("Failed to fetch databases.");
this.isRefreshingExplorer(false);
TelemetryProcessor.traceFailure(
Action.LoadResourceTree,
Action.LoadDatabases,
{
databaseAccountName: this.databaseAccount() && this.databaseAccount().name,
defaultExperience: this.defaultExperience && this.defaultExperience(),
databaseAccountName: this.databaseAccount().name,
defaultExperience: this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree,
error: getErrorMessage(error),
errorStack: getErrorStack(error)
error: error.message
},
resourceTreeStartKey
startKey
);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while refreshing databases: ${error.message}`
);
throw error;
}
}
);
)
.then(
() => {
if (resourceTreeStartKey != null) {
TelemetryProcessor.traceSuccess(
Action.LoadResourceTree,
{
databaseAccountName: this.databaseAccount() && this.databaseAccount().name,
defaultExperience: this.defaultExperience && this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree
},
resourceTreeStartKey
);
}
},
reason => {
if (resourceTreeStartKey != null) {
TelemetryProcessor.traceFailure(
Action.LoadResourceTree,
{
databaseAccountName: this.databaseAccount() && this.databaseAccount().name,
defaultExperience: this.defaultExperience && this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree,
error: reason
},
resourceTreeStartKey
);
}
}
);
}
public onRefreshDatabasesKeyPress = (source: any, event: KeyboardEvent): boolean => {
@@ -1511,7 +1498,41 @@ export default class Explorer {
dataExplorerArea: Constants.Areas.ResourceTree
});
this.isRefreshingExplorer(true);
this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases();
refreshCachedResources().then(
() => {
TelemetryProcessor.traceSuccess(
Action.LoadDatabases,
{
description: "Refresh successful",
databaseAccountName: this.databaseAccount() && this.databaseAccount().name,
defaultExperience: this.defaultExperience && this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree
},
startKey
);
this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases();
},
(error: any) => {
this.isRefreshingExplorer(false);
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error while refreshing data: ${error.message}`
);
TelemetryProcessor.traceFailure(
Action.LoadDatabases,
{
description: "Unable to refresh cached resources",
databaseAccountName: this.databaseAccount() && this.databaseAccount().name,
defaultExperience: this.defaultExperience && this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree,
error: error
},
startKey
);
throw error;
}
);
this.refreshNotebookList();
};
@@ -1535,7 +1556,7 @@ export default class Explorer {
resolve(token);
},
(error: any) => {
Logger.logError(getErrorMessage(error), "Explorer/getArcadiaToken");
Logger.logError(error, "Explorer/getArcadiaToken");
resolve(undefined);
}
);
@@ -1553,7 +1574,7 @@ export default class Explorer {
workspaceItems[i] = { ...workspace, sparkPools: sparkpools };
},
error => {
Logger.logError(getErrorMessage(error), "Explorer/this._arcadiaManager.listSparkPoolsAsync");
Logger.logError(error, "Explorer/this._arcadiaManager.listSparkPoolsAsync");
}
);
sparkPromises.push(promise);
@@ -1561,7 +1582,8 @@ export default class Explorer {
return Promise.all(sparkPromises).then(() => workspaceItems);
} catch (error) {
handleError(error, "Explorer/this._arcadiaManager.listWorkspacesAsync", "Get Arcadia workspaces failed");
Logger.logError(error, "Explorer/this._arcadiaManager.listWorkspacesAsync");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error.message);
return Promise.resolve([]);
}
}
@@ -1596,10 +1618,10 @@ export default class Explorer {
);
} catch (error) {
this._isInitializingNotebooks = false;
handleError(
error,
"initNotebooks/getNotebookConnectionInfoAsync",
`Failed to get notebook workspace connection info: ${getErrorMessage(error)}`
Logger.logError(error, "initNotebooks/getNotebookConnectionInfoAsync");
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to get notebook workspace connection info: ${error.message}`
);
throw error;
} finally {
@@ -1622,10 +1644,9 @@ export default class Explorer {
public resetNotebookWorkspace() {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookClient) {
handleError(
"Attempt to reset notebook workspace, but notebook is not enabled",
"Explorer/resetNotebookWorkspace"
);
const error = "Attempt to reset notebook workspace, but notebook is not enabled";
Logger.logError(error, "Explorer/resetNotebookWorkspace");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
return;
}
const resetConfirmationDialogProps: DialogProps = {
@@ -1650,7 +1671,7 @@ export default class Explorer {
const workspaces = await this.notebookWorkspaceManager.getNotebookWorkspacesAsync(databaseAccount?.id);
return workspaces && workspaces.length > 0 && workspaces.some(workspace => workspace.name === "default");
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace");
Logger.logError(error, "Explorer/_containsDefaultNotebookWorkspace");
return false;
}
}
@@ -1676,7 +1697,8 @@ export default class Explorer {
await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(this.databaseAccount().id, "default");
}
} catch (error) {
handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace");
Logger.logError(error, "Explorer/ensureNotebookWorkspaceRunning");
NotificationConsoleUtils.logConsoleError(`Failed to initialize notebook workspace: ${error.message}`);
} finally {
clearMessage && clearMessage();
}
@@ -1691,10 +1713,7 @@ export default class Explorer {
TelemetryProcessor.traceSuccess(Action.ResetNotebookWorkspace);
} catch (error) {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to reset notebook workspace: ${error}`);
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, {
error: getErrorMessage(error),
errorStack: getErrorStack(error)
});
TelemetryProcessor.traceFailure(Action.ResetNotebookWorkspace, error);
throw error;
} finally {
NotificationConsoleUtils.clearInProgressMessageWithId(id);
@@ -1760,7 +1779,7 @@ export default class Explorer {
inputs.extensionEndpoint = configContext.PROXY_PATH;
}
const initPromise: Q.Promise<void> = inputs ? this.initDataExplorerWithFrameInputs(inputs) : Q();
const initPromise: Promise<void> = inputs ? this.initDataExplorerWithFrameInputs(inputs) : Promise.resolve();
initPromise.then(() => {
const openAction: ActionContracts.DataExplorerAction = message.openAction;
@@ -1854,7 +1873,7 @@ export default class Explorer {
return false;
}
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): Q.Promise<void> {
public async initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): Promise<void> {
if (inputs != null) {
const authorizationToken = inputs.authorizationToken || "";
const masterKey = inputs.masterKey || "";
@@ -1904,7 +1923,6 @@ export default class Explorer {
this.isAccountReady(true);
}
return Q();
}
public setFeatureFlagsFromFlights(flights: readonly string[]): void {
@@ -2022,7 +2040,7 @@ export default class Explorer {
// we reload collections for all databases so the resource tree reflects any collection-level changes
// i.e addition of stored procedures, etc.
const deferred: Q.Deferred<void> = Q.defer<void>();
let loadCollectionPromises: Q.Promise<void>[] = [];
let loadCollectionPromises: Promise<void>[] = [];
// If the user has a lot of databases, only load expanded databases.
const databasesToLoad =
@@ -2061,8 +2079,7 @@ export default class Explorer {
databaseAccountName: this.databaseAccount() && this.databaseAccount().name,
defaultExperience: this.defaultExperience && this.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree,
error: getErrorMessage(error),
errorStack: getErrorStack(error)
trace: error.message
},
startKey
);
@@ -2228,7 +2245,8 @@ export default class Explorer {
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to upload notebook, but notebook is not enabled";
handleError(error, "Explorer/uploadFile");
Logger.logError(error, "Explorer/uploadFile");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
throw new Error(error);
}
@@ -2406,10 +2424,11 @@ export default class Explorer {
return true;
}
public renameNotebook(notebookFile: NotebookContentItem): Q.Promise<NotebookContentItem> {
public async renameNotebook(notebookFile: NotebookContentItem): Promise<NotebookContentItem> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to rename notebook, but notebook is not enabled";
handleError(error, "Explorer/renameNotebook");
Logger.logError(error, "Explorer/renameNotebook");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
throw new Error(error);
}
@@ -2422,46 +2441,43 @@ export default class Explorer {
);
if (openedNotebookTabs.length > 0) {
this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
return Q.reject();
throw new Error();
}
const originalPath = notebookFile.path;
const result = this.stringInputPane
.openWithOptions<NotebookContentItem>({
errorMessage: "Could not rename notebook",
inProgressMessage: "Renaming notebook to",
successMessage: "Renamed notebook to",
inputLabel: "Enter new notebook name",
paneTitle: "Rename Notebook",
submitButtonLabel: "Rename",
defaultInput: FileSystemUtil.stripExtension(notebookFile.name, "ipynb"),
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input)
})
.then(newNotebookFile => {
const notebookTabs = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
);
notebookTabs.forEach(tab => {
tab.tabTitle(newNotebookFile.name);
tab.tabPath(newNotebookFile.path);
(tab as NotebookV2Tab).notebookPath(newNotebookFile.path);
});
const newNotebookFile = await this.stringInputPane.openWithOptions<NotebookContentItem>({
errorMessage: "Could not rename notebook",
inProgressMessage: "Renaming notebook to",
successMessage: "Renamed notebook to",
inputLabel: "Enter new notebook name",
paneTitle: "Rename Notebook",
submitButtonLabel: "Rename",
defaultInput: FileSystemUtil.stripExtension(notebookFile.name, "ipynb"),
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input)
});
const notebookTabs = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
);
notebookTabs.forEach(tab => {
tab.tabTitle(newNotebookFile.name);
tab.tabPath(newNotebookFile.path);
(tab as NotebookV2Tab).notebookPath(newNotebookFile.path);
});
return newNotebookFile;
});
result.then(() => this.resourceTree.triggerRender());
return result;
this.resourceTree.triggerRender();
return newNotebookFile;
}
public onCreateDirectory(parent: NotebookContentItem): Q.Promise<NotebookContentItem> {
public async onCreateDirectory(parent: NotebookContentItem): Promise<NotebookContentItem> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create notebook directory, but notebook is not enabled";
handleError(error, "Explorer/onCreateDirectory");
Logger.logError(error, "Explorer/onCreateDirectory");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
throw new Error(error);
}
const result = this.stringInputPane.openWithOptions<NotebookContentItem>({
const result = await this.stringInputPane.openWithOptions<NotebookContentItem>({
errorMessage: "Could not create directory ",
inProgressMessage: "Creating directory ",
successMessage: "Created directory ",
@@ -2471,14 +2487,15 @@ export default class Explorer {
defaultInput: "",
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input)
});
result.then(() => this.resourceTree.triggerRender());
this.resourceTree.triggerRender();
return result;
}
public readFile(notebookFile: NotebookContentItem): Promise<string> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to read file, but notebook is not enabled";
handleError(error, "Explorer/downloadFile");
Logger.logError(error, "Explorer/downloadFile");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
throw new Error(error);
}
@@ -2488,7 +2505,8 @@ export default class Explorer {
public downloadFile(notebookFile: NotebookContentItem): Promise<void> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to download file, but notebook is not enabled";
handleError(error, "Explorer/downloadFile");
Logger.logError(error, "Explorer/downloadFile");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
throw new Error(error);
}
@@ -2519,7 +2537,7 @@ export default class Explorer {
(error: any) => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Could not download notebook ${getErrorMessage(error)}`
`Could not download notebook ${error.message}`
);
clearMessage();
@@ -2569,7 +2587,7 @@ export default class Explorer {
);
this.isNotebooksEnabledForAccount(isAccountInAllowedLocation);
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
Logger.logError(error, "Explorer/isNotebooksEnabledForAccount");
this.isNotebooksEnabledForAccount(false);
}
}
@@ -2598,7 +2616,7 @@ export default class Explorer {
false;
this.isSparkEnabledForAccount(isEnabled);
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/isSparkEnabledForAccount");
Logger.logError(error, "Explorer/isSparkEnabledForAccount");
this.isSparkEnabledForAccount(false);
}
};
@@ -2623,7 +2641,7 @@ export default class Explorer {
(featureStatus && featureStatus.properties && featureStatus.properties.state === "Registered") || false;
return isEnabled;
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/isSparkEnabledForAccount");
Logger.logError(error, "Explorer/isSparkEnabledForAccount");
return false;
}
};
@@ -2642,7 +2660,8 @@ export default class Explorer {
public deleteNotebookFile(item: NotebookContentItem): Promise<void> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to delete notebook file, but notebook is not enabled";
handleError(error, "Explorer/deleteNotebookFile");
Logger.logError(error, "Explorer/deleteNotebookFile");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
throw new Error(error);
}
@@ -2691,7 +2710,8 @@ export default class Explorer {
public onNewNotebookClicked(parent?: NotebookContentItem): void {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create new notebook, but notebook is not enabled";
handleError(error, "Explorer/onNewNotebookClicked");
Logger.logError(error, "Explorer/onNewNotebookClicked");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
throw new Error(error);
}
@@ -2724,17 +2744,16 @@ export default class Explorer {
return this.openNotebook(newFile);
})
.then(() => this.resourceTree.triggerRender())
.catch((error: any) => {
const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, errorMessage);
.catch((reason: any) => {
const error = `Failed to create a new notebook: ${reason}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
TelemetryProcessor.traceFailure(
Action.CreateNewNotebook,
{
databaseAccountName: this.databaseAccount().name,
defaultExperience: this.defaultExperience(),
dataExplorerArea: Constants.Areas.Notebook,
error: errorMessage,
errorStack: getErrorStack(error)
error
},
startKey
);
@@ -2777,7 +2796,8 @@ export default class Explorer {
public refreshContentItem(item: NotebookContentItem): Promise<void> {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to refresh notebook list, but notebook is not enabled";
handleError(error, "Explorer/refreshContentItem");
Logger.logError(error, "Explorer/refreshContentItem");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, error);
return Promise.reject(new Error(error));
}
@@ -2963,7 +2983,7 @@ export default class Explorer {
}
return tokenRefreshInterval;
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/getTokenRefreshInterval");
Logger.logError(error, "Explorer/getTokenRefreshInterval");
return tokenRefreshInterval;
}
}

View File

@@ -378,8 +378,7 @@ export class D3ForceGraph implements GraphRenderer {
* @param targetPosition
* @return promise with shift offset
*/
private shiftGraph(targetPosition: Point2D): Q.Promise<Point2D> {
const deferred: Q.Deferred<Point2D> = Q.defer<Point2D>();
private async shiftGraph(targetPosition: Point2D): Promise<Point2D> {
const offset = { x: this.width / 2 - targetPosition.x, y: this.height / 2 - targetPosition.y };
this.viewCenter = targetPosition;
@@ -391,18 +390,15 @@ export class D3ForceGraph implements GraphRenderer {
.translate(-targetPosition.x, -targetPosition.y);
};
this.zoomBackground
const transition = this.zoomBackground
.transition()
.duration(D3ForceGraph.TRANSITION_STEP1_MS)
.call(this.zoom.transform, transform)
.on("end", () => {
deferred.resolve(offset);
});
} else {
deferred.resolve(null);
}
.call(this.zoom.transform, transform);
return deferred.promise;
await new Promise(resolve => transition.on("end", resolve));
return offset;
}
return null;
}
private onGraphDataUpdate(graph: GraphData<D3Node, D3Link>) {
@@ -435,7 +431,7 @@ export class D3ForceGraph implements GraphRenderer {
}
}
private animateRemoveExitSelections(): Q.Promise<void> {
private animateRemoveExitSelections(): Promise<void> {
const deferred1 = Q.defer<void>();
const deferred2 = Q.defer<void>();
const linkExitSelection = this.linkSelection.exit();
@@ -508,7 +504,7 @@ export class D3ForceGraph implements GraphRenderer {
deferred2.resolve();
}
return Q.allSettled([deferred1.promise, deferred2.promise]).then(undefined);
return Promise.all([deferred1.promise, deferred2.promise]).then(undefined);
}
/**

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import React from "react";
import { mount, ReactWrapper } from "enzyme";
import * as Q from "q";
import { NodePropertiesComponent, NodePropertiesComponentProps, Mode } from "./NodePropertiesComponent";
import { GraphHighlightedNodeData, EditedProperties, EditedEdges, PossibleVertex } from "./GraphExplorer";
@@ -41,11 +40,11 @@ describe("Property pane", () => {
node: highlightedNode,
getPkIdFromNodeData: (v: GraphHighlightedNodeData): string => null,
collectionPartitionKeyProperty: null,
updateVertexProperties: (editedProperties: EditedProperties): Q.Promise<void> => Q.resolve(),
updateVertexProperties: (editedProperties: EditedProperties): Promise<void> => Promise.resolve(),
selectNode: (id: string): void => {},
updatePossibleVertices: (): Q.Promise<PossibleVertex[]> => Q.resolve(null),
updatePossibleVertices: async (): Promise<PossibleVertex[]> => null,
possibleEdgeLabels: null,
editGraphEdges: (editedEdges: EditedEdges): Q.Promise<any> => Q.resolve(),
editGraphEdges: async (editedEdges: EditedEdges): Promise<any> => undefined,
deleteHighlightedNode: (): void => {},
onModeChanged: (newMode: Mode): void => {},
viewMode: Mode.READONLY_PROP

View File

@@ -36,11 +36,11 @@ export interface NodePropertiesComponentProps {
node: GraphHighlightedNodeData;
getPkIdFromNodeData: (v: GraphHighlightedNodeData) => string;
collectionPartitionKeyProperty: string;
updateVertexProperties: (editedProperties: EditedProperties) => Q.Promise<void>;
updateVertexProperties: (editedProperties: EditedProperties) => Promise<void>;
selectNode: (id: string) => void;
updatePossibleVertices: () => Q.Promise<PossibleVertex[]>;
updatePossibleVertices: () => Promise<PossibleVertex[]>;
possibleEdgeLabels: Item[];
editGraphEdges: (editedEdges: EditedEdges) => Q.Promise<any>;
editGraphEdges: (editedEdges: EditedEdges) => Promise<any>;
deleteHighlightedNode: () => void;
onModeChanged: (newMode: Mode) => void;
viewMode: Mode; // If viewMode is specified in parent, keep state in sync with it

View File

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

View File

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

View File

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

View File

@@ -127,6 +127,7 @@
be shared across all containers within the database.</span>
</span>
</div>
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: databaseCreateNewShared() && databaseCreateNew()">
<!-- 1 -->
<throughput-input-autopilot-v3 params="{
@@ -157,6 +158,38 @@
</throughput-input-autopilot-v3>
</div>
<!-- /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 -->
</div>
@@ -178,6 +211,19 @@
data-bind="value: collectionId, attr: { 'aria-label': collectionIdTitle }">
</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 -->
<div class="seconddivpadding"
data-bind="visible: showIndexingOptionsForSharedThroughput() && !container.isPreferredApiMongoDB()">
@@ -241,6 +287,62 @@
</div>
<!-- Unlimited option button - End -->
</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 -->
<div class="tabcontent" data-bind="visible: isUnlimitedStorageSelected() || databaseHasSharedOffer()">
<div data-bind="visible: rupmVisible">
@@ -288,7 +390,7 @@
range of values and is likely to have evenly distributed access patterns.</span>
</span>
</p>
<input type="text" id="partitionKeyValue" data-test="addCollection-partitionKeyValue" aria-required="true" size="40"
<input type="text" data-test="addCollection-partitionKeyValue" aria-required="true" size="40"
class="textfontclr collid" data-bind="textInput: partitionKey,
attr: {
placeholder: partitionKeyPlaceholder,
@@ -340,6 +442,7 @@
<!-- Provision collection throughput checkbox - end -->
<!-- Provision collection throughput spinner - start -->
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: displayCollectionThroughput" data-test="addCollection-displayCollectionThroughput">
<!-- 3 -->
<throughput-input-autopilot-v3 params="{
@@ -369,6 +472,40 @@
}">
</throughput-input-autopilot-v3>
</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 -->
<!-- /ko -->
<!-- Provision collection throughput - end -->

View File

@@ -1,7 +1,8 @@
import * as Constants from "../../Common/Constants";
import AddCollectionPane from "./AddCollectionPane";
import Explorer from "../Explorer";
import { DatabaseAccount } from "../../Contracts/DataModels";
import ko from "knockout";
import { AutopilotTier, DatabaseAccount } from "../../Contracts/DataModels";
describe("Add Collection Pane", () => {
describe("isValid()", () => {
@@ -40,6 +41,25 @@ describe("Add Collection Pane", () => {
beforeEach(() => {
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", () => {

View File

@@ -3,6 +3,7 @@ import * as AddCollectionUtility from "../../Shared/AddCollectionUtility";
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
import * as ko from "knockout";
import * as PricingUtils from "../../Utils/PricingUtils";
import * as SharedConstants from "../../Shared/Constants";
@@ -13,8 +14,8 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import { configContext, Platform } from "../../ConfigContext";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
import { refreshCachedResources } from "../../Common/DocumentClientUtilityBase";
import { createCollection } from "../../Common/dataAccess/createCollection";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
isPreferredApiTable: ko.Computed<boolean>;
@@ -75,6 +76,10 @@ export default class AddCollectionPane extends ContextualPaneBase {
public debugstring: ko.Computed<string>;
public displayCollectionThroughput: ko.Computed<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 autoPilotThroughput: ko.Observable<number>;
public sharedAutoPilotThroughput: ko.Observable<number>;
@@ -88,6 +93,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
public isAnalyticalStorageOn: ko.Observable<boolean>;
public isSynapseLinkUpdating: ko.Computed<boolean>;
public canExceedMaximumValue: ko.PureComputed<boolean>;
public hasAutoPilotV2FeatureFlag: ko.PureComputed<boolean>;
public ruToolTipText: ko.Computed<string>;
public canConfigureThroughput: ko.PureComputed<boolean>;
public showUpsellMessage: ko.PureComputed<boolean>;
@@ -97,7 +103,8 @@ export default class AddCollectionPane extends ContextualPaneBase {
constructor(options: AddCollectionPaneOptions) {
super(options);
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText());
this.hasAutoPilotV2FeatureFlag = ko.pureComputed(() => this.container.hasAutoPilotV2FeatureFlag());
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText(this.hasAutoPilotV2FeatureFlag()));
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.formWarnings = ko.observable<string>();
@@ -165,13 +172,13 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.databaseHasSharedOffer = ko.observable<boolean>(true);
this.throughputRangeText = ko.pureComputed<string>(() => {
if (this.isAutoPilotSelected()) {
return AutoPilotUtils.getAutoPilotHeaderText();
return AutoPilotUtils.getAutoPilotHeaderText(this.hasAutoPilotV2FeatureFlag());
}
return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`;
});
this.sharedThroughputRangeText = ko.pureComputed<string>(() => {
if (this.isSharedAutoPilotSelected()) {
return AutoPilotUtils.getAutoPilotHeaderText();
return AutoPilotUtils.getAutoPilotHeaderText(this.hasAutoPilotV2FeatureFlag());
}
return `Throughput (${this.minThroughputRU().toLocaleString()} - ${this.maxThroughputRU().toLocaleString()} RU/s)`;
});
@@ -441,7 +448,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.throughputSpendAckVisible = ko.pureComputed<boolean>(() => {
const autoscaleThroughput = this.autoPilotThroughput() * 1;
if (this.isAutoPilotSelected()) {
if (!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected()) {
return autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K;
}
const selectedThroughput: number = this._getThroughput();
@@ -484,6 +491,14 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.isAutoPilotSelected = 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.sharedAutoPilotThroughput = ko.observable<number>(AutoPilotUtils.minAutoPilotThroughput);
this.autoPilotUsageCost = ko.pureComputed<string>(() => {
@@ -492,7 +507,9 @@ export default class AddCollectionPane extends ContextualPaneBase {
return "";
}
const isDatabaseThroughput: boolean = this.databaseCreateNewShared();
return PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, isDatabaseThroughput);
return !this.hasAutoPilotV2FeatureFlag()
? PricingUtils.getAutoPilotV3SpendHtml(autoPilot.maxThroughput, isDatabaseThroughput)
: PricingUtils.getAutoPilotV2SpendHtml(autoPilot.autopilotTier, isDatabaseThroughput);
});
this.resetData();
@@ -879,11 +896,14 @@ export default class AddCollectionPane extends ContextualPaneBase {
};
TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneSuccessMessage, startKey);
this.resetData();
this.container.refreshAllDatabases();
return refreshCachedResources().then(() => {
this.container.refreshAllDatabases();
});
},
(error: any) => {
(reason: any) => {
this.isExecuting(false);
const errorMessage: string = getErrorMessage(error);
const message = ErrorParserUtility.parse(reason);
const errorMessage = ErrorParserUtility.replaceKnownError(message[0].message);
this.formErrors(errorMessage);
this.formErrorsDetails(errorMessage);
const addCollectionPaneFailedMessage = {
@@ -911,8 +931,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
flight: this.container.flight()
},
dataExplorerArea: Constants.Areas.ContextualPane,
error: errorMessage,
errorStack: getErrorStack(error)
error: reason
};
TelemetryProcessor.traceFailure(Action.CreateCollection, addCollectionPaneFailedMessage, startKey);
}
@@ -926,9 +945,13 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.throughputSpendAck(false);
this.isAutoPilotSelected(false);
this.isSharedAutoPilotSelected(false);
this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
if (!this.hasAutoPilotV2FeatureFlag()) {
this.autoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
this.sharedAutoPilotThroughput(AutoPilotUtils.minAutoPilotThroughput);
} else {
this.selectedAutoPilotTier(undefined);
this.selectedSharedAutoPilotTier(undefined);
}
this.uniqueKeys([]);
this.useIndexingForSharedThroughput(true);
@@ -1007,12 +1030,17 @@ export default class AddCollectionPane extends ContextualPaneBase {
if ((this.databaseCreateNewShared() && this.isSharedAutoPilotSelected()) || this.isAutoPilotSelected()) {
const autoPilot = this._getAutoPilot();
if (
!autoPilot ||
!autoPilot.maxThroughput ||
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput)
(!this.hasAutoPilotV2FeatureFlag() &&
(!autoPilot ||
!autoPilot.maxThroughput ||
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput))) ||
(this.hasAutoPilotV2FeatureFlag() &&
(!autoPilot || !autoPilot.autopilotTier || !AutoPilotUtils.isValidAutoPilotTier(autoPilot.autopilotTier)))
) {
this.formErrors(
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
!this.hasAutoPilotV2FeatureFlag()
? `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
: "Please select an Autopilot tier from the list."
);
return false;
}
@@ -1042,6 +1070,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
const autoscaleThroughput = this.autoPilotThroughput() * 1;
if (
!this.hasAutoPilotV2FeatureFlag() &&
this.isAutoPilotSelected() &&
autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!this.throughputSpendAck()
@@ -1088,15 +1117,31 @@ export default class AddCollectionPane extends ContextualPaneBase {
}
private _getAutoPilot(): DataModels.AutoPilotCreationSettings {
if (this.databaseCreateNewShared() && this.isSharedAutoPilotSelected() && this.sharedAutoPilotThroughput()) {
return {
maxThroughput: this.sharedAutoPilotThroughput() * 1
};
if (
(!this.hasAutoPilotV2FeatureFlag() &&
this.databaseCreateNewShared() &&
this.isSharedAutoPilotSelected() &&
this.sharedAutoPilotThroughput()) ||
(this.hasAutoPilotV2FeatureFlag() &&
this.databaseCreateNewShared() &&
this.isSharedAutoPilotSelected() &&
this.selectedSharedAutoPilotTier())
) {
return !this.hasAutoPilotV2FeatureFlag()
? {
maxThroughput: this.sharedAutoPilotThroughput() * 1
}
: { autopilotTier: this.selectedSharedAutoPilotTier() };
}
if (this.isAutoPilotSelected() && this.autoPilotThroughput()) {
return {
maxThroughput: this.autoPilotThroughput() * 1
};
if (
(!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.autoPilotThroughput()) ||
(this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.selectedAutoPilotTier())
) {
return !this.hasAutoPilotV2FeatureFlag()
? {
maxThroughput: this.autoPilotThroughput() * 1
}
: { autopilotTier: this.selectedAutoPilotTier() };
}
return undefined;

View File

@@ -89,6 +89,7 @@
data-bind="text: databaseLevelThroughputTooltipText"></span>
</span>
</div>
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: databaseCreateNewShared">
<throughput-input-autopilot-v3 params="{
step: 100,
@@ -123,6 +124,42 @@
support</a> for more than <span data-bind="text: maxThroughputRUText"></span> RU/s.</p>
</div>
<!-- /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 -->
</div>
</div>

View File

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

View File

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

View File

@@ -142,6 +142,7 @@
</span>
</div>
<!-- 1 -->
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: keyspaceCreateNew() && keyspaceHasSharedOffer()">
<throughput-input-autopilot-v3
params="{
@@ -172,6 +173,38 @@
</throughput-input-autopilot-v3>
</div>
<!-- /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 -->
</div>
<div class="seconddivpadding">
@@ -224,6 +257,7 @@
</span>
</div>
<!-- 2 -->
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: !keyspaceHasSharedOffer() || dedicateTableThroughput()">
<throughput-input-autopilot-v3
params="{
@@ -255,6 +289,40 @@
</throughput-input-autopilot-v3>
</div>
<!-- /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 -->
</div>
<div class="paneFooter">

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