Compare commits

..

1 Commits

Author SHA1 Message Date
Sweatha Viswanathan
35b15a1e20 Container pool changes to plumb auth 2021-01-05 17:23:11 -08:00
108 changed files with 2457 additions and 2457 deletions

View File

@@ -14,6 +14,7 @@ src/Common/DataAccessUtilityBase.ts
src/Common/DeleteFeedback.ts src/Common/DeleteFeedback.ts
src/Common/DocumentClientUtilityBase.ts src/Common/DocumentClientUtilityBase.ts
src/Common/EditableUtility.ts src/Common/EditableUtility.ts
src/Common/EnvironmentUtility.ts
src/Common/HashMap.test.ts src/Common/HashMap.test.ts
src/Common/HashMap.ts src/Common/HashMap.ts
src/Common/HeadersUtility.test.ts src/Common/HeadersUtility.test.ts

View File

@@ -101,7 +101,6 @@ jobs:
PLATFORM: "Emulator" PLATFORM: "Emulator"
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
if: failure()
with: with:
name: screenshots name: screenshots
path: failed-* path: failed-*
@@ -160,7 +159,6 @@ jobs:
TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }} TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }}
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html" DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html"
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
if: failure()
with: with:
name: screenshots name: screenshots
path: failed-* path: failed-*

25
.github/workflows/runners.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Runners
on:
schedule:
- cron: "0 * 1 * *"
jobs:
sqlcreatecollection:
runs-on: ubuntu-latest
name: "SQL | Create Collection"
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- run: npm ci
- run: npm run test:e2e
env:
PORTAL_RUNNER_APP_INSIGHTS_KEY: ${{ secrets.PORTAL_RUNNER_APP_INSIGHTS_KEY }}
PORTAL_RUNNER_USERNAME: ${{ secrets.PORTAL_RUNNER_USERNAME }}
PORTAL_RUNNER_PASSWORD: ${{ secrets.PORTAL_RUNNER_PASSWORD }}
PORTAL_RUNNER_SUBSCRIPTION: 69e02f2d-f059-4409-9eac-97e8a276ae2c
PORTAL_RUNNER_RESOURCE_GROUP: runners
PORTAL_RUNNER_DATABASE_ACCOUNT: portal-sql-runner
- uses: actions/upload-artifact@v2
if: failure()
with:
name: screenshots
path: failure.png

View File

@@ -13,18 +13,29 @@ UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), ht
### Watch mode ### Watch mode
Run `npm start` to start the development server and automatically rebuild on changes Run `npm run watch` to start the development server and automatically rebuild on changes
### Hosted Development (https://cosmos.azure.com) ### Specifying Development Platform
- Visit: `https://localhost:1234/hostedExplorer.html` Setting the environment variable `PLATFORM` during the build process will force the explorer to load the specified platform. By default in development it will run in `Hosted` mode. Valid options:
- Local sign in via AAD will NOT work. Connection string only in dev mode. Use the Portal if you need AAD auth.
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine. - Hosted
- Emulator
- Portal
`PLATFORM=Emulator npm run watch`
### Hosted Development
The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
To run pure hosted mode, in `webpack.config.js` change index HtmlWebpackPlugin to use hostedExplorer.html and change entry for index to use HostedExplorer.ts.
### Emulator Development ### Emulator Development
- Start the Cosmos Emulator 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.
- Visit: https://localhost:1234/index.html
`PLATFORM=Emulator EMULATOR_ENDPOINT=https://my-vm.azure.com:8081 npm run watch`
#### Setting up a Remote Emulator #### Setting up a Remote Emulator
@@ -44,8 +55,16 @@ The Cosmos emulator currently only runs in Windows environments. You can still d
### Portal Development ### Portal Development
- Visit: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html The Cosmos Portal that consumes this repo is not currently open source. If you have access to this project, `npm run build` will copy the built files over to the portal where they will be loaded by the portal development environment
- You may have to manually visit https://localhost:1234/explorer.html first and click through any SSL certificate warnings
You can however load a local running instance of data explorer in the production portal.
1. Turn off browser SSL validation for localhost: chrome://flags/#allow-insecure-localhost OR Install valid SSL certs for localhost (on IE, follow these [instructions](https://www.technipages.com/ie-bypass-problem-with-this-websites-security-certificate) to install the localhost certificate in the right place)
2. Allowlist `https://localhost:1234` domain for CORS in the Azure Cosmos DB portal
3. Start the project in portal mode: `PLATFORM=Portal npm run watch`
4. Load the portal using the following link: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html
Live reload will occur, but data explorer will not properly integrate again with the parent iframe. You will have to manually reload the page.
### Testing ### Testing

1082
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,15 +6,15 @@
"dependencies": { "dependencies": {
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "3.9.0", "@azure/cosmos": "3.9.0",
"@azure/identity": "1.1.0",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.1.0",
"@jupyterlab/services": "6.0.0-rc.2", "@jupyterlab/services": "6.0.0-rc.2",
"@jupyterlab/terminal": "3.0.0-rc.2", "@jupyterlab/terminal": "3.0.0-rc.2",
"@microsoft/applicationinsights-web": "2.5.9", "@microsoft/applicationinsights-web": "2.5.9",
"@nteract/commutable": "7.3.2", "@nteract/commutable": "7.3.2",
"@nteract/connected-components": "6.8.2", "@nteract/connected-components": "6.8.2",
"@nteract/core": "15.1.0", "@nteract/core": "15.1.0",
"@nteract/data-explorer": "8.0.3", "@nteract/data-explorer": "8.2.9",
"@nteract/directory-listing": "2.0.6", "@nteract/directory-listing": "2.0.6",
"@nteract/dropdown-menu": "1.0.1", "@nteract/dropdown-menu": "1.0.1",
"@nteract/editor": "10.1.2", "@nteract/editor": "10.1.2",

View File

@@ -3,6 +3,7 @@
"offerThroughput": 400, "offerThroughput": 400,
"databaseLevelThroughput": false, "databaseLevelThroughput": false,
"collectionId": "Persons", "collectionId": "Persons",
"rupmEnabled": false,
"partitionKey": { "kind": "Hash", "paths": ["/name"] }, "partitionKey": { "kind": "Hash", "paths": ["/name"] },
"data": [ "data": [
"g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)", "g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)",
@@ -12,4 +13,4 @@
"g.V('1').addE('knows').to(g.V('2')).outV().addE('knows').to(g.V('3'))", "g.V('1').addE('knows').to(g.V('2')).outV().addE('knows').to(g.V('3'))",
"g.V('3').addE('knows').to(g.V('4'))" "g.V('3').addE('knows').to(g.V('4'))"
] ]
} }

View File

@@ -108,11 +108,13 @@ export class CapabilityNames {
export class Features { export class Features {
public static readonly cosmosdb = "cosmosdb"; public static readonly cosmosdb = "cosmosdb";
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy"; public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
public static readonly enableRupm = "enablerupm";
public static readonly executeSproc = "dataexplorerexecutesproc"; public static readonly executeSproc = "dataexplorerexecutesproc";
public static readonly hostedDataExplorer = "hosteddataexplorerenabled"; public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
public static readonly enableTtl = "enablettl"; public static readonly enableTtl = "enablettl";
public static readonly enableNotebooks = "enablenotebooks"; public static readonly enableNotebooks = "enablenotebooks";
public static readonly enableGalleryPublish = "enablegallerypublish"; public static readonly enableGalleryPublish = "enablegallerypublish";
public static readonly enableCodeOfConduct = "enablecodeofconduct";
public static readonly enableLinkInjection = "enablelinkinjection"; public static readonly enableLinkInjection = "enablelinkinjection";
public static readonly enableSpark = "enablespark"; public static readonly enableSpark = "enablespark";
public static readonly livyEndpoint = "livyendpoint"; public static readonly livyEndpoint = "livyendpoint";
@@ -179,6 +181,11 @@ export class CassandraBackend {
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema"; public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
} }
export class RUPMStates {
public static on: string = "on";
public static off: string = "off";
}
export class Queries { export class Queries {
public static CustomPageOption: string = "custom"; public static CustomPageOption: string = "custom";
public static UnlimitedPageOption: string = "unlimited"; public static UnlimitedPageOption: string = "unlimited";

View File

@@ -1,13 +1,13 @@
import { getCommonQueryOptions } from "./queryDocuments"; import { getCommonQueryOptions } from "./DataAccessUtilityBase";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
describe("getCommonQueryOptions", () => { describe("getCommonQueryOptions", () => {
it("builds the correct default options objects", () => { it("builds the correct default options objects", () => {
expect(getCommonQueryOptions({})).toMatchSnapshot(); expect(getCommonQueryOptions({})).toMatchSnapshot();
}); });
it("reads from localStorage", () => { it("reads from localStorage", () => {
LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37); LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37);
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17); LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17);
expect(getCommonQueryOptions({})).toMatchSnapshot(); expect(getCommonQueryOptions({})).toMatchSnapshot();
}); });
}); });

View File

@@ -0,0 +1,169 @@
import { ConflictDefinition, FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import Q from "q";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import ConflictId from "../Explorer/Tree/ConflictId";
import DocumentId from "../Explorer/Tree/DocumentId";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
import * as Constants from "./Constants";
import { client } from "./CosmosClient";
export function getCommonQueryOptions(options: FeedOptions): any {
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
options = options || {};
options.populateQueryMetrics = true;
options.enableScanInQuery = options.enableScanInQuery || true;
if (!options.partitionKey) {
options.forceQueryPlan = true;
}
options.maxItemCount =
options.maxItemCount ||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
Constants.Queries.itemsPerPage;
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
return options;
}
export function queryDocuments(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
options = getCommonQueryOptions(options);
const documentsIterator = client()
.database(databaseId)
.container(containerId)
.items.query(query, options);
return Q(documentsIterator);
}
export function getPartitionKeyHeaderForConflict(conflictId: ConflictId): Object {
const partitionKeyDefinition: DataModels.PartitionKey = conflictId.partitionKey;
const partitionKeyValue: any = conflictId.partitionKeyValue;
return getPartitionKeyHeader(partitionKeyDefinition, partitionKeyValue);
}
export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.PartitionKey, partitionKeyValue: any): Object {
if (!partitionKeyDefinition) {
return undefined;
}
if (partitionKeyValue === undefined) {
return [{}];
}
return [partitionKeyValue];
}
export function updateDocument(
collection: ViewModels.CollectionBase,
documentId: DocumentId,
newDocument: any
): Q.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)
);
}
export function executeStoredProcedure(
collection: ViewModels.Collection,
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({
result: response.resource,
scriptLogs: response.headers[Constants.HttpHeaders.scriptLogResults]
})
)
.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 readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
const partitionKey = documentId.partitionKeyValue;
return Q(
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> {
const partitionKey = documentId.partitionKeyValue;
return Q(
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> {
options.partitionKey = options.partitionKey || getPartitionKeyHeaderForConflict(conflictId);
return Q(
client()
.database(collection.databaseId)
.container(collection.id())
.conflict(conflictId.id())
.delete(options)
);
}
export function queryConflicts(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
const documentsIterator = client()
.database(databaseId)
.container(containerId)
.conflicts.query(query, options);
return Q(documentsIterator);
}

View File

@@ -0,0 +1,217 @@
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";
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
import { logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import * as Constants from "./Constants";
import * as DataAccessUtilityBase from "./DataAccessUtilityBase";
import { MinimalQueryIterator, nextPage } from "./IteratorUtilities";
import { handleError } from "./ErrorHandlingUtils";
// TODO: Log all promise resolutions and errors with verbosity levels
export function queryDocuments(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
return DataAccessUtilityBase.queryDocuments(databaseId, containerId, query, options);
}
export function queryConflicts(
databaseId: string,
containerId: string,
query: string,
options: any
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
return DataAccessUtilityBase.queryConflicts(databaseId, containerId, query, options);
}
export function getEntityName() {
const defaultExperience =
window.dataExplorer && window.dataExplorer.defaultExperience && window.dataExplorer.defaultExperience();
if (defaultExperience === Constants.DefaultAccountExperience.MongoDB) {
return "document";
}
return "item";
}
export function executeStoredProcedure(
collection: ViewModels.Collection,
storedProcedure: StoredProcedure,
partitionKeyValue: any,
params: any[]
): Q.Promise<any> {
var deferred = Q.defer<any>();
const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`);
DataAccessUtilityBase.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params)
.then(
(response: any) => {
deferred.resolve(response);
logConsoleInfo(
`Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
);
},
(error: any) => {
handleError(
error,
"ExecuteStoredProcedure",
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function queryDocumentsPage(
resourceName: string,
documentsIterator: MinimalQueryIterator,
firstItemIndex: number,
options: any
): Q.Promise<ViewModels.QueryResults> {
var deferred = Q.defer<ViewModels.QueryResults>();
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
Q(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);
},
(error: any) => {
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
var deferred = Q.defer<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;
}
export function updateDocument(
collection: ViewModels.CollectionBase,
documentId: DocumentId,
newDocument: any
): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`);
DataAccessUtilityBase.updateDocument(collection, documentId, newDocument)
.then(
(updatedDocument: any) => {
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
deferred.resolve(updatedDocument);
},
(error: any) => {
handleError(error, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`);
DataAccessUtilityBase.createDocument(collection, newDocument)
.then(
(savedDocument: any) => {
logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`);
deferred.resolve(savedDocument);
},
(error: any) => {
handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
var deferred = Q.defer<any>();
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`);
DataAccessUtilityBase.deleteDocument(collection, documentId)
.then(
(response: any) => {
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
deferred.resolve(response);
},
(error: any) => {
handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}
export function deleteConflict(
collection: ViewModels.CollectionBase,
conflictId: ConflictId,
options?: any
): Q.Promise<any> {
var deferred = Q.defer<any>();
const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`);
DataAccessUtilityBase.deleteConflict(collection, conflictId, options)
.then(
(response: any) => {
logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`);
deferred.resolve(response);
},
(error: any) => {
handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`);
deferred.reject(error);
}
)
.finally(() => {
clearMessage();
});
return deferred.promise;
}

View File

@@ -1,10 +0,0 @@
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
import { userContext } from "../UserContext";
export const getEntityName = (): string => {
if (userContext.defaultExperience === DefaultAccountExperienceType.MongoDB) {
return "document";
}
return "item";
};

View File

@@ -1,6 +1,8 @@
export function normalizeArmEndpoint(uri: string): string { export default class EnvironmentUtility {
if (uri && uri.slice(-1) !== "/") { public static normalizeArmEndpointUri(uri: string): string {
return `${uri}/`; if (uri && uri.slice(-1) !== "/") {
return `${uri}/`;
}
return uri;
} }
return uri;
} }

View File

@@ -24,8 +24,7 @@ describe("parseSDKOfferResponse", () => {
autoscaleMaxThroughput: undefined, autoscaleMaxThroughput: undefined,
minimumThroughput: 400, minimumThroughput: 400,
id: "test", id: "test",
offerDefinition: mockOfferDefinition, offerDefinition: mockOfferDefinition
offerReplacePending: false
}; };
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult); expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
@@ -55,8 +54,7 @@ describe("parseSDKOfferResponse", () => {
autoscaleMaxThroughput: 5000, autoscaleMaxThroughput: 5000,
minimumThroughput: 400, minimumThroughput: 400,
id: "test", id: "test",
offerDefinition: mockOfferDefinition, offerDefinition: mockOfferDefinition
offerReplacePending: false
}; };
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult); expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);

View File

@@ -1,6 +1,5 @@
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels"; import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
import { OfferResponse } from "@azure/cosmos"; import { OfferResponse } from "@azure/cosmos";
import { HttpHeaders } from "./Constants";
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => { export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
const offerDefinition: SDKOfferDefinition = offerResponse?.resource; const offerDefinition: SDKOfferDefinition = offerResponse?.resource;
@@ -19,7 +18,7 @@ export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
manualThroughput: undefined, manualThroughput: undefined,
minimumThroughput, minimumThroughput,
offerDefinition, offerDefinition,
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true" headers: offerResponse.headers
}; };
} }
@@ -29,6 +28,6 @@ export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
manualThroughput: offerContent.offerThroughput, manualThroughput: offerContent.offerThroughput,
minimumThroughput, minimumThroughput,
offerDefinition, offerDefinition,
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true" headers: offerResponse.headers
}; };
}; };

View File

@@ -3,18 +3,16 @@ import * as _ from "underscore";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { QueryUtils } from "../Utils/QueryUtils"; import { QueryUtils } from "../Utils/QueryUtils";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage"; import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase";
import { createCollection } from "./dataAccess/createCollection"; import { createCollection } from "./dataAccess/createCollection";
import { handleError } from "./ErrorHandlingUtils"; import { handleError } from "./ErrorHandlingUtils";
import { createDocument } from "./dataAccess/createDocument";
import { deleteDocument } from "./dataAccess/deleteDocument";
import { queryDocuments } from "./dataAccess/queryDocuments";
export class QueriesClient { export class QueriesClient {
private static readonly PartitionKey: DataModels.PartitionKey = { private static readonly PartitionKey: DataModels.PartitionKey = {
@@ -33,7 +31,10 @@ export class QueriesClient {
return Promise.resolve(queriesCollection.rawDataModel); return Promise.resolve(queriesCollection.rawDataModel);
} }
const clearMessage = NotificationConsoleUtils.logConsoleProgress("Setting up account for saving queries"); const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
"Setting up account for saving queries"
);
return createCollection({ return createCollection({
collectionId: SavedQueries.CollectionName, collectionId: SavedQueries.CollectionName,
createNewDatabase: true, createNewDatabase: true,
@@ -44,7 +45,10 @@ export class QueriesClient {
}) })
.then( .then(
(collection: DataModels.Collection) => { (collection: DataModels.Collection) => {
NotificationConsoleUtils.logConsoleInfo("Successfully set up account for saving queries"); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
"Successfully set up account for saving queries"
);
return Promise.resolve(collection); return Promise.resolve(collection);
}, },
(error: any) => { (error: any) => {
@@ -52,14 +56,17 @@ export class QueriesClient {
return Promise.reject(error); return Promise.reject(error);
} }
) )
.finally(() => clearMessage()); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
} }
public async saveQuery(query: DataModels.Query): Promise<void> { public async saveQuery(query: DataModels.Query): Promise<void> {
const queriesCollection = this.findQueriesCollection(); const queriesCollection = this.findQueriesCollection();
if (!queriesCollection) { if (!queriesCollection) {
const errorMessage: string = "Account not set up to perform saved query operations"; const errorMessage: string = "Account not set up to perform saved query operations";
NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to save query ${query.queryName}: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
@@ -67,16 +74,25 @@ export class QueriesClient {
this.validateQuery(query); this.validateQuery(query);
} catch (error) { } catch (error) {
const errorMessage: string = "Invalid query specified"; const errorMessage: string = "Invalid query specified";
NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to save query ${query.queryName}: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Saving query ${query.queryName}`); const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Saving query ${query.queryName}`
);
query.id = query.queryName; query.id = query.queryName;
return createDocument(queriesCollection, query) return createDocument(queriesCollection, query)
.then( .then(
(savedQuery: DataModels.Query) => { (savedQuery: DataModels.Query) => {
NotificationConsoleUtils.logConsoleInfo(`Successfully saved query ${query.queryName}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully saved query ${query.queryName}`
);
return Promise.resolve(); return Promise.resolve();
}, },
(error: any) => { (error: any) => {
@@ -87,65 +103,74 @@ export class QueriesClient {
return Promise.reject(error); return Promise.reject(error);
} }
) )
.finally(() => clearMessage()); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
} }
public async getQueries(): Promise<DataModels.Query[]> { public async getQueries(): Promise<DataModels.Query[]> {
const queriesCollection = this.findQueriesCollection(); const queriesCollection = this.findQueriesCollection();
if (!queriesCollection) { if (!queriesCollection) {
const errorMessage: string = "Account not set up to perform saved query operations"; const errorMessage: string = "Account not set up to perform saved query operations";
NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to fetch saved queries: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
const options: any = { enableCrossPartitionQuery: true }; const options: any = { enableCrossPartitionQuery: true };
const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries"); const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries");
const queryIterator: QueryIterator<ItemDefinition & Resource> = queryDocuments( return queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options)
SavedQueries.DatabaseName,
SavedQueries.CollectionName,
this.fetchQueriesQuery(),
options
);
const fetchQueries = async (firstItemIndex: number): Promise<ViewModels.QueryResults> =>
await queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex);
return QueryUtils.queryAllPages(fetchQueries)
.then( .then(
(results: ViewModels.QueryResults) => { (queryIterator: QueryIterator<ItemDefinition & Resource>) => {
let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => { const fetchQueries = (firstItemIndex: number): Q.Promise<ViewModels.QueryResults> =>
if (!document) { queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex, options);
return undefined; return QueryUtils.queryAllPages(fetchQueries).then(
(results: ViewModels.QueryResults) => {
let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => {
if (!document) {
return undefined;
}
const { id, resourceId, query, queryName } = document;
const parsedQuery: DataModels.Query = {
resourceId: resourceId,
queryName: queryName,
query: query,
id: id
};
try {
this.validateQuery(parsedQuery);
return parsedQuery;
} catch (error) {
return undefined;
}
});
queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully fetched saved queries");
return Promise.resolve(queries);
},
(error: any) => {
handleError(error, "getSavedQueries", "Failed to fetch saved queries");
return Promise.reject(error);
} }
const { id, resourceId, query, queryName } = document; );
const parsedQuery: DataModels.Query = {
resourceId: resourceId,
queryName: queryName,
query: query,
id: id
};
try {
this.validateQuery(parsedQuery);
return parsedQuery;
} catch (error) {
return undefined;
}
});
queries = _.reject(queries, (parsedQuery: DataModels.Query) => !parsedQuery);
NotificationConsoleUtils.logConsoleInfo("Successfully fetched saved queries");
return Promise.resolve(queries);
}, },
(error: any) => { (error: any) => {
// should never get into this state but we handle this regardless
handleError(error, "getSavedQueries", "Failed to fetch saved queries"); handleError(error, "getSavedQueries", "Failed to fetch saved queries");
return Promise.reject(error); return Promise.reject(error);
} }
) )
.finally(() => clearMessage()); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
} }
public async deleteQuery(query: DataModels.Query): Promise<void> { public async deleteQuery(query: DataModels.Query): Promise<void> {
const queriesCollection = this.findQueriesCollection(); const queriesCollection = this.findQueriesCollection();
if (!queriesCollection) { if (!queriesCollection) {
const errorMessage: string = "Account not set up to perform saved query operations"; const errorMessage: string = "Account not set up to perform saved query operations";
NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to fetch saved queries: ${errorMessage}`
);
return Promise.reject(errorMessage); return Promise.reject(errorMessage);
} }
@@ -153,10 +178,16 @@ export class QueriesClient {
this.validateQuery(query); this.validateQuery(query);
} catch (error) { } catch (error) {
const errorMessage: string = "Invalid query specified"; const errorMessage: string = "Invalid query specified";
NotificationConsoleUtils.logConsoleError(`Failed to delete query ${query.queryName}: ${errorMessage}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to delete query ${query.queryName}: ${errorMessage}`
);
} }
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting query ${query.queryName}`); const id = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Deleting query ${query.queryName}`
);
query.id = query.queryName; query.id = query.queryName;
const documentId = new DocumentId( const documentId = new DocumentId(
{ {
@@ -170,7 +201,10 @@ export class QueriesClient {
return deleteDocument(queriesCollection, documentId) return deleteDocument(queriesCollection, documentId)
.then( .then(
() => { () => {
NotificationConsoleUtils.logConsoleInfo(`Successfully deleted query ${query.queryName}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully deleted query ${query.queryName}`
);
return Promise.resolve(); return Promise.resolve();
}, },
(error: any) => { (error: any) => {
@@ -178,7 +212,7 @@ export class QueriesClient {
return Promise.reject(error); return Promise.reject(error);
} }
) )
.finally(() => clearMessage()); .finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
} }
public getResourceId(): string { public getResourceId(): string {

View File

@@ -1,5 +1,6 @@
jest.mock("../../Utils/arm/request"); jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient"); jest.mock("../CosmosClient");
jest.mock("../DataAccessUtilityBase");
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels"; import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType"; import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";

View File

@@ -1,25 +0,0 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
export const createDocument = async (collection: CollectionBase, newDocument: unknown): Promise<unknown> => {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`);
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.items.create(newDocument);
logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`);
return response?.resource;
} catch (error) {
handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,36 +0,0 @@
import ConflictId from "../../Explorer/Tree/ConflictId";
import { CollectionBase } from "../../Contracts/ViewModels";
import { RequestOptions } from "@azure/cosmos";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
export const deleteConflict = async (collection: CollectionBase, conflictId: ConflictId): Promise<void> => {
const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`);
try {
const options = {
partitionKey: getPartitionKeyHeaderForConflict(conflictId)
};
await client()
.database(collection.databaseId)
.container(collection.id())
.conflict(conflictId.id())
.delete(options as RequestOptions);
logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`);
} catch (error) {
handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`);
throw error;
} finally {
clearMessage();
}
};
const getPartitionKeyHeaderForConflict = (conflictId: ConflictId): unknown => {
if (!conflictId.partitionKey) {
return undefined;
}
return conflictId.partitionKeyValue === undefined ? [{}] : [conflictId.partitionKeyValue];
};

View File

@@ -1,25 +0,0 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<void> => {
const entityName: string = getEntityName();
const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`);
try {
await client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), documentId.partitionKeyValue)
.delete();
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
} catch (error) {
handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,48 +0,0 @@
import { Collection } from "../../Contracts/ViewModels";
import { ClientDefaults, HttpHeaders } from "../Constants";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import StoredProcedure from "../../Explorer/Tree/StoredProcedure";
export interface ExecuteSprocResult {
result: StoredProcedure;
scriptLogs: string;
}
export const executeStoredProcedure = async (
collection: Collection,
storedProcedure: StoredProcedure,
partitionKeyValue: string,
params: string[]
): Promise<ExecuteSprocResult> => {
const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`);
const timeout = setTimeout(() => {
throw Error(`Request timed out while executing stored procedure ${storedProcedure.id()}`);
}, ClientDefaults.requestTimeoutMs);
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.scripts.storedProcedure(storedProcedure.id())
.execute(partitionKeyValue, params, { enableScriptLogging: true });
clearTimeout(timeout);
logConsoleInfo(
`Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
);
return {
result: response.resource,
scriptLogs: response.headers[HttpHeaders.scriptLogResults] as string
};
} catch (error) {
handleError(
error,
"ExecuteStoredProcedure",
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,92 +0,0 @@
import { AuthType } from "../../AuthType";
import { armRequest } from "../../Utils/arm/request";
import { configContext } from "../../ConfigContext";
import { handleError } from "../ErrorHandlingUtils";
import { userContext } from "../../UserContext";
interface TimeSeriesData {
data: {
timeStamp: string;
total: number;
}[];
metadatavalues: {
name: {
localizedValue: string;
value: string;
};
value: string;
};
}
interface MetricsData {
displayDescription: string;
errorCode: string;
id: string;
name: {
value: string;
localizedValue: string;
};
timeseries: TimeSeriesData[];
type: string;
unit: string;
}
interface MetricsResponse {
cost: number;
interval: string;
namespace: string;
resourceregion: string;
timespan: string;
value: MetricsData[];
}
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
if (window.authType !== AuthType.AAD) {
return undefined;
}
const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup;
const accountName = userContext.databaseAccount.name;
const filter = `DatabaseName eq '${databaseName}' and CollectionName eq '${containerName}'`;
const metricNames = "DataUsage,IndexUsage";
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/providers/microsoft.insights/metrics`;
try {
const metricsResponse: MetricsResponse = await armRequest({
host: configContext.ARM_ENDPOINT,
path,
method: "GET",
apiVersion: "2018-01-01",
queryParams: {
filter,
metricNames
}
});
if (metricsResponse?.value?.length !== 2) {
return undefined;
}
const dataUsageData: MetricsData = metricsResponse.value[0];
const indexUsagedata: MetricsData = metricsResponse.value[1];
const dataUsageSizeInKb: number = getUsageSizeInKb(dataUsageData);
const indexUsageSizeInKb: number = getUsageSizeInKb(indexUsagedata);
return dataUsageSizeInKb + indexUsageSizeInKb;
} catch (error) {
handleError(error, "getCollectionUsageSize");
throw error;
}
};
const getUsageSizeInKb = (metricsData: MetricsData): number => {
if (metricsData?.errorCode !== "Success") {
throw Error(`Get collection usage size failed: ${metricsData.errorCode}`);
}
const timeSeriesData: TimeSeriesData = metricsData?.timeseries?.[0];
const usageSizeInBytes: number = timeSeriesData?.data?.[0]?.total;
return usageSizeInBytes ? usageSizeInBytes / 1024 : 0;
};

View File

@@ -1,14 +0,0 @@
import { ConflictDefinition, FeedOptions, QueryIterator, Resource } from "@azure/cosmos";
import { client } from "../CosmosClient";
export const queryConflicts = (
databaseId: string,
containerId: string,
query: string,
options: FeedOptions
): QueryIterator<ConflictDefinition & Resource> => {
return client()
.database(databaseId)
.container(containerId)
.conflicts.query(query, options);
};

View File

@@ -1,34 +0,0 @@
import { Queries } from "../Constants";
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { client } from "../CosmosClient";
export const queryDocuments = (
databaseId: string,
containerId: string,
query: string,
options: FeedOptions
): QueryIterator<ItemDefinition & Resource> => {
options = getCommonQueryOptions(options);
return client()
.database(databaseId)
.container(containerId)
.items.query(query, options);
};
export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
options = options || {};
options.populateQueryMetrics = true;
options.enableScanInQuery = options.enableScanInQuery || true;
if (!options.partitionKey) {
options.forceQueryPlan = true;
}
options.maxItemCount =
options.maxItemCount ||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
Queries.itemsPerPage;
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
return options;
};

View File

@@ -1,26 +0,0 @@
import { QueryResults } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { MinimalQueryIterator, nextPage } from "../IteratorUtilities";
import { handleError } from "../ErrorHandlingUtils";
import { getEntityName } from "../DocumentUtility";
export const queryDocumentsPage = async (
resourceName: string,
documentsIterator: MinimalQueryIterator,
firstItemIndex: number
): Promise<QueryResults> => {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
try {
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex);
const itemCount = (result.documents && result.documents.length) || 0;
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
return result;
} catch (error) {
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -105,8 +105,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
id: offerId, id: offerId,
autoscaleMaxThroughput: autoscaleSettings.maxThroughput, autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
manualThroughput: undefined, manualThroughput: undefined,
minimumThroughput, minimumThroughput
offerReplacePending: resource.offerReplacePending === "true"
}; };
} }
@@ -114,8 +113,7 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
id: offerId, id: offerId,
autoscaleMaxThroughput: undefined, autoscaleMaxThroughput: undefined,
manualThroughput: resource.throughput, manualThroughput: resource.throughput,
minimumThroughput, minimumThroughput
offerReplacePending: resource.offerReplacePending === "true"
}; };
} }

View File

@@ -0,0 +1,45 @@
import * as DataModels from "../../Contracts/DataModels";
import * as HeadersUtility from "../HeadersUtility";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContainerDefinition, Resource } from "@azure/cosmos";
import { HttpHeaders } from "../Constants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
interface ResourceWithStatistics {
statistics: DataModels.Statistic[];
}
export const readCollectionQuotaInfo = async (
collection: ViewModels.Collection
): Promise<DataModels.CollectionQuotaInfo> => {
const clearMessage = logConsoleProgress(`Querying containers for database ${collection.id}`);
const options: RequestOptions = {};
options.populateQuotaInfo = true;
options.initialHeaders = options.initialHeaders || {};
options.initialHeaders[HttpHeaders.populatePartitionStatistics] = true;
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.read(options);
const quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers);
const resource = response.resource as ContainerDefinition & Resource & ResourceWithStatistics;
quota["usageSizeInKB"] = resource.statistics.reduce(
(previousValue: number, currentValue: DataModels.Statistic) => previousValue + currentValue.sizeInKB,
0
);
quota["numPartitions"] = resource.statistics.length;
quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617)
return quota;
} catch (error) {
handleError(error, "ReadCollectionQuotaInfo", `Error while querying quota info for container ${collection.id}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -77,8 +77,7 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
id: offerId, id: offerId,
autoscaleMaxThroughput: autoscaleSettings.maxThroughput, autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
manualThroughput: undefined, manualThroughput: undefined,
minimumThroughput, minimumThroughput
offerReplacePending: resource.offerReplacePending === "true"
}; };
} }
@@ -86,8 +85,7 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
id: offerId, id: offerId,
autoscaleMaxThroughput: undefined, autoscaleMaxThroughput: undefined,
manualThroughput: resource.throughput, manualThroughput: resource.throughput,
minimumThroughput, minimumThroughput
offerReplacePending: resource.offerReplacePending === "true"
}; };
} }

View File

@@ -1,27 +0,0 @@
import { Item } from "@azure/cosmos";
import { CollectionBase } from "../../Contracts/ViewModels";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<Item> => {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`);
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), documentId.partitionKeyValue)
.read();
return response?.resource;
} catch (error) {
handleError(error, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -1,32 +0,0 @@
import { CollectionBase } from "../../Contracts/ViewModels";
import { Item } from "@azure/cosmos";
import { client } from "../CosmosClient";
import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import DocumentId from "../../Explorer/Tree/DocumentId";
export const updateDocument = async (
collection: CollectionBase,
documentId: DocumentId,
newDocument: Item
): Promise<Item> => {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`);
try {
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), documentId.partitionKeyValue)
.replace(newDocument);
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
return response?.resource;
} catch (error) {
handleError(error, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`);
throw error;
} finally {
clearMessage();
}
};

View File

@@ -214,7 +214,7 @@ export interface Offer {
manualThroughput: number; manualThroughput: number;
minimumThroughput: number; minimumThroughput: number;
offerDefinition?: SDKOfferDefinition; offerDefinition?: SDKOfferDefinition;
offerReplacePending: boolean; headers?: any;
} }
export interface SDKOfferDefinition extends Resource { export interface SDKOfferDefinition extends Resource {
@@ -230,6 +230,18 @@ export interface SDKOfferDefinition extends Resource {
offerResourceId?: string; offerResourceId?: string;
} }
export interface CollectionQuotaInfo {
storedProcedures: number;
triggers: number;
functions: number;
documentsSize: number;
collectionSize: number;
documentsCount: number;
usageSizeInKB: number;
numPartitions: number;
uniqueKeyPolicy?: UniqueKeyPolicy; // TODO: This should ideally not be a part of the collection quota. Remove after refactoring. (#119617)
}
export interface OfferThroughputInfo { export interface OfferThroughputInfo {
minimumRUForCollection: number; minimumRUForCollection: number;
numPhysicalPartitions: number; numPhysicalPartitions: number;
@@ -248,6 +260,7 @@ export interface CreateDatabaseAndCollectionRequest {
collectionId: string; collectionId: string;
offerThroughput: number; offerThroughput: number;
databaseLevelThroughput: boolean; databaseLevelThroughput: boolean;
rupmEnabled?: boolean;
partitionKey?: PartitionKey; partitionKey?: PartitionKey;
indexingPolicy?: IndexingPolicy; indexingPolicy?: IndexingPolicy;
uniqueKeyPolicy?: UniqueKeyPolicy; uniqueKeyPolicy?: UniqueKeyPolicy;

View File

@@ -120,7 +120,7 @@ export interface Collection extends CollectionBase {
requestSchema?: () => void; requestSchema?: () => void;
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>; indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
uniqueKeyPolicy: DataModels.UniqueKeyPolicy; uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
usageSizeInKB: ko.Observable<number>; quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
offer: ko.Observable<DataModels.Offer>; offer: ko.Observable<DataModels.Offer>;
conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>; conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>; changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;

View File

@@ -44,10 +44,12 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
onChange?: (_?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void; onChange?: (_?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
}[] = [ }[] = [
{ key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" }, { key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" },
{ key: "feature.enablerupm", label: "Enable RUPM", value: "true" },
{ key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" }, { key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" },
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" }, { key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
{ key: "feature.enablettl", label: "Enable TTL", value: "true" }, { key: "feature.enablettl", label: "Enable TTL", value: "true" },
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" }, { key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
{ key: "feature.enablecodeofconduct", label: "Enable Code Of Conduct Acknowledgement", value: "true" },
{ {
key: "feature.enableLinkInjection", key: "feature.enableLinkInjection",
label: "Enable Injecting Notebook Viewer Link into the first cell", label: "Enable Injecting Notebook Viewer Link into the first cell",

View File

@@ -131,6 +131,12 @@ exports[`Feature panel renders all flags 1`] = `
label="Enable change feed policy" label="Enable change feed policy"
onChange={[Function]} onChange={[Function]}
/> />
<StyledCheckboxBase
checked={false}
key="feature.enablerupm"
label="Enable RUPM"
onChange={[Function]}
/>
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={false}
key="feature.dataexplorerexecutesproc" key="feature.dataexplorerexecutesproc"
@@ -157,14 +163,14 @@ exports[`Feature panel renders all flags 1`] = `
/> />
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={false}
key="feature.enableLinkInjection" key="feature.enablecodeofconduct"
label="Enable Injecting Notebook Viewer Link into the first cell" label="Enable Code Of Conduct Acknowledgement"
onChange={[Function]} onChange={[Function]}
/> />
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={false}
key="feature.canexceedmaximumvalue" key="feature.enableLinkInjection"
label="Can exceed max value" label="Enable Injecting Notebook Viewer Link into the first cell"
onChange={[Function]} onChange={[Function]}
/> />
</Stack> </Stack>
@@ -172,6 +178,12 @@ exports[`Feature panel renders all flags 1`] = `
className="checkboxRow" className="checkboxRow"
horizontalAlign="space-between" horizontalAlign="space-between"
> >
<StyledCheckboxBase
checked={false}
key="feature.canexceedmaximumvalue"
label="Can exceed max value"
onChange={[Function]}
/>
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={false}
key="feature.enablefixedcollectionwithsharedthroughput" key="feature.enablefixedcollectionwithsharedthroughput"

View File

@@ -39,6 +39,10 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint); params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
} }
params.set("account","contoso-retail-mongodb");
params.set("port","10255");
//tofill
params.set("token","");
return params; return params;
} }

View File

@@ -1,7 +1,6 @@
import { import {
Dropdown, Dropdown,
FocusZone, FocusZone,
FontIcon,
FontWeights, FontWeights,
IDropdownOption, IDropdownOption,
IPageSpecification, IPageSpecification,
@@ -17,7 +16,7 @@ import {
Text Text
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient"; import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils"; import * as GalleryUtils from "../../../Utils/GalleryUtils";
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent"; import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent"; import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
@@ -137,7 +136,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
} }
public render(): JSX.Element { public render(): JSX.Element {
const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)]; const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
if (this.props.container?.isGalleryPublishEnabled()) { if (this.props.container?.isGalleryPublishEnabled()) {
tabs.push( tabs.push(
@@ -147,7 +146,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
this.state.isCodeOfConductAccepted this.state.isCodeOfConductAccepted
) )
); );
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks)); tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined. // explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
// Displaying code of conduct component on gallery load should not be the default behavior. // Displaying code of conduct component on gallery load should not be the default behavior.
@@ -184,27 +183,6 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
); );
} }
private isEmptyData = (data: IGalleryItem[]): boolean => {
return !data || data.length === 0;
};
private createEmptyTabContent = (iconName: string, line1: string, line2: string): JSX.Element => {
return (
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}>
<FontIcon iconName={iconName} style={{ fontSize: 100, color: "lightgray", marginTop: 20 }} />
<Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{line1}</Text>
<Text>{line2}</Text>
</Stack>
);
};
private createSamplesTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
return {
tab,
content: this.createSearchBarHeader(this.createCardsTabContent(data))
};
};
private createPublicGalleryTab( private createPublicGalleryTab(
tab: GalleryTab, tab: GalleryTab,
data: IGalleryItem[], data: IGalleryItem[],
@@ -216,29 +194,17 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}; };
} }
private createFavoritesTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo { private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
return { return {
tab, tab,
content: this.isEmptyData(data) content: this.createSearchBarHeader(this.createCardsTabContent(data))
? this.createEmptyTabContent(
"ContactHeart",
"You have not liked anything",
"Like any notebook from Official Samples or Public gallery"
)
: this.createSearchBarHeader(this.createCardsTabContent(data))
}; };
} }
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => { private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
return { return {
tab, tab,
content: this.isEmptyData(data) content: this.createPublishedNotebooksTabContent(data)
? this.createEmptyTabContent(
"Contact",
"You have not published anything",
"Publish your sample notebooks to share your published work with others"
)
: this.createPublishedNotebooksTabContent(data)
}; };
}; };
@@ -398,9 +364,9 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> { private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
if (!offline) { if (!offline) {
try { try {
let response: IJunoResponse<IGalleryItem[]> | IJunoResponse<IPublicGalleryData>; let response: IJunoResponse<IPublicGalleryData> | IJunoResponse<IGalleryItem[]>;
if (this.props.container) { if (this.props.container.isCodeOfConductEnabled()) {
response = await this.props.junoClient.getPublicGalleryData(); response = await this.props.junoClient.fetchPublicNotebooks();
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct; this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
this.publicNotebooks = response.data?.notebooksData; this.publicNotebooks = response.data?.notebooksData;
} else { } else {
@@ -602,7 +568,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private deleteItem = async (data: IGalleryItem): Promise<void> => { private deleteItem = async (data: IGalleryItem): Promise<void> => {
GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, item => { GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, item => {
this.publishedNotebooks = this.publishedNotebooks?.filter(notebook => item.id !== notebook.id); this.publishedNotebooks = this.publishedNotebooks.filter(notebook => item.id !== notebook.id);
this.refreshSelectedTab(item); this.refreshSelectedTab(item);
}); });
}; };

View File

@@ -92,8 +92,7 @@ describe("SettingsComponent", () => {
autoscaleMaxThroughput: 10000, autoscaleMaxThroughput: 10000,
manualThroughput: undefined, manualThroughput: undefined,
minimumThroughput: 400, minimumThroughput: 400,
id: "test", id: "test"
offerReplacePending: false
}); });
const props = { ...baseProps }; const props = { ...baseProps };

View File

@@ -295,7 +295,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
!!this.collection.conflictResolutionPolicy(); !!this.collection.conflictResolutionPolicy();
public isOfferReplacePending = (): boolean => { public isOfferReplacePending = (): boolean => {
return this.collection?.offer()?.offerReplacePending; return !!this.collection?.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending];
}; };
public onSaveClick = async (): Promise<void> => { public onSaveClick = async (): Promise<void> => {

View File

@@ -31,7 +31,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
{getAutoPilotV3SpendElement(1000, true)} {getAutoPilotV3SpendElement(1000, true)}
{getAutoPilotV3SpendElement(undefined, true)} {getAutoPilotV3SpendElement(undefined, true)}
{getEstimatedSpendElement(1000, "mooncake", 2, false)} {getEstimatedSpendElement(1000, "mooncake", 2, false, true)}
{getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)} {getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)}

View File

@@ -199,9 +199,10 @@ export const getEstimatedSpendElement = (
throughput: number, throughput: number,
serverId: string, serverId: string,
regions: number, regions: number,
multimaster: boolean multimaster: boolean,
rupmEnabled: boolean
): JSX.Element => { ): JSX.Element => {
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster); const hourlyPrice: number = computeRUUsagePriceHourly(serverId, rupmEnabled, throughput, regions, multimaster);
const dailyPrice: number = hourlyPrice * 24; const dailyPrice: number = hourlyPrice * 24;
const monthlyPrice: number = hourlyPrice * hoursInAMonth; const monthlyPrice: number = hourlyPrice * hoursInAMonth;
const currency: string = getPriceCurrency(serverId); const currency: string = getPriceCurrency(serverId);

View File

@@ -59,7 +59,7 @@ describe("ScaleComponent", () => {
autoscaleMaxThroughput: maxThroughput, autoscaleMaxThroughput: maxThroughput,
minimumThroughput: 400, minimumThroughput: 400,
id: "offer", id: "offer",
offerReplacePending: true headers: { "x-ms-offer-replace-pending": true }
}); });
const newProps = { const newProps = {
...baseProps, ...baseProps,

View File

@@ -116,7 +116,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
} }
const offer = this.props.collection?.offer(); const offer = this.props.collection?.offer();
if (offer?.offerReplacePending) { if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) {
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput; const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
return getThroughputApplyShortDelayMessage( return getThroughputApplyShortDelayMessage(
this.props.isAutoPilotSelected, this.props.isAutoPilotSelected,
@@ -186,7 +186,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
onScaleSaveableChange={this.props.onScaleSaveableChange} onScaleSaveableChange={this.props.onScaleSaveableChange}
onScaleDiscardableChange={this.props.onScaleDiscardableChange} onScaleDiscardableChange={this.props.onScaleDiscardableChange}
getThroughputWarningMessage={this.getThroughputWarningMessage} getThroughputWarningMessage={this.getThroughputWarningMessage}
usageSizeInKB={this.props.collection.usageSizeInKB()} usageSizeInKB={this.props.collection.quotaInfo().usageSizeInKB}
/> />
); );

View File

@@ -179,7 +179,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : offerThroughput, this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : offerThroughput,
serverId, serverId,
regions, regions,
multimaster multimaster,
false
); );
} else { } else {
estimatedSpend = getEstimatedAutoscaleSpendElement( estimatedSpend = getEstimatedAutoscaleSpendElement(

View File

@@ -58,7 +58,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
spendAckChecked={false} spendAckChecked={false}
throughput={1000} throughput={1000}
throughputBaseline={1000} throughputBaseline={1000}
usageSizeInKB={100}
wasAutopilotOriginallySet={true} wasAutopilotOriginallySet={true}
/> />
<Stack <Stack

View File

@@ -18,13 +18,12 @@ export const collection = ({
excludedPaths: [] excludedPaths: []
}), }),
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy, uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
usageSizeInKB: ko.observable(100), quotaInfo: ko.observable<DataModels.CollectionQuotaInfo>({} as DataModels.CollectionQuotaInfo),
offer: ko.observable<DataModels.Offer>({ offer: ko.observable<DataModels.Offer>({
autoscaleMaxThroughput: undefined, autoscaleMaxThroughput: undefined,
manualThroughput: 10000, manualThroughput: 10000,
minimumThroughput: 6000, minimumThroughput: 6000,
id: "offer", id: "offer"
offerReplacePending: false
}), }),
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>( conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
{} as DataModels.ConflictResolutionPolicy {} as DataModels.ConflictResolutionPolicy

View File

@@ -133,6 +133,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function], "partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
@@ -620,6 +622,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function], "partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
@@ -731,6 +735,7 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"arcadiaToken": [Function], "arcadiaToken": [Function],
"armEndpoint": [Function],
"browseQueriesPane": BrowseQueriesPane { "browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function], "canSaveQueries": [Function],
"container": [Circular], "container": [Circular],
@@ -942,6 +947,7 @@ exports[`SettingsComponent renders 1`] = `
"hasWriteAccess": [Function], "hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAuthWithResourceToken": [Function], "isAuthWithResourceToken": [Function],
"isCodeOfConductEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
@@ -1022,6 +1028,7 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function], "onToggleKeyDown": [Function],
"parentFrameDataExplorerVersion": [Function],
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -1047,6 +1054,7 @@ exports[`SettingsComponent renders 1`] = `
"titleLabel": "Select Columns", "titleLabel": "Select Columns",
"visible": [Function], "visible": [Function],
}, },
"quotaId": [Function],
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -1292,9 +1300,9 @@ exports[`SettingsComponent renders 1`] = `
"version": 2, "version": 2,
}, },
"partitionKeyProperty": "partitionKey", "partitionKeyProperty": "partitionKey",
"quotaInfo": [Function],
"readSettings": [Function], "readSettings": [Function],
"uniqueKeyPolicy": Object {}, "uniqueKeyPolicy": Object {},
"usageSizeInKB": [Function],
} }
} }
container={ container={
@@ -1404,6 +1412,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function], "partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
@@ -1891,6 +1901,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function], "partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
@@ -2002,6 +2014,7 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"arcadiaToken": [Function], "arcadiaToken": [Function],
"armEndpoint": [Function],
"browseQueriesPane": BrowseQueriesPane { "browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function], "canSaveQueries": [Function],
"container": [Circular], "container": [Circular],
@@ -2213,6 +2226,7 @@ exports[`SettingsComponent renders 1`] = `
"hasWriteAccess": [Function], "hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAuthWithResourceToken": [Function], "isAuthWithResourceToken": [Function],
"isCodeOfConductEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
@@ -2293,6 +2307,7 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function], "onToggleKeyDown": [Function],
"parentFrameDataExplorerVersion": [Function],
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -2318,6 +2333,7 @@ exports[`SettingsComponent renders 1`] = `
"titleLabel": "Select Columns", "titleLabel": "Select Columns",
"visible": [Function], "visible": [Function],
}, },
"quotaId": [Function],
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -2688,6 +2704,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function], "partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
@@ -3175,6 +3193,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function], "partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
@@ -3286,6 +3306,7 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"arcadiaToken": [Function], "arcadiaToken": [Function],
"armEndpoint": [Function],
"browseQueriesPane": BrowseQueriesPane { "browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function], "canSaveQueries": [Function],
"container": [Circular], "container": [Circular],
@@ -3497,6 +3518,7 @@ exports[`SettingsComponent renders 1`] = `
"hasWriteAccess": [Function], "hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAuthWithResourceToken": [Function], "isAuthWithResourceToken": [Function],
"isCodeOfConductEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
@@ -3577,6 +3599,7 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function], "onToggleKeyDown": [Function],
"parentFrameDataExplorerVersion": [Function],
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -3602,6 +3625,7 @@ exports[`SettingsComponent renders 1`] = `
"titleLabel": "Select Columns", "titleLabel": "Select Columns",
"visible": [Function], "visible": [Function],
}, },
"quotaId": [Function],
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],
@@ -3847,9 +3871,9 @@ exports[`SettingsComponent renders 1`] = `
"version": 2, "version": 2,
}, },
"partitionKeyProperty": "partitionKey", "partitionKeyProperty": "partitionKey",
"quotaInfo": [Function],
"readSettings": [Function], "readSettings": [Function],
"uniqueKeyPolicy": Object {}, "uniqueKeyPolicy": Object {},
"usageSizeInKB": [Function],
} }
} }
container={ container={
@@ -3959,6 +3983,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function], "partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
@@ -4446,6 +4472,8 @@ exports[`SettingsComponent renders 1`] = `
"partitionKeyVisible": [Function], "partitionKeyVisible": [Function],
"requestUnitsUsageCost": [Function], "requestUnitsUsageCost": [Function],
"ruToolTipText": [Function], "ruToolTipText": [Function],
"rupm": [Function],
"rupmVisible": [Function],
"sharedAutoPilotThroughput": [Function], "sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function], "sharedThroughputRangeText": [Function],
"shouldCreateMongoWildcardIndex": [Function], "shouldCreateMongoWildcardIndex": [Function],
@@ -4557,6 +4585,7 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"arcadiaToken": [Function], "arcadiaToken": [Function],
"armEndpoint": [Function],
"browseQueriesPane": BrowseQueriesPane { "browseQueriesPane": BrowseQueriesPane {
"canSaveQueries": [Function], "canSaveQueries": [Function],
"container": [Circular], "container": [Circular],
@@ -4768,6 +4797,7 @@ exports[`SettingsComponent renders 1`] = `
"hasWriteAccess": [Function], "hasWriteAccess": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAuthWithResourceToken": [Function], "isAuthWithResourceToken": [Function],
"isCodeOfConductEnabled": [Function],
"isCopyNotebookPaneEnabled": [Function], "isCopyNotebookPaneEnabled": [Function],
"isEnableMongoCapabilityPresent": [Function], "isEnableMongoCapabilityPresent": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
@@ -4848,6 +4878,7 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function], "onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function], "onToggleKeyDown": [Function],
"parentFrameDataExplorerVersion": [Function],
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
@@ -4873,6 +4904,7 @@ exports[`SettingsComponent renders 1`] = `
"titleLabel": "Select Columns", "titleLabel": "Select Columns",
"visible": [Function], "visible": [Function],
}, },
"quotaId": [Function],
"refreshDatabaseAccount": [Function], "refreshDatabaseAccount": [Function],
"refreshNotebookList": [Function], "refreshNotebookList": [Function],
"refreshTreeTitle": [Function], "refreshTreeTitle": [Function],

View File

@@ -69,15 +69,15 @@ exports[`SettingsUtils functions render 1`] = `
<b> <b>
¥ ¥
1.02 1.29
hourly hourly
/ /
¥ ¥
24.48 31.06
daily daily
/ /
¥ ¥
744.60 944.60
monthly monthly
</b> </b>

View File

@@ -126,12 +126,6 @@
</div> </div>
<div data-bind="visible: !isAutoPilotSelected()"> <div data-bind="visible: !isAutoPilotSelected()">
<p>
<span
>Estimate your required throughput with
<a target="_blank" href="https://cosmos.azure.com/capacitycalculator/">capacity calculator</a></span
>
</p>
<div data-bind="setTemplateReady: true"> <div data-bind="setTemplateReady: true">
<input <input
data-bind=" data-bind="

View File

@@ -1,11 +1,11 @@
jest.mock("../../Common/DocumentClientUtilityBase");
jest.mock("../Graph/GraphExplorerComponent/GremlinClient"); jest.mock("../Graph/GraphExplorerComponent/GremlinClient");
jest.mock("../../Common/dataAccess/createCollection"); jest.mock("../../Common/dataAccess/createCollection");
jest.mock("../../Common/dataAccess/createDocument");
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import Q from "q"; import Q from "q";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { createDocument } from "../../Common/dataAccess/createDocument"; import { createDocument } from "../../Common/DocumentClientUtilityBase";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";

View File

@@ -4,8 +4,8 @@ import GraphTab from ".././Tabs/GraphTab";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient"; import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { createDocument } from "../../Common/DocumentClientUtilityBase";
import { createCollection } from "../../Common/dataAccess/createCollection"; import { createCollection } from "../../Common/dataAccess/createCollection";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
interface SampleDataFile extends DataModels.CreateCollectionParams { interface SampleDataFile extends DataModels.CreateCollectionParams {
@@ -95,15 +95,12 @@ export class ContainerSampleGenerator {
.reduce((previous, current) => previous.then(current), Promise.resolve()); .reduce((previous, current) => previous.then(current), Promise.resolve());
} else { } else {
// For SQL all queries are executed at the same time // For SQL all queries are executed at the same time
await Promise.all( this.sampleDataFile.data.map(doc => {
this.sampleDataFile.data.map(async doc => { const subPromise = createDocument(collection, doc);
try { subPromise.catch(reason => NotificationConsoleUtils.logConsoleError(reason));
await createDocument(collection, doc); promises.push(subPromise);
} catch (error) { });
NotificationConsoleUtils.logConsoleError(error); await Promise.all(promises);
}
})
);
} }
} }

View File

@@ -18,7 +18,7 @@ import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPa
import { readCollection } from "../Common/dataAccess/readCollection"; import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases"; import { readDatabases } from "../Common/dataAccess/readDatabases";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; import EnvironmentUtility from "../Common/EnvironmentUtility";
import GraphStylingPane from "./Panes/GraphStylingPane"; import GraphStylingPane from "./Panes/GraphStylingPane";
import hasher from "hasher"; import hasher from "hasher";
import NewVertexPane from "./Panes/NewVertexPane"; import NewVertexPane from "./Panes/NewVertexPane";
@@ -121,6 +121,7 @@ export default class Explorer {
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>; public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults; public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
public subscriptionType: ko.Observable<SubscriptionType>; public subscriptionType: ko.Observable<SubscriptionType>;
public quotaId: ko.Observable<string>;
public defaultExperience: ko.Observable<string>; public defaultExperience: ko.Observable<string>;
public isPreferredApiDocumentDB: ko.Computed<boolean>; public isPreferredApiDocumentDB: ko.Computed<boolean>;
public isPreferredApiCassandra: ko.Computed<boolean>; public isPreferredApiCassandra: ko.Computed<boolean>;
@@ -134,10 +135,12 @@ export default class Explorer {
public canSaveQueries: ko.Computed<boolean>; public canSaveQueries: ko.Computed<boolean>;
public features: ko.Observable<any>; public features: ko.Observable<any>;
public serverId: ko.Observable<string>; public serverId: ko.Observable<string>;
public armEndpoint: ko.Observable<string>;
public isTryCosmosDBSubscription: ko.Observable<boolean>; public isTryCosmosDBSubscription: ko.Observable<boolean>;
public queriesClient: QueriesClient; public queriesClient: QueriesClient;
public tableDataClient: TableDataClient; public tableDataClient: TableDataClient;
public splitter: Splitter; public splitter: Splitter;
public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>("");
public mostRecentActivity: MostRecentActivity.MostRecentActivity; public mostRecentActivity: MostRecentActivity.MostRecentActivity;
// Notification Console // Notification Console
@@ -201,6 +204,7 @@ export default class Explorer {
// features // features
public isGalleryPublishEnabled: ko.Computed<boolean>; public isGalleryPublishEnabled: ko.Computed<boolean>;
public isCodeOfConductEnabled: ko.Computed<boolean>;
public isLinkInjectionEnabled: ko.Computed<boolean>; public isLinkInjectionEnabled: ko.Computed<boolean>;
public isGitHubPaneEnabled: ko.Observable<boolean>; public isGitHubPaneEnabled: ko.Observable<boolean>;
public isPublishNotebookPaneEnabled: ko.Observable<boolean>; public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
@@ -275,6 +279,7 @@ export default class Explorer {
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>(); this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType); this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
this.quotaId = ko.observable<string>("");
let firstInitialization = true; let firstInitialization = true;
this.isRefreshingExplorer = ko.observable<boolean>(true); this.isRefreshingExplorer = ko.observable<boolean>(true);
this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => { this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => {
@@ -314,9 +319,9 @@ export default class Explorer {
if (isAccountReady) { if (isAccountReady) {
this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true); this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true);
RouteHandler.getInstance().initHandler(); RouteHandler.getInstance().initHandler();
this.notebookWorkspaceManager = new NotebookWorkspaceManager(); this.notebookWorkspaceManager = new NotebookWorkspaceManager(this.armEndpoint());
this.arcadiaWorkspaces = ko.observableArray(); this.arcadiaWorkspaces = ko.observableArray();
this._arcadiaManager = new ArcadiaResourceManager(); this._arcadiaManager = new ArcadiaResourceManager(this.armEndpoint());
this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then(isRegistered => this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then(isRegistered =>
this.hasStorageAnalyticsAfecFeature(isRegistered) this.hasStorageAnalyticsAfecFeature(isRegistered)
); );
@@ -366,6 +371,7 @@ export default class Explorer {
this.features = ko.observable(); this.features = ko.observable();
this.serverId = ko.observable<string>(); this.serverId = ko.observable<string>();
this.armEndpoint = ko.observable<string>(undefined);
this.queriesClient = new QueriesClient(this); this.queriesClient = new QueriesClient(this);
this.isTryCosmosDBSubscription = ko.observable<boolean>(false); this.isTryCosmosDBSubscription = ko.observable<boolean>(false);
@@ -398,6 +404,9 @@ export default class Explorer {
this.isGalleryPublishEnabled = ko.computed<boolean>(() => this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableGalleryPublish) this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
); );
this.isCodeOfConductEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableCodeOfConduct)
);
this.isLinkInjectionEnabled = ko.computed<boolean>(() => this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableLinkInjection) this.isFeatureEnabled(Constants.Features.enableLinkInjection)
); );
@@ -1007,7 +1016,9 @@ export default class Explorer {
this.isSynapseLinkUpdating(true); this.isSynapseLinkUpdating(true);
this._closeSynapseLinkModalDialog(); this._closeSynapseLinkModalDialog();
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(this.databaseAccount().id); const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(
this.databaseAccount().id
);
try { try {
const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync( const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync(
@@ -1747,59 +1758,61 @@ export default class Explorer {
inputs.extensionEndpoint = configContext.PROXY_PATH; inputs.extensionEndpoint = configContext.PROXY_PATH;
} }
this.initDataExplorerWithFrameInputs(inputs); const initPromise: Q.Promise<void> = inputs ? this.initDataExplorerWithFrameInputs(inputs) : Q();
const openAction: ActionContracts.DataExplorerAction = message.openAction; initPromise.then(() => {
if (!!openAction) { const openAction: ActionContracts.DataExplorerAction = message.openAction;
if (this.isRefreshingExplorer()) { if (!!openAction) {
const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => { if (this.isRefreshingExplorer()) {
const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => {
handleOpenAction(openAction, this.nonSystemDatabases(), this);
subscription.dispose();
});
} else {
handleOpenAction(openAction, this.nonSystemDatabases(), this); handleOpenAction(openAction, this.nonSystemDatabases(), this);
subscription.dispose(); }
});
} else {
handleOpenAction(openAction, this.nonSystemDatabases(), this);
} }
} if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
if (message.actionType === ActionContracts.ActionType.TransmitCachedData) { handleCachedDataMessage(message);
handleCachedDataMessage(message); return;
return; }
} if (message.type) {
if (message.type) { switch (message.type) {
switch (message.type) { case MessageTypes.UpdateLocationHash:
case MessageTypes.UpdateLocationHash: if (!message.locationHash) {
if (!message.locationHash) { break;
break; }
} hasher.replaceHash(message.locationHash);
hasher.replaceHash(message.locationHash); RouteHandler.getInstance().parseHash(message.locationHash);
RouteHandler.getInstance().parseHash(message.locationHash); break;
break; case MessageTypes.SendNotification:
case MessageTypes.SendNotification: if (!message.message) {
if (!message.message) { break;
break; }
} NotificationConsoleUtils.logConsoleMessage(
NotificationConsoleUtils.logConsoleMessage( message.consoleDataType || ConsoleDataType.Info,
message.consoleDataType || ConsoleDataType.Info, message.message,
message.message, message.id
message.id );
); break;
break; case MessageTypes.ClearNotification:
case MessageTypes.ClearNotification: if (!message.id) {
if (!message.id) { break;
break; }
} NotificationConsoleUtils.clearInProgressMessageWithId(message.id);
NotificationConsoleUtils.clearInProgressMessageWithId(message.id); break;
break; case MessageTypes.LoadingStatus:
case MessageTypes.LoadingStatus: if (!message.text) {
if (!message.text) { break;
break; }
} this._setLoadingStatusText(message.text, message.title);
this._setLoadingStatusText(message.text, message.title); break;
break; }
return;
} }
return;
}
this.splashScreenAdapter.forceRender(); this.splashScreenAdapter.forceRender();
});
} }
public findSelectedDatabase(): ViewModels.Database { public findSelectedDatabase(): ViewModels.Database {
@@ -1839,14 +1852,8 @@ export default class Explorer {
return false; return false;
} }
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): void { public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): Q.Promise<void> {
if (inputs != null) { if (inputs != null) {
// In development mode, save the iframe message from the portal in session storage.
// This allows webpack hot reload to funciton properly
if (process.env.NODE_ENV === "development") {
sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs));
}
const authorizationToken = inputs.authorizationToken || ""; const authorizationToken = inputs.authorizationToken || "";
const masterKey = inputs.masterKey || ""; const masterKey = inputs.masterKey || "";
const databaseAccount = inputs.databaseAccount || null; const databaseAccount = inputs.databaseAccount || null;
@@ -1855,18 +1862,25 @@ export default class Explorer {
} }
this.features(inputs.features); this.features(inputs.features);
this.serverId(inputs.serverId); this.serverId(inputs.serverId);
this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || configContext.ARM_ENDPOINT));
this.databaseAccount(databaseAccount); this.databaseAccount(databaseAccount);
this.subscriptionType(inputs.subscriptionType); this.subscriptionType(inputs.subscriptionType);
this.quotaId(inputs.quotaId);
this.hasWriteAccess(inputs.hasWriteAccess); this.hasWriteAccess(inputs.hasWriteAccess);
this.flight(inputs.addCollectionDefaultFlight); this.flight(inputs.addCollectionDefaultFlight);
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription); this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription);
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken); this.isAuthWithResourceToken(inputs.isAuthWithresourceToken);
this.setFeatureFlagsFromFlights(inputs.flights); this.setFeatureFlagsFromFlights(inputs.flights);
if (!!inputs.dataExplorerVersion) {
this.parentFrameDataExplorerVersion(inputs.dataExplorerVersion);
}
this._importExplorerConfigComplete = true; this._importExplorerConfigComplete = true;
updateConfigContext({ updateConfigContext({
BACKEND_ENDPOINT: inputs.extensionEndpoint || "", BACKEND_ENDPOINT: inputs.extensionEndpoint || "",
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT) ARM_ENDPOINT: this.armEndpoint()
}); });
updateUserContext({ updateUserContext({
@@ -1875,8 +1889,7 @@ export default class Explorer {
databaseAccount, databaseAccount,
resourceGroup: inputs.resourceGroup, resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId, subscriptionId: inputs.subscriptionId,
subscriptionType: inputs.subscriptionType, subscriptionType: inputs.subscriptionType
quotaId: inputs.quotaId
}); });
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount, Action.LoadDatabaseAccount,
@@ -1890,6 +1903,7 @@ export default class Explorer {
this.isAccountReady(true); this.isAccountReady(true);
} }
return Q();
} }
public setFeatureFlagsFromFlights(flights: readonly string[]): void { public setFeatureFlagsFromFlights(flights: readonly string[]): void {
@@ -2262,6 +2276,7 @@ export default class Explorer {
name, name,
content, content,
parentDomElement, parentDomElement,
this.isCodeOfConductEnabled(),
this.isLinkInjectionEnabled() this.isLinkInjectionEnabled()
); );
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter; this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
@@ -2552,7 +2567,7 @@ export default class Explorer {
public _refreshSparkEnabledStateForAccount = async (): Promise<void> => { public _refreshSparkEnabledStateForAccount = async (): Promise<void> => {
const subscriptionId = userContext.subscriptionId; const subscriptionId = userContext.subscriptionId;
const armEndpoint = configContext.ARM_ENDPOINT; const armEndpoint = this.armEndpoint();
const authType = window.authType as AuthType; const authType = window.authType as AuthType;
if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
// explorer is not aware of the database account yet // explorer is not aware of the database account yet
@@ -2561,7 +2576,7 @@ export default class Explorer {
} }
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${Constants.AfecFeatures.Spark}`; const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${Constants.AfecFeatures.Spark}`;
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri); const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(featureUri);
try { try {
const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync( const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync(
featureUri, featureUri,
@@ -2581,7 +2596,7 @@ export default class Explorer {
public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => { public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => {
const subscriptionId = userContext.subscriptionId; const subscriptionId = userContext.subscriptionId;
const armEndpoint = configContext.ARM_ENDPOINT; const armEndpoint = this.armEndpoint();
const authType = window.authType as AuthType; const authType = window.authType as AuthType;
if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) { if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
// explorer is not aware of the database account yet // explorer is not aware of the database account yet
@@ -2589,7 +2604,7 @@ export default class Explorer {
} }
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${featureName}`; const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${featureName}`;
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri); const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(featureUri);
try { try {
const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync( const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync(
featureUri, featureUri,

View File

@@ -1,5 +1,4 @@
jest.mock("../../../Common/dataAccess/queryDocuments"); jest.mock("../../../Common/DocumentClientUtilityBase");
jest.mock("../../../Common/dataAccess/queryDocumentsPage");
import React from "react"; import React from "react";
import * as sinon from "sinon"; import * as sinon from "sinon";
import { mount, ReactWrapper } from "enzyme"; import { mount, ReactWrapper } from "enzyme";
@@ -13,8 +12,7 @@ import * as DataModels from "../../../Contracts/DataModels";
import * as StorageUtility from "../../../Shared/StorageUtility"; import * as StorageUtility from "../../../Shared/StorageUtility";
import GraphTab from "../../Tabs/GraphTab"; import GraphTab from "../../Tabs/GraphTab";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments"; import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
describe("Check whether query result is vertex array", () => { describe("Check whether query result is vertex array", () => {
it("should reject null as vertex array", () => { it("should reject null as vertex array", () => {
@@ -301,12 +299,12 @@ describe("GraphExplorer", () => {
ignoreD3Update: boolean ignoreD3Update: boolean
): GraphExplorer => { ): GraphExplorer => {
(queryDocuments as jest.Mock).mockImplementation((container: any, query: string, options: any) => { (queryDocuments as jest.Mock).mockImplementation((container: any, query: string, options: any) => {
return { return Q.resolve({
_query: query, _query: query,
nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {}, nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {},
hasMoreResults: () => false, hasMoreResults: () => false,
executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {} executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {}
}; });
}); });
(queryDocumentsPage as jest.Mock).mockImplementation( (queryDocumentsPage as jest.Mock).mockImplementation(
(rid: string, iterator: any, firstItemIndex: number, options: any) => { (rid: string, iterator: any, firstItemIndex: number, options: any) => {

View File

@@ -28,10 +28,8 @@ import * as Constants from "../../../Common/Constants";
import { InputProperty } from "../../../Contracts/ViewModels"; import { InputProperty } from "../../../Contracts/ViewModels";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos"; import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif"; import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments"; import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
import { FeedOptions } from "@azure/cosmos";
export interface GraphAccessor { export interface GraphAccessor {
applyFilter: () => void; applyFilter: () => void;
@@ -727,32 +725,26 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
/** /**
* Execute DocDB query and get all results * Execute DocDB query and get all results
*/ */
public async executeNonPagedDocDbQuery(query: string): Promise<DataModels.DocumentId[]> { public executeNonPagedDocDbQuery(query: string): Q.Promise<DataModels.DocumentId[]> {
try { // TODO maxItemCount: this reduces throttling, but won't cap the # of results
// TODO maxItemCount: this reduces throttling, but won't cap the # of results return queryDocuments(this.props.databaseId, this.props.collectionId, query, {
const iterator: QueryIterator<ItemDefinition & Resource> = queryDocuments( maxItemCount: GraphExplorer.PAGE_ALL,
this.props.databaseId, enableCrossPartitionQuery:
this.props.collectionId, StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.IsCrossPartitionQueryEnabled) ===
query, "true"
{ }).then(
maxItemCount: GraphExplorer.PAGE_ALL, (iterator: QueryIterator<ItemDefinition & Resource>) => {
enableCrossPartitionQuery: return iterator.fetchNext().then(response => response.resources);
StorageUtility.LocalStorageUtility.getEntryString( },
StorageUtility.StorageKey.IsCrossPartitionQueryEnabled (reason: any) => {
) === "true" GraphExplorer.reportToConsole(
} as FeedOptions ConsoleDataType.Error,
); `Failed to execute non-paged query ${query}. Reason:${reason}`,
const response = await iterator.fetchNext(); reason
);
return response?.resources; return null;
} catch (error) { }
GraphExplorer.reportToConsole( );
ConsoleDataType.Error,
`Failed to execute non-paged query ${query}. Reason:${error}`,
error
);
return null;
}
} }
/** /**
@@ -872,7 +864,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
/** /**
* User executes query * User executes query
*/ */
public async submitQuery(query: string): Promise<void> { public submitQuery(query: string): void {
// Clear any progress indicator // Clear any progress indicator
this.executeCounter = 0; this.executeCounter = 0;
this.setState({ this.setState({
@@ -890,22 +882,24 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// Remember query // Remember query
this.pushToLatestQueryFragments(query); this.pushToLatestQueryFragments(query);
try { let backendPromise;
let result: UserQueryResult;
if (query.toLocaleLowerCase() === "g.V()".toLocaleLowerCase()) {
result = await this.executeDocDbGVQuery();
} else {
result = await this.executeGremlinQuery(query);
}
this.queryTotalRequestCharge = result.requestCharge; if (query.toLocaleLowerCase() === "g.V()".toLocaleLowerCase()) {
} catch (error) { backendPromise = this.executeDocDbGVQuery();
const errorMsg = `Failure in submitting query: ${query}: ${getErrorMessage(error)}`; } else {
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); backendPromise = this.executeGremlinQuery(query);
this.setState({
filterQueryError: errorMsg
});
} }
backendPromise.then(
(result: UserQueryResult) => (this.queryTotalRequestCharge = result.requestCharge),
(error: any) => {
const errorMsg = `Failure in submitting query: ${query}: ${getErrorMessage(error)}`;
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
this.setState({
filterQueryError: errorMsg
});
}
);
} }
/** /**
@@ -1396,7 +1390,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
/** /**
* Update possible vertices to display in UI * Update possible vertices to display in UI
*/ */
private updatePossibleVertices(): Promise<PossibleVertex[]> { private updatePossibleVertices(): Q.Promise<PossibleVertex[]> {
const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null; const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null;
const q = `SELECT c.id, c["${this.props.graphConfigUiData.nodeCaptionChoice() || const q = `SELECT c.id, c["${this.props.graphConfigUiData.nodeCaptionChoice() ||
@@ -1727,81 +1721,85 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
); );
} }
private async executeDocDbGVQuery(): Promise<UserQueryResult> { private executeDocDbGVQuery(): Q.Promise<UserQueryResult> {
let query = "select root.id from root where IS_DEFINED(root._isEdge) = false order by root._ts desc"; let query = "select root.id from root where IS_DEFINED(root._isEdge) = false order by root._ts desc";
if (this.props.collectionPartitionKeyProperty) { if (this.props.collectionPartitionKeyProperty) {
query = `select root.id, root.${this.props.collectionPartitionKeyProperty} from root where IS_DEFINED(root._isEdge) = false order by root._ts asc`; query = `select root.id, root.${this.props.collectionPartitionKeyProperty} from root where IS_DEFINED(root._isEdge) = false order by root._ts asc`;
} }
try { return queryDocuments(this.props.databaseId, this.props.collectionId, query, {
const iterator: QueryIterator<ItemDefinition & Resource> = queryDocuments( maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
this.props.databaseId, enableCrossPartitionQuery: LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
this.props.collectionId, })
query, .then(
{ (iterator: QueryIterator<ItemDefinition & Resource>) => {
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE, this.currentDocDBQueryInfo = {
enableCrossPartitionQuery: iterator: iterator,
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true" index: 0,
} as FeedOptions query: query
); };
this.currentDocDBQueryInfo = { },
iterator: iterator, (reason: any) => {
index: 0, GraphExplorer.reportToConsole(
query: query ConsoleDataType.Error,
}; `Failed to execute CosmosDB query: ${query} reason:${reason}`
return await this.loadMoreRootNodes(); );
} catch (error) { }
GraphExplorer.reportToConsole( )
ConsoleDataType.Error, .then(() => this.loadMoreRootNodes());
`Failed to execute CosmosDB query: ${query} reason:${error}`
);
throw error;
}
} }
private async loadMoreRootNodes(): Promise<UserQueryResult> { private loadMoreRootNodes(): Q.Promise<UserQueryResult> {
if (!this.currentDocDBQueryInfo) { if (!this.currentDocDBQueryInfo) {
return undefined; return Q.resolve(null);
} }
let RU: string = GraphExplorer.REQUEST_CHARGE_UNKNOWN_MSG; let RU: string = GraphExplorer.REQUEST_CHARGE_UNKNOWN_MSG;
const queryInfoStr = `${this.currentDocDBQueryInfo.query} (${this.currentDocDBQueryInfo.index + 1}-${this const queryInfoStr = `${this.currentDocDBQueryInfo.query} (${this.currentDocDBQueryInfo.index + 1}-${this
.currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE})`; .currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE})`;
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`); const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`);
try { return queryDocumentsPage(
const results: ViewModels.QueryResults = await queryDocumentsPage( this.props.collectionId,
this.props.collectionId, this.currentDocDBQueryInfo.iterator,
this.currentDocDBQueryInfo.iterator, this.currentDocDBQueryInfo.index,
this.currentDocDBQueryInfo.index {
); enableCrossPartitionQuery:
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
GraphExplorer.clearConsoleProgress(id); }
this.currentDocDBQueryInfo.index = results.lastItemIndex + 1; )
this.setState({ hasMoreRoots: results.hasMoreResults }); .then((results: ViewModels.QueryResults) => {
RU = results.requestCharge.toString(); GraphExplorer.clearConsoleProgress(id);
GraphExplorer.reportToConsole( this.currentDocDBQueryInfo.index = results.lastItemIndex + 1;
ConsoleDataType.Info, this.setState({ hasMoreRoots: results.hasMoreResults });
`Executed: ${queryInfoStr} ${GremlinClient.GremlinClient.getRequestChargeString(RU)}` RU = results.requestCharge.toString();
); GraphExplorer.reportToConsole(
const pkIds: string[] = (results.documents || []).map((item: DataModels.DocumentId) => ConsoleDataType.Info,
GraphExplorer.getPkIdFromDocumentId(item, this.props.collectionPartitionKeyProperty) `Executed: ${queryInfoStr} ${GremlinClient.GremlinClient.getRequestChargeString(RU)}`
); );
const documents = results.documents || [];
const arg = pkIds.join(","); return documents.map(
await this.executeGremlinQuery(`g.V(${arg})`); (item: DataModels.DocumentId) => {
return GraphExplorer.getPkIdFromDocumentId(item, this.props.collectionPartitionKeyProperty);
return { requestCharge: RU }; },
} catch (error) { (reason: any) => {
GraphExplorer.clearConsoleProgress(id); // Failure
const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${getErrorMessage(error)}`; GraphExplorer.clearConsoleProgress(id);
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg); const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${reason}`;
this.setState({ GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
filterQueryError: errorMsg this.setState({
}); filterQueryError: errorMsg
this.setFilterQueryStatus(FilterQueryStatus.ErrorResult); });
throw error; this.setFilterQueryStatus(FilterQueryStatus.ErrorResult);
} throw reason;
}
);
})
.then((pkIds: string[]) => {
const arg = pkIds.join(",");
return this.executeGremlinQuery(`g.V(${arg})`);
})
.then(() => ({ requestCharge: RU }));
} }
private executeGremlinQuery(query: string): Q.Promise<UserQueryResult> { private executeGremlinQuery(query: string): Q.Promise<UserQueryResult> {

View File

@@ -99,22 +99,7 @@
.notificationConsoleControls { .notificationConsoleControls {
padding: @MediumSpace; padding: @MediumSpace;
margin-left:@DefaultSpace; margin-left:@DefaultSpace;
display: flex;
align-items: center;
.ms-Dropdown-container {
display: flex;
.ms-Dropdown-title {
height: 25px;
line-height: 25px;
}
.ms-Dropdown {
min-width: 110px;
margin-left: 10px;
height: 25px;
line-height: 25px;
}
}
#consoleFilterLabel { #consoleFilterLabel {
padding: 4px; padding: 4px;
} }
@@ -122,7 +107,6 @@
.consoleSplitter { .consoleSplitter {
border-left: 1px solid @BaseMedium; border-left: 1px solid @BaseMedium;
margin: @MediumSpace; margin: @MediumSpace;
height: 20px;
} }
.clearNotificationsButton { .clearNotificationsButton {

View File

@@ -5,7 +5,7 @@
import * as React from "react"; import * as React from "react";
import { ClientDefaults, KeyCodes } from "../../../Common/Constants"; import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
import AnimateHeight from "react-animate-height"; import AnimateHeight from "react-animate-height";
import { Dropdown, IDropdownOption } from "office-ui-fabric-react";
import LoadingIcon from "../../../../images/loading.svg"; import LoadingIcon from "../../../../images/loading.svg";
import ErrorBlackIcon from "../../../../images/error_black.svg"; import ErrorBlackIcon from "../../../../images/error_black.svg";
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg"; import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
@@ -53,12 +53,7 @@ export class NotificationConsoleComponent extends React.Component<
NotificationConsoleComponentState NotificationConsoleComponentState
> { > {
private static readonly transitionDurationMs = 200; private static readonly transitionDurationMs = 200;
private static readonly FilterOptions = [ private static readonly FilterOptions = ["All", "In Progress", "Info", "Error"];
{ key: "All", text: "All" },
{ key: "In Progress", text: "In progress" },
{ key: "Info", text: "Info" },
{ key: "Error", text: "Error" }
];
private headerTimeoutId: number; private headerTimeoutId: number;
private prevHeaderStatus: string; private prevHeaderStatus: string;
private consoleHeaderElement: HTMLElement; private consoleHeaderElement: HTMLElement;
@@ -67,7 +62,7 @@ export class NotificationConsoleComponent extends React.Component<
super(props); super(props);
this.state = { this.state = {
headerStatus: "", headerStatus: "",
selectedFilter: NotificationConsoleComponent.FilterOptions[0].key || "", selectedFilter: NotificationConsoleComponent.FilterOptions[0],
isExpanded: props.isConsoleExpanded isExpanded: props.isConsoleExpanded
}; };
this.prevHeaderStatus = null; this.prevHeaderStatus = null;
@@ -155,15 +150,20 @@ export class NotificationConsoleComponent extends React.Component<
> >
<div className="notificationConsoleContents"> <div className="notificationConsoleContents">
<div className="notificationConsoleControls"> <div className="notificationConsoleControls">
<Dropdown <label id="consoleFilterLabel">Filter</label>
label="Filter:" <select
role="combobox"
selectedKey={this.state.selectedFilter}
options={NotificationConsoleComponent.FilterOptions}
onChange={this.onFilterSelected.bind(this)}
aria-labelledby="consoleFilterLabel" aria-labelledby="consoleFilterLabel"
role="combobox"
aria-label={this.state.selectedFilter} aria-label={this.state.selectedFilter}
/> value={this.state.selectedFilter}
onChange={this.onFilterSelected.bind(this)}
>
{NotificationConsoleComponent.FilterOptions.map((value: string) => (
<option value={value} key={value}>
{value}
</option>
))}
</select>
<span className="consoleSplitter" /> <span className="consoleSplitter" />
<span <span
className="clearNotificationsButton" className="clearNotificationsButton"
@@ -220,8 +220,8 @@ export class NotificationConsoleComponent extends React.Component<
)); ));
} }
private onFilterSelected(event: React.ChangeEvent<HTMLSelectElement>, option: IDropdownOption): void { private onFilterSelected(event: React.ChangeEvent<HTMLSelectElement>): void {
this.setState({ selectedFilter: String(option.key) }); this.setState({ selectedFilter: event.target.value });
} }
private getFilteredConsoleData(): ConsoleData[] { private getFilteredConsoleData(): ConsoleData[] {

View File

@@ -110,34 +110,43 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
<div <div
className="notificationConsoleControls" className="notificationConsoleControls"
> >
<StyledWithResponsiveMode <label
id="consoleFilterLabel"
>
Filter
</label>
<select
aria-label="All" aria-label="All"
aria-labelledby="consoleFilterLabel" aria-labelledby="consoleFilterLabel"
label="Filter:"
onChange={[Function]} onChange={[Function]}
options={
Array [
Object {
"key": "All",
"text": "All",
},
Object {
"key": "In Progress",
"text": "In progress",
},
Object {
"key": "Info",
"text": "Info",
},
Object {
"key": "Error",
"text": "Error",
},
]
}
role="combobox" role="combobox"
selectedKey="All" value="All"
/> >
<option
key="All"
value="All"
>
All
</option>
<option
key="In Progress"
value="In Progress"
>
In Progress
</option>
<option
key="Info"
value="Info"
>
Info
</option>
<option
key="Error"
value="Error"
>
Error
</option>
</select>
<span <span
className="consoleSplitter" className="consoleSplitter"
/> />

View File

@@ -350,7 +350,9 @@ export const launchWebSocketKernelEpic = (
} as any, } as any,
name: "", name: "",
path: content.filepath.replace(/^\/+/g, ""), path: content.filepath.replace(/^\/+/g, ""),
type: "notebook" type: "notebook",
endpoint:"https://dech-notebooks-demo-7.documents.azure.com:443/",
token:"" //to fill
}; };
return sessions.create(serverConfig, sessionPayload).pipe( return sessions.create(serverConfig, sessionPayload).pipe(

View File

@@ -128,9 +128,17 @@ export default class NotebookManager {
name: string, name: string,
content: string | ImmutableNotebook, content: string | ImmutableNotebook,
parentDomElement: HTMLElement, parentDomElement: HTMLElement,
isCodeOfConductEnabled: boolean,
isLinkInjectionEnabled: boolean isLinkInjectionEnabled: boolean
): Promise<void> { ): Promise<void> {
await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement, isLinkInjectionEnabled); await this.publishNotebookPaneAdapter.open(
name,
getFullName(),
content,
parentDomElement,
isCodeOfConductEnabled,
isLinkInjectionEnabled
);
} }
public openCopyNotebookPane(name: string, content: string): void { public openCopyNotebookPane(name: string, content: string): void {

View File

@@ -243,6 +243,38 @@
</div> </div>
<!-- Unlimited Button Content - Start --> <!-- Unlimited Button Content - Start -->
<div class="tabcontent" data-bind="visible: isUnlimitedStorageSelected() || databaseHasSharedOffer()"> <div class="tabcontent" data-bind="visible: isUnlimitedStorageSelected() || databaseHasSharedOffer()">
<div data-bind="visible: rupmVisible">
<div class="tabs">
<p>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel">RU/m</span>
<span class="infoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information">
<span class="tooltiptext throughputRuInfo">
For each 100 Request Units per second (RU/s) provisioned, 1,000 Request Units
per
minute
(RU/m) can be provisioned. E.g.: for a container with 5,000 RU/s provisioned
with
RU/m
enabled, the RU/m budget will be 50,000 RU/m.
</span>
</span>
</p>
<div tabindex="0" data-bind="event: { keydown: onRupmOptionsKeyDown }" aria-label="RU/m">
<div class="tab">
<input type="radio" id="rupmOn2" name="rupmcoll2" value="on" class="radio"
data-bind="checked: rupm">
<label for="rupmOn2">ON</label>
</div>
<div class="tab">
<input type="radio" id="rupmOff2" name="rupmcoll2" value="off" class="radio"
data-bind="checked: rupm">
<label for="rupmOff2">OFF</label>
</div>
</div>
</div>
</div>
<div data-bind="visible: partitionKeyVisible"> <div data-bind="visible: partitionKeyVisible">
<p> <p>
<span class="mandatoryStar">*</span> <span class="mandatoryStar">*</span>

View File

@@ -16,7 +16,6 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent"; import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
import { createCollection } from "../../Common/dataAccess/createCollection"; import { createCollection } from "../../Common/dataAccess/createCollection";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { userContext } from "../../UserContext";
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions { export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
isPreferredApiTable: ko.Computed<boolean>; isPreferredApiTable: ko.Computed<boolean>;
@@ -43,6 +42,8 @@ export default class AddCollectionPane extends ContextualPaneBase {
public partitionKeyVisible: ko.Computed<boolean>; public partitionKeyVisible: ko.Computed<boolean>;
public partitionKeyPattern: ko.Computed<string>; public partitionKeyPattern: ko.Computed<string>;
public partitionKeyTitle: ko.Computed<string>; public partitionKeyTitle: ko.Computed<string>;
public rupm: ko.Observable<string>;
public rupmVisible: ko.Observable<boolean>;
public storage: ko.Observable<string>; public storage: ko.Observable<string>;
public throughputSinglePartition: ViewModels.Editable<number>; public throughputSinglePartition: ViewModels.Editable<number>;
public throughputMultiPartition: ViewModels.Editable<number>; public throughputMultiPartition: ViewModels.Editable<number>;
@@ -142,6 +143,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
} }
return ""; return "";
}); });
this.rupm = ko.observable<string>(Constants.RUPMStates.off);
this.rupmVisible = ko.observable<boolean>(false);
const featureSubcription = this.container.features.subscribe(() => {
this.rupmVisible(this.container.isFeatureEnabled(Constants.Features.enableRupm));
featureSubcription.dispose();
});
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue()); this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
@@ -194,6 +201,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
account.properties.readLocations.length) || account.properties.readLocations.length) ||
1; 1;
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false; const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
const rupmEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
let throughputSpendAckText: string; let throughputSpendAckText: string;
let estimatedSpend: string; let estimatedSpend: string;
@@ -203,15 +211,23 @@ export default class AddCollectionPane extends ContextualPaneBase {
serverId, serverId,
regions, regions,
multimaster, multimaster,
rupmEnabled,
this.isSharedAutoPilotSelected() this.isSharedAutoPilotSelected()
); );
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster); estimatedSpend = PricingUtils.getEstimatedSpendHtml(
offerThroughput,
serverId,
regions,
multimaster,
rupmEnabled
);
} else { } else {
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString( throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
this.sharedAutoPilotThroughput(), this.sharedAutoPilotThroughput(),
serverId, serverId,
regions, regions,
multimaster, multimaster,
rupmEnabled,
this.isSharedAutoPilotSelected() this.isSharedAutoPilotSelected()
); );
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
@@ -248,6 +264,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
account.properties.readLocations.length) || account.properties.readLocations.length) ||
1; 1;
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false; const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
const rupmEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
let throughputSpendAckText: string; let throughputSpendAckText: string;
let estimatedSpend: string; let estimatedSpend: string;
@@ -257,13 +274,15 @@ export default class AddCollectionPane extends ContextualPaneBase {
serverId, serverId,
regions, regions,
multimaster, multimaster,
rupmEnabled,
this.isAutoPilotSelected() this.isAutoPilotSelected()
); );
estimatedSpend = PricingUtils.getEstimatedSpendHtml( estimatedSpend = PricingUtils.getEstimatedSpendHtml(
this.throughputMultiPartition(), this.throughputMultiPartition(),
serverId, serverId,
regions, regions,
multimaster multimaster,
rupmEnabled
); );
} else { } else {
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString( throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
@@ -271,6 +290,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
serverId, serverId,
regions, regions,
multimaster, multimaster,
rupmEnabled,
this.isAutoPilotSelected() this.isAutoPilotSelected()
); );
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
@@ -666,10 +686,11 @@ export default class AddCollectionPane extends ContextualPaneBase {
storage: this.storage(), storage: this.storage(),
offerThroughput: this._getThroughput(), offerThroughput: this._getThroughput(),
partitionKey: this.partitionKey(), partitionKey: this.partitionKey(),
databaseId: this.databaseId() databaseId: this.databaseId(),
rupm: this.rupm()
}), }),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: this._getThroughput(), throughput: this._getThroughput(),
@@ -767,11 +788,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
id: this.collectionId(), id: this.collectionId(),
storage: this.storage(), storage: this.storage(),
partitionKey, partitionKey,
rupm: this.rupm(),
uniqueKeyPolicy, uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared() collectionWithThroughputInShared: this.collectionWithThroughputInShared()
}), }),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput, throughput: offerThroughput,
@@ -841,11 +863,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
id: this.collectionId(), id: this.collectionId(),
storage: this.storage(), storage: this.storage(),
partitionKey, partitionKey,
rupm: this.rupm(),
uniqueKeyPolicy, uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared() collectionWithThroughputInShared: this.collectionWithThroughputInShared()
}), }),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput, throughput: offerThroughput,
@@ -875,11 +898,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
id: this.collectionId(), id: this.collectionId(),
storage: this.storage(), storage: this.storage(),
partitionKey, partitionKey,
rupm: this.rupm(),
uniqueKeyPolicy, uniqueKeyPolicy,
collectionWithThroughputInShared: this.collectionWithThroughputInShared() collectionWithThroughputInShared: this.collectionWithThroughputInShared()
}, },
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u", storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
throughput: offerThroughput, throughput: offerThroughput,
@@ -957,6 +981,20 @@ export default class AddCollectionPane extends ContextualPaneBase {
return true; return true;
} }
public onRupmOptionsKeyDown(source: any, event: KeyboardEvent): boolean {
if (event.key === "ArrowRight") {
this.rupm("off");
return false;
}
if (event.key === "ArrowLeft") {
this.rupm("on");
return false;
}
return true;
}
public onEnableSynapseLinkButtonClicked() { public onEnableSynapseLinkButtonClicked() {
this.container.openEnableSynapseLinkDialog(); this.container.openEnableSynapseLinkDialog();
} }
@@ -980,6 +1018,16 @@ export default class AddCollectionPane extends ContextualPaneBase {
} }
const throughput = this._getThroughput(); const throughput = this._getThroughput();
const maxThroughputWithRUPM =
SharedConstants.CollectionCreation.MaxRUPMPerPartition * this._calculateNumberOfPartitions();
if (this.rupm() === Constants.RUPMStates.on && throughput > maxThroughputWithRUPM) {
this.formErrors(
`The maximum supported provisioned throughput with RU/m enabled is ${maxThroughputWithRUPM} RU/s. Please turn off RU/m to incease thoughput above ${maxThroughputWithRUPM} RU/s.`
);
return false;
}
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) { if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) {
this.formErrors(`Please acknowledge the estimated daily spend.`); this.formErrors(`Please acknowledge the estimated daily spend.`);
return false; return false;

View File

@@ -13,7 +13,6 @@ import { createDatabase } from "../../Common/dataAccess/createDatabase";
import { configContext, Platform } from "../../ConfigContext"; import { configContext, Platform } from "../../ConfigContext";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { SubscriptionType } from "../../Contracts/SubscriptionType"; import { SubscriptionType } from "../../Contracts/SubscriptionType";
import { userContext } from "../../UserContext";
export default class AddDatabasePane extends ContextualPaneBase { export default class AddDatabasePane extends ContextualPaneBase {
public defaultExperience: ko.Computed<string>; public defaultExperience: ko.Computed<string>;
@@ -134,12 +133,19 @@ export default class AddDatabasePane extends ContextualPaneBase {
let estimatedSpendAcknowledge: string; let estimatedSpendAcknowledge: string;
let estimatedSpend: string; let estimatedSpend: string;
if (!this.isAutoPilotSelected()) { if (!this.isAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster); estimatedSpend = PricingUtils.getEstimatedSpendHtml(
offerThroughput,
serverId,
regions,
multimaster,
false /*rupmEnabled*/
);
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
offerThroughput, offerThroughput,
serverId, serverId,
regions, regions,
multimaster, multimaster,
false /*rupmEnabled*/,
this.isAutoPilotSelected() this.isAutoPilotSelected()
); );
} else { } else {
@@ -154,6 +160,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
serverId, serverId,
regions, regions,
multimaster, multimaster,
false /*rupmEnabled*/,
this.isAutoPilotSelected() this.isAutoPilotSelected()
); );
} }
@@ -251,7 +258,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
databaseAccountName: this.container.databaseAccount().name, databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(), defaultExperience: this.container.defaultExperience(),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
throughput: this.throughput(), throughput: this.throughput(),
flight: this.container.flight() flight: this.container.flight()
@@ -279,7 +286,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
}), }),
offerThroughput, offerThroughput,
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
flight: this.container.flight() flight: this.container.flight()
}, },
@@ -343,7 +350,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
}), }),
offerThroughput: offerThroughput, offerThroughput: offerThroughput,
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
flight: this.container.flight() flight: this.container.flight()
}, },
@@ -367,7 +374,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
}), }),
offerThroughput: offerThroughput, offerThroughput: offerThroughput,
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
flight: this.container.flight() flight: this.container.flight()
}, },

View File

@@ -15,7 +15,6 @@ import { HashMap } from "../../Common/HashMap";
import { configContext, Platform } from "../../ConfigContext"; import { configContext, Platform } from "../../ConfigContext";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { SubscriptionType } from "../../Contracts/SubscriptionType"; import { SubscriptionType } from "../../Contracts/SubscriptionType";
import { userContext } from "../../UserContext";
export default class CassandraAddCollectionPane extends ContextualPaneBase { export default class CassandraAddCollectionPane extends ContextualPaneBase {
public createTableQuery: ko.Observable<string>; public createTableQuery: ko.Observable<string>;
@@ -139,12 +138,19 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
let estimatedSpend: string; let estimatedSpend: string;
let estimatedDedicatedSpendAcknowledge: string; let estimatedDedicatedSpendAcknowledge: string;
if (!this.isAutoPilotSelected()) { if (!this.isAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster); estimatedSpend = PricingUtils.getEstimatedSpendHtml(
offerThroughput,
serverId,
regions,
multimaster,
false /*rupmEnabled*/
);
estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
offerThroughput, offerThroughput,
serverId, serverId,
regions, regions,
multimaster, multimaster,
false /*rupmEnabled*/,
this.isAutoPilotSelected() this.isAutoPilotSelected()
); );
} else { } else {
@@ -159,6 +165,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
serverId, serverId,
regions, regions,
multimaster, multimaster,
false /*rupmEnabled*/,
this.isAutoPilotSelected() this.isAutoPilotSelected()
); );
} }
@@ -183,12 +190,19 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
let estimatedSpend: string; let estimatedSpend: string;
let estimatedSharedSpendAcknowledge: string; let estimatedSharedSpendAcknowledge: string;
if (!this.isSharedAutoPilotSelected()) { if (!this.isSharedAutoPilotSelected()) {
estimatedSpend = PricingUtils.getEstimatedSpendHtml(this.keyspaceThroughput(), serverId, regions, multimaster); estimatedSpend = PricingUtils.getEstimatedSpendHtml(
this.keyspaceThroughput(),
serverId,
regions,
multimaster,
false /*rupmEnabled*/
);
estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString( estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
this.keyspaceThroughput(), this.keyspaceThroughput(),
serverId, serverId,
regions, regions,
multimaster, multimaster,
false /*rupmEnabled*/,
this.isSharedAutoPilotSelected() this.isSharedAutoPilotSelected()
); );
} else { } else {
@@ -203,6 +217,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
serverId, serverId,
regions, regions,
multimaster, multimaster,
false /*rupmEnabled*/,
this.isSharedAutoPilotSelected() this.isSharedAutoPilotSelected()
); );
} }
@@ -297,10 +312,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
storage: Constants.BackendDefaults.multiPartitionStorageInGb, storage: Constants.BackendDefaults.multiPartitionStorageInGb,
offerThroughput: this.throughput(), offerThroughput: this.throughput(),
partitionKey: "", partitionKey: "",
databaseId: this.keyspaceId() databaseId: this.keyspaceId(),
rupm: false
}), }),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: this.throughput(), throughput: this.throughput(),
@@ -350,11 +366,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
offerThroughput: this.throughput(), offerThroughput: this.throughput(),
partitionKey: "", partitionKey: "",
databaseId: this.keyspaceId(), databaseId: this.keyspaceId(),
rupm: false,
hasDedicatedThroughput: this.dedicateTableThroughput() hasDedicatedThroughput: this.dedicateTableThroughput()
}), }),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: this.throughput(), throughput: this.throughput(),
@@ -396,11 +413,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
offerThroughput: this.throughput(), offerThroughput: this.throughput(),
partitionKey: "", partitionKey: "",
databaseId: this.keyspaceId(), databaseId: this.keyspaceId(),
rupm: false,
hasDedicatedThroughput: this.dedicateTableThroughput() hasDedicatedThroughput: this.dedicateTableThroughput()
}), }),
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: this.throughput(), throughput: this.throughput(),
@@ -426,11 +444,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
offerThroughput: this.throughput(), offerThroughput: this.throughput(),
partitionKey: "", partitionKey: "",
databaseId: this.keyspaceId(), databaseId: this.keyspaceId(),
rupm: false,
hasDedicatedThroughput: this.dedicateTableThroughput() hasDedicatedThroughput: this.dedicateTableThroughput()
}, },
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(), keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
subscriptionType: SubscriptionType[this.container.subscriptionType()], subscriptionType: SubscriptionType[this.container.subscriptionType()],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: this.container.quotaId(),
defaultsCheck: { defaultsCheck: {
storage: "u", storage: "u",
throughput: this.throughput(), throughput: this.throughput(),

View File

@@ -98,21 +98,26 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
author: string, author: string,
notebookContent: string | ImmutableNotebook, notebookContent: string | ImmutableNotebook,
parentDomElement: HTMLElement, parentDomElement: HTMLElement,
isCodeOfConductEnabled: boolean,
isLinkInjectionEnabled: boolean isLinkInjectionEnabled: boolean
): Promise<void> { ): Promise<void> {
try { if (isCodeOfConductEnabled) {
const response = await this.junoClient.isCodeOfConductAccepted(); try {
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { const response = await this.junoClient.isCodeOfConductAccepted();
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`); if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
} throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
}
this.isCodeOfConductAccepted = response.data; this.isCodeOfConductAccepted = response.data;
} catch (error) { } catch (error) {
handleError( handleError(
error, error,
"PublishNotebookPaneAdapter/isCodeOfConductAccepted", "PublishNotebookPaneAdapter/isCodeOfConductAccepted",
"Failed to check if code of conduct was accepted" "Failed to check if code of conduct was accepted"
); );
}
} else {
this.isCodeOfConductAccepted = true;
} }
this.name = name; this.name = name;

View File

@@ -47,7 +47,7 @@
padding: 32px 16px; padding: 32px 16px;
display: flex; display: flex;
background-color: @BaseLight; background-color: @BaseLight;
border: 1px solid #949494; border: 1px solid #E5E5E5;
box-sizing: border-box; box-sizing: border-box;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
border-radius: 4px; border-radius: 4px;

View File

@@ -421,47 +421,53 @@ export default class TableEntityListViewModel extends DataTableViewModel {
* Note that this also means that we can get less entities than the requested download size in a successful call. * Note that this also means that we can get less entities than the requested download size in a successful call.
* See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx * See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx
*/ */
private async prefetchData( private prefetchData(
tableQuery: Entities.ITableQuery, tableQuery: Entities.ITableQuery,
downloadSize: number, downloadSize: number,
currentRetry: number = 0 currentRetry: number = 0
): Promise<IListTableEntitiesSegmentedResult> { ): Q.Promise<any> {
if (!this.cache.serverCallInProgress) { if (!this.cache.serverCallInProgress) {
this.cache.serverCallInProgress = true; this.cache.serverCallInProgress = true;
this.allDownloaded = false; this.allDownloaded = false;
this.lastPrefetchTime = new Date().getTime(); this.lastPrefetchTime = new Date().getTime();
const time = this.lastPrefetchTime; var time = this.lastPrefetchTime;
var promise: Q.Promise<IListTableEntitiesSegmentedResult>;
if (this._documentIterator && this.continuationToken) { if (this._documentIterator && this.continuationToken) {
// TODO handle Cassandra case // TODO handle Cassandra case
const response = await this._documentIterator.fetchNext();
const entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(response?.resources);
return { promise = Q(this._documentIterator.fetchNext().then(response => response.resources)).then(
Results: entities, (documents: any[]) => {
ContinuationToken: this._documentIterator.hasMoreResults() let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents);
}; let finalEntities: IListTableEntitiesSegmentedResult = <IListTableEntitiesSegmentedResult>{
Results: entities,
ContinuationToken: this._documentIterator.hasMoreResults()
};
return Q.resolve(finalEntities);
}
);
} else if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) {
promise = this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection,
this.cqlQuery(),
true,
this.continuationToken
);
} else {
let query = this.sqlQuery();
if (this.queryTablesTab.container.isPreferredApiCassandra()) {
query = this.cqlQuery();
}
promise = this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection,
query,
true
);
} }
return promise
try { .then((result: IListTableEntitiesSegmentedResult) => {
let documents: IListTableEntitiesSegmentedResult;
if (this.continuationToken && this.queryTablesTab.container.isPreferredApiCassandra()) {
documents = await this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection,
this.cqlQuery(),
true,
this.continuationToken
);
} else {
const query = this.queryTablesTab.container.isPreferredApiCassandra() ? this.cqlQuery() : this.sqlQuery();
documents = await this.queryTablesTab.container.tableDataClient.queryDocuments(
this.queryTablesTab.collection,
query,
true
);
if (!this._documentIterator) { if (!this._documentIterator) {
this._documentIterator = documents.iterator; this._documentIterator = result.iterator;
} }
var actualDownloadSize: number = 0; var actualDownloadSize: number = 0;
@@ -472,11 +478,11 @@ export default class TableEntityListViewModel extends DataTableViewModel {
return Q.resolve(null); return Q.resolve(null);
} }
var entities = documents.Results; var entities = result.Results;
actualDownloadSize = entities.length; actualDownloadSize = entities.length;
// Queries can fetch no results and still return a continuation header. See prefetchAndRender() method. // Queries can fetch no results and still return a continuation header. See prefetchAndRender() method.
this.continuationToken = this.isCancelled ? null : documents.ContinuationToken; this.continuationToken = this.isCancelled ? null : result.ContinuationToken;
if (!this.continuationToken) { if (!this.continuationToken) {
this.allDownloaded = true; this.allDownloaded = true;
@@ -508,22 +514,20 @@ export default class TableEntityListViewModel extends DataTableViewModel {
// For #2.1, set prefetch exceeds maximum retry number and end prefetch. // For #2.1, set prefetch exceeds maximum retry number and end prefetch.
// For #2.2, go to next round prefetch. // For #2.2, go to next round prefetch.
if (this.allDownloaded || nextDownloadSize === 0) { if (this.allDownloaded || nextDownloadSize === 0) {
return documents; return Q.resolve(result);
} }
if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) { if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) {
documents.ExceedMaximumRetries = true; result.ExceedMaximumRetries = true;
return documents; return Q.resolve(result);
} }
return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
return await this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1); })
} .catch((error: Error) => {
} catch (error) { this.cache.serverCallInProgress = false;
this.cache.serverCallInProgress = false; return Q.reject(error);
throw error; });
}
} }
return null;
return undefined;
} }
} }

View File

@@ -4,7 +4,6 @@ import Q from "q";
import { displayTokenRenewalPromptForStatus, getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; import { displayTokenRenewalPromptForStatus, getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { FeedOptions } from "@azure/cosmos";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as Entities from "./Entities"; import * as Entities from "./Entities";
import * as HeadersUtility from "../../Common/HeadersUtility"; import * as HeadersUtility from "../../Common/HeadersUtility";
@@ -13,12 +12,9 @@ import * as TableConstants from "./Constants";
import * as TableEntityProcessor from "./TableEntityProcessor"; import * as TableEntityProcessor from "./TableEntityProcessor";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { queryDocuments, deleteDocument, updateDocument, createDocument } from "../../Common/DocumentClientUtilityBase";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import { handleError } from "../../Common/ErrorHandlingUtils"; import { handleError } from "../../Common/ErrorHandlingUtils";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
export interface CassandraTableKeys { export interface CassandraTableKeys {
partitionKeys: CassandraTableKey[]; partitionKeys: CassandraTableKey[];
@@ -42,19 +38,19 @@ export abstract class TableDataClient {
collection: ViewModels.Collection, collection: ViewModels.Collection,
originalDocument: any, originalDocument: any,
newEntity: Entities.ITableEntity newEntity: Entities.ITableEntity
): Promise<Entities.ITableEntity>; ): Q.Promise<Entities.ITableEntity>;
public abstract queryDocuments( public abstract queryDocuments(
collection: ViewModels.Collection, collection: ViewModels.Collection,
query: string, query: string,
shouldNotify?: boolean, shouldNotify?: boolean,
paginationToken?: string paginationToken?: string
): Promise<Entities.IListTableEntitiesResult>; ): Q.Promise<Entities.IListTableEntitiesResult>;
public abstract deleteDocuments( public abstract deleteDocuments(
collection: ViewModels.Collection, collection: ViewModels.Collection,
entitiesToDelete: Entities.ITableEntity[] entitiesToDelete: Entities.ITableEntity[]
): Promise<any>; ): Q.Promise<any>;
} }
export class TablesAPIDataClient extends TableDataClient { export class TablesAPIDataClient extends TableDataClient {
@@ -78,63 +74,77 @@ export class TablesAPIDataClient extends TableDataClient {
return deferred.promise; return deferred.promise;
} }
public async updateDocument( public updateDocument(
collection: ViewModels.Collection, collection: ViewModels.Collection,
originalDocument: any, originalDocument: any,
entity: Entities.ITableEntity entity: Entities.ITableEntity
): Promise<Entities.ITableEntity> { ): Q.Promise<Entities.ITableEntity> {
try { const deferred = Q.defer<Entities.ITableEntity>();
const newDocument = await updateDocument(
collection, updateDocument(
originalDocument, collection,
TableEntityProcessor.convertEntityToNewDocument(<Entities.ITableEntityForTablesAPI>entity) originalDocument,
); TableEntityProcessor.convertEntityToNewDocument(<Entities.ITableEntityForTablesAPI>entity)
return TableEntityProcessor.convertDocumentsToEntities([newDocument])[0]; ).then(
} catch (error) { (newDocument: any) => {
handleError(error, "TablesAPIDataClient/updateDocument"); const newEntity = TableEntityProcessor.convertDocumentsToEntities([newDocument])[0];
throw error; deferred.resolve(newEntity);
} },
reason => {
deferred.reject(reason);
}
);
return deferred.promise;
} }
public async queryDocuments( public queryDocuments(
collection: ViewModels.Collection, collection: ViewModels.Collection,
query: string query: string
): Promise<Entities.IListTableEntitiesResult> { ): Q.Promise<Entities.IListTableEntitiesResult> {
try { const deferred = Q.defer<Entities.IListTableEntitiesResult>();
const options = {
enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey()
} as FeedOptions;
const iterator = queryDocuments(collection.databaseId, collection.id(), query, options);
const response = await iterator.fetchNext();
const documents = response?.resources;
const entities = TableEntityProcessor.convertDocumentsToEntities(documents);
return { let options: any = {};
Results: entities, options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
ContinuationToken: iterator.hasMoreResults(), queryDocuments(collection.databaseId, collection.id(), query, options).then(
iterator: iterator iterator => {
}; iterator
} catch (error) { .fetchNext()
handleError(error, "TablesAPIDataClient/queryDocuments", "Query documents failed"); .then(response => response.resources)
throw error; .then(
} (documents: any[] = []) => {
let entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(documents);
let finalEntities: Entities.IListTableEntitiesResult = <Entities.IListTableEntitiesResult>{
Results: entities,
ContinuationToken: iterator.hasMoreResults(),
iterator: iterator
};
deferred.resolve(finalEntities);
},
reason => {
deferred.reject(reason);
}
);
},
reason => {
deferred.reject(reason);
}
);
return deferred.promise;
} }
public async deleteDocuments( public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise<any> {
collection: ViewModels.Collection, let documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments(
entitiesToDelete: Entities.ITableEntity[]
): Promise<any> {
const documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments(
<Entities.ITableEntityForTablesAPI[]>entitiesToDelete, <Entities.ITableEntityForTablesAPI[]>entitiesToDelete,
collection collection
); );
let promiseArray: Q.Promise<any>[] = [];
await Promise.all( documentsToDelete &&
documentsToDelete?.map(async document => { documentsToDelete.forEach(document => {
document.id = ko.observable<string>(document.id); document.id = ko.observable<string>(document.id);
await deleteDocument(collection, document); let promise: Q.Promise<any> = deleteDocument(collection, document);
}) promiseArray.push(promise);
); });
return Q.all(promiseArray);
} }
} }
@@ -170,7 +180,10 @@ export class CassandraAPIDataClient extends TableDataClient {
(data: any) => { (data: any) => {
entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)]; entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)];
entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString(); entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString();
NotificationConsoleUtils.logConsoleInfo(`Successfully added new row to table ${collection.id()}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully added new row to table ${collection.id()}`
);
deferred.resolve(entity); deferred.resolve(entity);
}, },
error => { error => {
@@ -184,149 +197,181 @@ export class CassandraAPIDataClient extends TableDataClient {
return deferred.promise; return deferred.promise;
} }
public async updateDocument( public updateDocument(
collection: ViewModels.Collection, collection: ViewModels.Collection,
originalDocument: any, originalDocument: any,
newEntity: Entities.ITableEntity newEntity: Entities.ITableEntity
): Promise<Entities.ITableEntity> { ): Q.Promise<Entities.ITableEntity> {
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Updating row ${originalDocument.RowKey._}`); const notificationId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
try { `Updating row ${originalDocument.RowKey._}`
let whereSegment = " WHERE"; );
let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat( const deferred = Q.defer<Entities.ITableEntity>();
collection.cassandraKeys.clusteringKeys let promiseArray: Q.Promise<any>[] = [];
); let query = `UPDATE ${collection.databaseId}.${collection.id()}`;
for (let keyIndex in keys) { let isChange: boolean = false;
const key = keys[keyIndex].property; for (let property in newEntity) {
const keyType = keys[keyIndex].type; if (!originalDocument[property] || newEntity[property]._.toString() !== originalDocument[property]._.toString()) {
whereSegment += this.isStringType(keyType) if (this.isStringType(newEntity[property].$)) {
? ` ${key} = '${newEntity[key]._}' AND` query = `${query} SET ${property} = '${newEntity[property]._}',`;
: ` ${key} = ${newEntity[key]._} AND`; } else {
} query = `${query} SET ${property} = ${newEntity[property]._},`;
whereSegment = whereSegment.slice(0, whereSegment.length - 4);
let updateQuery = `UPDATE ${collection.databaseId}.${collection.id()}`;
let isPropertyUpdated = false;
for (let property in newEntity) {
if (
!originalDocument[property] ||
newEntity[property]._.toString() !== originalDocument[property]._.toString()
) {
updateQuery += this.isStringType(newEntity[property].$)
? ` SET ${property} = '${newEntity[property]._}',`
: ` SET ${property} = ${newEntity[property]._},`;
isPropertyUpdated = true;
} }
isChange = true;
} }
if (isPropertyUpdated) {
updateQuery = updateQuery.slice(0, updateQuery.length - 1);
updateQuery += whereSegment;
await this.queryDocuments(collection, updateQuery);
}
let deleteQuery = `DELETE `;
let isPropertyDeleted = false;
for (let property in originalDocument) {
if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) {
deleteQuery += ` ${property},`;
isPropertyDeleted = true;
}
}
if (isPropertyDeleted) {
deleteQuery = deleteQuery.slice(0, deleteQuery.length - 1);
deleteQuery += ` FROM ${collection.databaseId}.${collection.id()}${whereSegment}`;
await this.queryDocuments(collection, deleteQuery);
}
newEntity[TableConstants.EntityKeyNames.RowKey] = originalDocument[TableConstants.EntityKeyNames.RowKey];
NotificationConsoleUtils.logConsoleInfo(`Successfully updated row ${newEntity.RowKey._}`);
return newEntity;
} catch (error) {
handleError(error, "UpdateRowCassandra", "Failed to update row ${newEntity.RowKey._}");
throw error;
} finally {
clearMessage();
} }
query = query.slice(0, query.length - 1);
let whereSegment = " WHERE";
let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat(
collection.cassandraKeys.clusteringKeys
);
for (let keyIndex in keys) {
const key = keys[keyIndex].property;
const keyType = keys[keyIndex].type;
if (this.isStringType(keyType)) {
whereSegment = `${whereSegment} ${key} = '${newEntity[key]._}' AND`;
} else {
whereSegment = `${whereSegment} ${key} = ${newEntity[key]._} AND`;
}
}
whereSegment = whereSegment.slice(0, whereSegment.length - 4);
query = query + whereSegment;
if (isChange) {
promiseArray.push(this.queryDocuments(collection, query));
}
query = `DELETE `;
for (let property in originalDocument) {
if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) {
query = `${query} ${property},`;
}
}
if (query.length > 7) {
query = query.slice(0, query.length - 1);
query = `${query} FROM ${collection.databaseId}.${collection.id()}${whereSegment}`;
promiseArray.push(this.queryDocuments(collection, query));
}
Q.all(promiseArray)
.then(
(data: any) => {
newEntity[TableConstants.EntityKeyNames.RowKey] = originalDocument[TableConstants.EntityKeyNames.RowKey];
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully updated row ${newEntity.RowKey._}`
);
deferred.resolve(newEntity);
},
error => {
handleError(error, "UpdateRowCassandra", `Failed to update row ${newEntity.RowKey._}`);
deferred.reject(error);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
});
return deferred.promise;
} }
public async queryDocuments( public queryDocuments(
collection: ViewModels.Collection, collection: ViewModels.Collection,
query: string, query: string,
shouldNotify?: boolean, shouldNotify?: boolean,
paginationToken?: string paginationToken?: string
): Promise<Entities.IListTableEntitiesResult> { ): Q.Promise<Entities.IListTableEntitiesResult> {
const clearMessage = let notificationId: string;
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`); if (shouldNotify) {
try { notificationId = NotificationConsoleUtils.logConsoleMessage(
const authType = window.authType; ConsoleDataType.InProgress,
const apiEndpoint: string = `Querying rows for table ${collection.id()}`
authType === AuthType.EncryptedToken );
? Constants.CassandraBackend.guestQueryApi
: Constants.CassandraBackend.queryApi;
const data: any = await $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
type: "POST",
data: {
accountName:
collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
cassandraEndpoint: this.trimCassandraEndpoint(
collection.container.databaseAccount().properties.cassandraEndpoint
),
resourceId: collection.container.databaseAccount().id,
keyspaceId: collection.databaseId,
tableId: collection.id(),
query,
paginationToken
},
beforeSend: this.setAuthorizationHeader,
error: this.handleAjaxError,
cache: false
});
shouldNotify &&
NotificationConsoleUtils.logConsoleInfo(
`Successfully fetched ${data.result.length} rows for table ${collection.id()}`
);
return {
Results: data.result,
ContinuationToken: data.paginationToken
};
} catch (error) {
shouldNotify &&
handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`);
throw error;
} finally {
clearMessage?.();
} }
const deferred = Q.defer<Entities.IListTableEntitiesResult>();
const authType = window.authType;
const apiEndpoint: string =
authType === AuthType.EncryptedToken
? Constants.CassandraBackend.guestQueryApi
: Constants.CassandraBackend.queryApi;
$.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
type: "POST",
data: {
accountName: collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
cassandraEndpoint: this.trimCassandraEndpoint(
collection.container.databaseAccount().properties.cassandraEndpoint
),
resourceId: collection.container.databaseAccount().id,
keyspaceId: collection.databaseId,
tableId: collection.id(),
query: query,
paginationToken: paginationToken
},
beforeSend: this.setAuthorizationHeader,
error: this.handleAjaxError,
cache: false
})
.then(
(data: any) => {
if (shouldNotify) {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully fetched ${data.result.length} rows for table ${collection.id()}`
);
}
deferred.resolve({
Results: data.result,
ContinuationToken: data.paginationToken
});
},
(error: any) => {
if (shouldNotify) {
handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`);
}
deferred.reject(error);
}
)
.done(() => {
if (shouldNotify) {
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
}
});
return deferred.promise;
} }
public async deleteDocuments( public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise<any> {
collection: ViewModels.Collection,
entitiesToDelete: Entities.ITableEntity[]
): Promise<any> {
const query = `DELETE FROM ${collection.databaseId}.${collection.id()} WHERE `; const query = `DELETE FROM ${collection.databaseId}.${collection.id()} WHERE `;
const partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection); let promiseArray: Q.Promise<any>[] = [];
let partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection);
await Promise.all( for (let i = 0, len = entitiesToDelete.length; i < len; i++) {
entitiesToDelete.map(async (currEntityToDelete: Entities.ITableEntity) => { let currEntityToDelete: Entities.ITableEntity = entitiesToDelete[i];
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting row ${currEntityToDelete.RowKey._}`); let currQuery = query;
const partitionKeyValue = currEntityToDelete[partitionKeyProperty]; let partitionKeyValue = currEntityToDelete[partitionKeyProperty];
const currQuery = if (partitionKeyValue._ != null && this.isStringType(partitionKeyValue.$)) {
query + this.isStringType(partitionKeyValue.$) currQuery = `${currQuery}${partitionKeyProperty} = '${partitionKeyValue._}' AND `;
? `${partitionKeyProperty} = '${partitionKeyValue._}'` } else {
: `${partitionKeyProperty} = ${partitionKeyValue._}`; currQuery = `${currQuery}${partitionKeyProperty} = ${partitionKeyValue._} AND `;
}
try { currQuery = currQuery.slice(0, currQuery.length - 5);
await this.queryDocuments(collection, currQuery); const notificationId = NotificationConsoleUtils.logConsoleMessage(
NotificationConsoleUtils.logConsoleInfo(`Successfully deleted row ${currEntityToDelete.RowKey._}`); ConsoleDataType.InProgress,
} catch (error) { `Deleting row ${currEntityToDelete.RowKey._}`
handleError(error, "DeleteRowCassandra", `Error while deleting row ${currEntityToDelete.RowKey._}`); );
throw error; promiseArray.push(
} finally { this.queryDocuments(collection, currQuery)
clearMessage(); .then(
} () => {
}) NotificationConsoleUtils.logConsoleMessage(
); ConsoleDataType.Info,
`Successfully deleted row ${currEntityToDelete.RowKey._}`
);
},
error => {
handleError(error, "DeleteRowCassandra", `Error while deleting row ${currEntityToDelete.RowKey._}`);
}
)
.finally(() => {
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
})
);
}
return Q.all(promiseArray);
} }
public createKeyspace( public createKeyspace(

View File

@@ -16,16 +16,18 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
import DiscardIcon from "../../../images/discard.svg"; import DiscardIcon from "../../../images/discard.svg";
import DeleteIcon from "../../../images/delete.svg"; import DeleteIcon from "../../../images/delete.svg";
import { QueryIterator, Resource, ConflictDefinition, FeedOptions } from "@azure/cosmos"; import { QueryIterator, ItemDefinition, Resource, ConflictDefinition } from "@azure/cosmos";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import {
queryConflicts,
deleteConflict,
deleteDocument,
createDocument,
updateDocument
} from "../../Common/DocumentClientUtilityBase";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import { deleteConflict } from "../../Common/dataAccess/deleteConflict";
import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
export default class ConflictsTab extends TabsBase { export default class ConflictsTab extends TabsBase {
public selectedConflictId: ko.Observable<ConflictId>; public selectedConflictId: ko.Observable<ConflictId>;
@@ -223,15 +225,25 @@ export default class ConflictsTab extends TabsBase {
}); });
} }
public async refreshDocumentsGrid(): Promise<void> { public refreshDocumentsGrid(): Q.Promise<any> {
try { // clear documents grid
// clear documents grid this.conflictIds([]);
this.conflictIds([]); return this.createIterator()
this._documentsIterator = this.createIterator(); .then(
await this.loadNextPage(); // reset iterator
} catch (error) { iterator => {
window.alert(getErrorMessage(error)); this._documentsIterator = iterator;
} }
)
.then(
// load documents
() => {
return this.loadNextPage();
}
)
.catch(error => {
window.alert(getErrorMessage(error));
});
} }
public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
@@ -253,9 +265,9 @@ export default class ConflictsTab extends TabsBase {
return Q(); return Q();
} }
public onAcceptChangesClick = async (): Promise<void> => { public onAcceptChangesClick = (): Q.Promise<any> => {
if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) { if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) {
return; return Q();
} }
this.isExecutionError(false); this.isExecutionError(false);
@@ -273,79 +285,81 @@ export default class ConflictsTab extends TabsBase {
conflictResourceId: selectedConflict.resourceId conflictResourceId: selectedConflict.resourceId
}); });
try { let operationPromise: Q.Promise<any> = Q();
if (selectedConflict.operationType === Constants.ConflictOperationType.Replace) { if (selectedConflict.operationType === Constants.ConflictOperationType.Replace) {
const documentContent = JSON.parse(this.selectedConflictContent()); const documentContent = JSON.parse(this.selectedConflictContent());
await updateDocument( operationPromise = updateDocument(
this.collection, this.collection,
selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]), selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]),
documentContent documentContent
);
}
if (selectedConflict.operationType === Constants.ConflictOperationType.Create) {
const documentContent = JSON.parse(this.selectedConflictContent());
await createDocument(this.collection, documentContent);
}
if (
selectedConflict.operationType === Constants.ConflictOperationType.Delete &&
!!this.selectedConflictContent()
) {
const documentContent = JSON.parse(this.selectedConflictContent());
await deleteDocument(
this.collection,
selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty])
);
}
await deleteConflict(this.collection, selectedConflict);
this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid);
this.selectedConflictContent("");
this.selectedConflictCurrent("");
this.selectedConflictId(null);
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
TelemetryProcessor.traceSuccess(
Action.ResolveConflict,
{
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
defaultExperience: this.collection && this.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
conflictResourceType: selectedConflict.resourceType,
conflictOperationType: selectedConflict.operationType,
conflictResourceId: selectedConflict.resourceId
},
startKey
); );
} catch (error) {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
window.alert(errorMessage);
TelemetryProcessor.traceFailure(
Action.ResolveConflict,
{
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
defaultExperience: this.collection && this.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
conflictResourceType: selectedConflict.resourceType,
conflictOperationType: selectedConflict.operationType,
conflictResourceId: selectedConflict.resourceId,
error: errorMessage,
errorStack: getErrorStack(error)
},
startKey
);
} finally {
this.isExecuting(false);
} }
if (selectedConflict.operationType === Constants.ConflictOperationType.Create) {
const documentContent = JSON.parse(this.selectedConflictContent());
operationPromise = createDocument(this.collection, documentContent);
}
if (selectedConflict.operationType === Constants.ConflictOperationType.Delete && !!this.selectedConflictContent()) {
const documentContent = JSON.parse(this.selectedConflictContent());
operationPromise = deleteDocument(
this.collection,
selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty])
);
}
return operationPromise
.then(
() => {
return deleteConflict(this.collection, selectedConflict).then(() => {
this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid);
this.selectedConflictContent("");
this.selectedConflictCurrent("");
this.selectedConflictId(null);
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
TelemetryProcessor.traceSuccess(
Action.ResolveConflict,
{
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
defaultExperience: this.collection && this.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
conflictResourceType: selectedConflict.resourceType,
conflictOperationType: selectedConflict.operationType,
conflictResourceId: selectedConflict.resourceId
},
startKey
);
});
},
error => {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
window.alert(errorMessage);
TelemetryProcessor.traceFailure(
Action.ResolveConflict,
{
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
defaultExperience: this.collection && this.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
conflictResourceType: selectedConflict.resourceType,
conflictOperationType: selectedConflict.operationType,
conflictResourceId: selectedConflict.resourceId,
error: errorMessage,
errorStack: getErrorStack(error)
},
startKey
);
}
)
.finally(() => this.isExecuting(false));
}; };
public onDeleteClick = async (): Promise<void> => { public onDeleteClick = (): Q.Promise<any> => {
this.isExecutionError(false); this.isExecutionError(false);
this.isExecuting(true); this.isExecuting(true);
@@ -361,48 +375,50 @@ export default class ConflictsTab extends TabsBase {
conflictResourceId: selectedConflict.resourceId conflictResourceId: selectedConflict.resourceId
}); });
try { return deleteConflict(this.collection, selectedConflict)
await deleteConflict(this.collection, selectedConflict); .then(
this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid); () => {
this.selectedConflictContent(""); this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid);
this.selectedConflictCurrent(""); this.selectedConflictContent("");
this.selectedConflictId(null); this.selectedConflictCurrent("");
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected); this.selectedConflictId(null);
TelemetryProcessor.traceSuccess( this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
Action.DeleteConflict, TelemetryProcessor.traceSuccess(
{ Action.DeleteConflict,
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, {
defaultExperience: this.collection && this.collection.container.defaultExperience(), databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
dataExplorerArea: Constants.Areas.Tab, defaultExperience: this.collection && this.collection.container.defaultExperience(),
tabTitle: this.tabTitle(), dataExplorerArea: Constants.Areas.Tab,
conflictResourceType: selectedConflict.resourceType, tabTitle: this.tabTitle(),
conflictOperationType: selectedConflict.operationType, conflictResourceType: selectedConflict.resourceType,
conflictResourceId: selectedConflict.resourceId conflictOperationType: selectedConflict.operationType,
conflictResourceId: selectedConflict.resourceId
},
startKey
);
}, },
startKey error => {
); this.isExecutionError(true);
} catch (error) { const errorMessage = getErrorMessage(error);
this.isExecutionError(true); window.alert(errorMessage);
const errorMessage = getErrorMessage(error); TelemetryProcessor.traceFailure(
window.alert(errorMessage); Action.DeleteConflict,
TelemetryProcessor.traceFailure( {
Action.DeleteConflict, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
{ defaultExperience: this.collection && this.collection.container.defaultExperience(),
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, dataExplorerArea: Constants.Areas.Tab,
defaultExperience: this.collection && this.collection.container.defaultExperience(), tabTitle: this.tabTitle(),
dataExplorerArea: Constants.Areas.Tab, conflictResourceType: selectedConflict.resourceType,
tabTitle: this.tabTitle(), conflictOperationType: selectedConflict.operationType,
conflictResourceType: selectedConflict.resourceType, conflictResourceId: selectedConflict.resourceId,
conflictOperationType: selectedConflict.operationType, error: errorMessage,
conflictResourceId: selectedConflict.resourceId, errorStack: getErrorStack(error)
error: errorMessage, },
errorStack: getErrorStack(error) startKey
}, );
startKey }
); )
} finally { .finally(() => this.isExecuting(false));
this.isExecuting(false);
}
}; };
public onDiscardClick = (): Q.Promise<any> => { public onDiscardClick = (): Q.Promise<any> => {
@@ -429,47 +445,60 @@ export default class ConflictsTab extends TabsBase {
return Q(); return Q();
} }
public onTabClick(): void { public onTabClick(): Q.Promise<any> {
super.onTabClick(); return super.onTabClick().then(() => {
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts); this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts);
});
} }
public async onActivate(): Promise<void> { public onActivate(): Q.Promise<any> {
super.onActivate(); return super.onActivate().then(() => {
if (this._documentsIterator) {
if (!this._documentsIterator) { return Q.resolve(this._documentsIterator);
try {
this._documentsIterator = await this.createIterator();
await this.loadNextPage();
} catch (error) {
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
TelemetryProcessor.traceFailure(
Action.Tab,
{
databaseAccountName: this.collection.container.databaseAccount().name,
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
defaultExperience: this.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error)
},
this.onLoadStartKey
);
this.onLoadStartKey = null;
}
} }
}
return this.createIterator().then(
(iterator: QueryIterator<ItemDefinition & Resource>) => {
this._documentsIterator = iterator;
return this.loadNextPage();
},
error => {
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
TelemetryProcessor.traceFailure(
Action.Tab,
{
databaseAccountName: this.collection.container.databaseAccount().name,
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
defaultExperience: this.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error)
},
this.onLoadStartKey
);
this.onLoadStartKey = null;
}
}
);
});
} }
public createIterator(): QueryIterator<ConflictDefinition & Resource> { public onRefreshClick(): Q.Promise<any> {
return this.refreshDocumentsGrid().then(() => {
this.selectedConflictContent("");
this.selectedConflictId(null);
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
});
}
public createIterator(): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
// TODO: Conflict Feed does not allow filtering atm // TODO: Conflict Feed does not allow filtering atm
const query: string = undefined; const query: string = undefined;
const options = { let options: any = {};
enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey() options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
}; return queryConflicts(this.collection.databaseId, this.collection.id(), query, options);
return queryConflicts(this.collection.databaseId, this.collection.id(), query, options as FeedOptions);
} }
public loadNextPage(): Q.Promise<any> { public loadNextPage(): Q.Promise<any> {

View File

@@ -155,7 +155,8 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
this.overrideWithAutoPilotSettings() ? this.autoPilotThroughput() : this.throughput(), this.overrideWithAutoPilotSettings() ? this.autoPilotThroughput() : this.throughput(),
serverId, serverId,
regions, regions,
multimaster multimaster,
false /*rupmEnabled*/
); );
} else { } else {
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml( estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
@@ -230,7 +231,9 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
return this.throughputTitle() + this.requestUnitsUsageCost(); return this.throughputTitle() + this.requestUnitsUsageCost();
}); });
this.pendingNotification = ko.observable<DataModels.Notification>(); this.pendingNotification = ko.observable<DataModels.Notification>();
this._offerReplacePending = ko.observable<boolean>(!!this.database.offer()?.offerReplacePending); this._offerReplacePending = ko.observable<boolean>(
!!this.database.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending]
);
this.notificationStatusInfo = ko.observable<string>(""); this.notificationStatusInfo = ko.observable<string>("");
this.shouldShowNotificationStatusPrompt = ko.computed<boolean>(() => this.notificationStatusInfo().length > 0); this.shouldShowNotificationStatusPrompt = ko.computed<boolean>(() => this.notificationStatusInfo().length > 0);
this.warningMessage = ko.computed<string>(() => { this.warningMessage = ko.computed<string>(() => {
@@ -239,7 +242,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
} }
const offer = this.database.offer(); const offer = this.database.offer();
if (offer?.offerReplacePending) { if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) {
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput; const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id()); return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id());
} }
@@ -429,10 +432,11 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
return Q(); return Q();
}; };
public async onActivate(): Promise<void> { public onActivate(): Q.Promise<any> {
super.onActivate(); return super.onActivate().then(async () => {
this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings); this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
await this.database.loadOffer(); await this.database.loadOffer();
});
} }
private _setBaseline() { private _setBaseline() {

View File

@@ -103,7 +103,7 @@
<button <button
class="filterbtnstyle queryButton" class="filterbtnstyle queryButton"
data-bind=" data-bind="
click: refreshDocumentsGrid, click: onApplyFilterClick,
enable: applyFilterButton.enabled" enable: applyFilterButton.enabled"
aria-label="Apply filter" aria-label="Apply filter"
tabindex="0" tabindex="0"

View File

@@ -19,24 +19,19 @@ import SaveIcon from "../../../images/save-cosmos.svg";
import DiscardIcon from "../../../images/discard.svg"; import DiscardIcon from "../../../images/discard.svg";
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg"; import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
import UploadIcon from "../../../images/Upload_16x16.svg"; import UploadIcon from "../../../images/Upload_16x16.svg";
import { import { extractPartitionKey, PartitionKeyDefinition, QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
extractPartitionKey,
PartitionKeyDefinition,
QueryIterator,
ItemDefinition,
Resource,
Item
} from "@azure/cosmos";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import {
readDocument,
queryDocuments,
deleteDocument,
updateDocument,
createDocument
} from "../../Common/DocumentClientUtilityBase";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { readDocument } from "../../Common/dataAccess/readDocument";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import { createDocument } from "../../Common/dataAccess/createDocument";
export default class DocumentsTab extends TabsBase { export default class DocumentsTab extends TabsBase {
public selectedDocumentId: ko.Observable<DocumentId>; public selectedDocumentId: ko.Observable<DocumentId>;
@@ -374,22 +369,36 @@ export default class DocumentsTab extends TabsBase {
return true; return true;
}; };
public async refreshDocumentsGrid(): Promise<void> { public onApplyFilterClick(): Q.Promise<any> {
// clear documents grid // clear documents grid
this.documentIds([]); this.documentIds([]);
return this.createIterator()
.then(
// reset iterator
iterator => {
this._documentsIterator = iterator;
}
)
.then(
// load documents
() => {
return this.loadNextPage();
}
)
.then(() => {
// collapse filter
this.appliedFilter(this.filterContent());
this.isFilterExpanded(false);
const focusElement = document.getElementById("errorStatusIcon");
focusElement && focusElement.focus();
})
.catch(error => {
window.alert(getErrorMessage(error));
});
}
try { public refreshDocumentsGrid(): Q.Promise<any> {
// reset iterator return this.onApplyFilterClick();
this._documentsIterator = this.createIterator();
// load documents
await this.loadNextPage();
// collapse filter
this.appliedFilter(this.filterContent());
this.isFilterExpanded(false);
document.getElementById("errorStatusIcon")?.focus();
} catch (error) {
window.alert(getErrorMessage(error));
}
} }
public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => { public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
@@ -425,7 +434,7 @@ export default class DocumentsTab extends TabsBase {
return Q(); return Q();
}; };
public onSaveNewDocumentClick = (): Promise<any> => { public onSaveNewDocumentClick = (): Q.Promise<any> => {
this.isExecutionError(false); this.isExecutionError(false);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
@@ -493,7 +502,7 @@ export default class DocumentsTab extends TabsBase {
return Q(); return Q();
}; };
public onSaveExisitingDocumentClick = (): Promise<any> => { public onSaveExisitingDocumentClick = (): Q.Promise<any> => {
const selectedDocumentId = this.selectedDocumentId(); const selectedDocumentId = this.selectedDocumentId();
const documentContent = JSON.parse(this.selectedDocumentContent()); const documentContent = JSON.parse(this.selectedDocumentContent());
@@ -562,15 +571,17 @@ export default class DocumentsTab extends TabsBase {
return Q(); return Q();
}; };
public onDeleteExisitingDocumentClick = async (): Promise<void> => { public onDeleteExisitingDocumentClick = (): Q.Promise<any> => {
const selectedDocumentId = this.selectedDocumentId(); const selectedDocumentId = this.selectedDocumentId();
const msg = !this.isPreferredApiMongoDB const msg = !this.isPreferredApiMongoDB
? "Are you sure you want to delete the selected item ?" ? "Are you sure you want to delete the selected item ?"
: "Are you sure you want to delete the selected document ?"; : "Are you sure you want to delete the selected document ?";
if (window.confirm(msg)) { if (window.confirm(msg)) {
await this._deleteDocument(selectedDocumentId); return this._deleteDocument(selectedDocumentId);
} }
return Q();
}; };
public onValidDocumentEdit(): Q.Promise<any> { public onValidDocumentEdit(): Q.Promise<any> {
@@ -606,50 +617,63 @@ export default class DocumentsTab extends TabsBase {
return Q(); return Q();
} }
public onTabClick(): void { public onTabClick(): Q.Promise<any> {
super.onTabClick(); return super.onTabClick().then(() => {
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
});
} }
public async onActivate(): Promise<void> { public onActivate(): Q.Promise<any> {
super.onActivate(); return super.onActivate().then(() => {
if (this._documentsIterator) {
if (!this._documentsIterator) { return Q.resolve(this._documentsIterator);
try {
this._documentsIterator = this.createIterator();
await this.loadNextPage();
} catch (error) {
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
TelemetryProcessor.traceFailure(
Action.Tab,
{
databaseAccountName: this.collection.container.databaseAccount().name,
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
defaultExperience: this.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error)
},
this.onLoadStartKey
);
this.onLoadStartKey = null;
}
} }
}
return this.createIterator().then(
(iterator: QueryIterator<ItemDefinition & Resource>) => {
this._documentsIterator = iterator;
return this.loadNextPage();
},
error => {
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
TelemetryProcessor.traceFailure(
Action.Tab,
{
databaseAccountName: this.collection.container.databaseAccount().name,
databaseName: this.collection.databaseId,
collectionName: this.collection.id(),
defaultExperience: this.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle(),
error: getErrorMessage(error),
errorStack: getErrorStack(error)
},
this.onLoadStartKey
);
this.onLoadStartKey = null;
}
}
);
});
} }
public onRefreshClick(): Q.Promise<any> {
return this.refreshDocumentsGrid().then(() => {
this.selectedDocumentContent("");
this.selectedDocumentId(null);
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
});
}
private _isIgnoreDirtyEditor = (): boolean => { private _isIgnoreDirtyEditor = (): boolean => {
var msg: string = "Changes will be lost. Do you want to continue?"; var msg: string = "Changes will be lost. Do you want to continue?";
return window.confirm(msg); return window.confirm(msg);
}; };
protected __deleteDocument(documentId: DocumentId): Promise<void> { protected __deleteDocument(documentId: DocumentId): Q.Promise<any> {
return deleteDocument(this.collection, documentId); return deleteDocument(this.collection, documentId);
} }
private _deleteDocument(selectedDocumentId: DocumentId): Promise<void> { private _deleteDocument(selectedDocumentId: DocumentId): Q.Promise<any> {
this.isExecutionError(false); this.isExecutionError(false);
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, { const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, {
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
@@ -660,7 +684,7 @@ export default class DocumentsTab extends TabsBase {
this.isExecuting(true); this.isExecuting(true);
return this.__deleteDocument(selectedDocumentId) return this.__deleteDocument(selectedDocumentId)
.then( .then(
() => { (result: any) => {
this.documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid); this.documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid);
this.selectedDocumentContent(""); this.selectedDocumentContent("");
this.selectedDocumentId(null); this.selectedDocumentId(null);
@@ -696,7 +720,7 @@ export default class DocumentsTab extends TabsBase {
.finally(() => this.isExecuting(false)); .finally(() => this.isExecuting(false));
} }
public createIterator(): QueryIterator<ItemDefinition & Resource> { public createIterator(): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
let filters = this.lastFilterContents(); let filters = this.lastFilterContents();
const filter: string = this.filterContent().trim(); const filter: string = this.filterContent().trim();
const query: string = this.buildQuery(filter); const query: string = this.buildQuery(filter);
@@ -710,10 +734,11 @@ export default class DocumentsTab extends TabsBase {
return queryDocuments(this.collection.databaseId, this.collection.id(), query, options); return queryDocuments(this.collection.databaseId, this.collection.id(), query, options);
} }
public async selectDocument(documentId: DocumentId): Promise<void> { public selectDocument(documentId: DocumentId): Q.Promise<any> {
this.selectedDocumentId(documentId); this.selectedDocumentId(documentId);
const content = await readDocument(this.collection, documentId); return readDocument(this.collection, documentId).then((content: any) => {
this.initDocumentEditor(documentId, content); this.initDocumentEditor(documentId, content);
});
} }
public loadNextPage(): Q.Promise<any> { public loadNextPage(): Q.Promise<any> {

View File

@@ -114,9 +114,10 @@ export default class GraphTab extends TabsBase {
: `${account.name}.graphs.azure.com:443/`; : `${account.name}.graphs.azure.com:443/`;
} }
public onTabClick(): void { public onTabClick(): Q.Promise<any> {
super.onTabClick(); return super.onTabClick().then(() => {
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Graph); this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Graph);
});
} }
/** /**

View File

@@ -289,7 +289,7 @@
<button <button
class="filterbtnstyle queryButton" class="filterbtnstyle queryButton"
data-bind=" data-bind="
click: refreshDocumentsGrid, click: onApplyFilterClick,
enable: applyFilterButton.enabled" enable: applyFilterButton.enabled"
> >
Apply Filter Apply Filter

View File

@@ -44,7 +44,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
super.buildCommandBarOptions(); super.buildCommandBarOptions();
} }
public onSaveNewDocumentClick = (): Promise<any> => { public onSaveNewDocumentClick = (): Q.Promise<any> => {
const documentContent = JSON.parse(this.selectedDocumentContent()); const documentContent = JSON.parse(this.selectedDocumentContent());
this.displayedError(""); this.displayedError("");
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
@@ -78,12 +78,12 @@ export default class MongoDocumentsTab extends DocumentsTab {
startKey startKey
); );
Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab"); Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab");
throw new Error("Document without shard key"); return Q.reject("Document without shard key");
} }
this.isExecutionError(false); this.isExecutionError(false);
this.isExecuting(true); this.isExecuting(true);
return createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent) return Q(createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent))
.then( .then(
(savedDocument: any) => { (savedDocument: any) => {
let partitionKeyArray = extractPartitionKey( let partitionKeyArray = extractPartitionKey(
@@ -136,7 +136,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
.finally(() => this.isExecuting(false)); .finally(() => this.isExecuting(false));
}; };
public onSaveExisitingDocumentClick = (): Promise<any> => { public onSaveExisitingDocumentClick = (): Q.Promise<any> => {
const selectedDocumentId = this.selectedDocumentId(); const selectedDocumentId = this.selectedDocumentId();
const documentContent = this.selectedDocumentContent(); const documentContent = this.selectedDocumentContent();
this.isExecutionError(false); this.isExecutionError(false);
@@ -148,7 +148,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
tabTitle: this.tabTitle() tabTitle: this.tabTitle()
}); });
return updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent) return Q(updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent))
.then( .then(
(updatedDocument: any) => { (updatedDocument: any) => {
let value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4); let value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4);
@@ -204,10 +204,13 @@ export default class MongoDocumentsTab extends DocumentsTab {
return filter || "{}"; return filter || "{}";
} }
public async selectDocument(documentId: DocumentId): Promise<void> { public selectDocument(documentId: DocumentId): Q.Promise<any> {
this.selectedDocumentId(documentId); this.selectedDocumentId(documentId);
const content = await readDocument(this.collection.databaseId, this.collection, documentId); return Q(
this.initDocumentEditor(documentId, content); readDocument(this.collection.databaseId, this.collection, documentId).then((content: any) => {
this.initDocumentEditor(documentId, content);
})
);
} }
public loadNextPage(): Q.Promise<any> { public loadNextPage(): Q.Promise<any> {
@@ -327,7 +330,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
return partitionKey; return partitionKey;
} }
protected __deleteDocument(documentId: DocumentId): Promise<void> { protected __deleteDocument(documentId: DocumentId): Q.Promise<any> {
return deleteDocument(this.collection.databaseId, this.collection, documentId); return Q(deleteDocument(this.collection.databaseId, this.collection, documentId));
} }
} }

View File

@@ -53,9 +53,10 @@ export default class MongoShellTab extends TabsBase {
// } // }
} }
public onTabClick(): void { public onTabClick(): Q.Promise<any> {
super.onTabClick(); return super.onTabClick().then(() => {
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
});
} }
public handleMessage(event: MessageEvent) { public handleMessage(event: MessageEvent) {

View File

@@ -15,10 +15,9 @@ import { QueryUtils } from "../../Utils/QueryUtils";
import SaveQueryIcon from "../../../images/save-cosmos.svg"; import SaveQueryIcon from "../../../images/save-cosmos.svg";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import { queryDocuments, queryDocumentsPage } from "../../Common/DocumentClientUtilityBase";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage";
enum ToggleState { enum ToggleState {
Result, Result,
@@ -164,19 +163,20 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
this._buildCommandBarOptions(); this._buildCommandBarOptions();
} }
public onTabClick(): void { public onTabClick(): Q.Promise<any> {
super.onTabClick(); return super.onTabClick().then(() => {
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query); this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query);
});
} }
public onExecuteQueryClick = async (): Promise<void> => { public onExecuteQueryClick = (): Q.Promise<any> => {
const sqlStatement: string = this.selectedContent() || this.sqlQueryEditorContent(); const sqlStatement: string = this.selectedContent() || this.sqlQueryEditorContent();
this.sqlStatementToExecute(sqlStatement); this.sqlStatementToExecute(sqlStatement);
this.allResultsMetadata([]); this.allResultsMetadata([]);
this.queryResults(""); this.queryResults("");
this._iterator = undefined; this._iterator = null;
await this._executeQueryDocumentsPage(0); return this._executeQueryDocumentsPage(0);
}; };
public onLoadQueryClick = (): void => { public onLoadQueryClick = (): void => {
@@ -191,13 +191,13 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
this.collection && this.collection.container && this.collection.container.browseQueriesPane.open(); this.collection && this.collection.container && this.collection.container.browseQueriesPane.open();
}; };
public async onFetchNextPageClick(): Promise<void> { public onFetchNextPageClick(): Q.Promise<any> {
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || []; const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1]; const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1; const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1;
const itemCount: number = (metadata && Number(metadata.itemCount)) || 0; const itemCount: number = (metadata && Number(metadata.itemCount)) || 0;
await this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1); return this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1);
} }
public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => { public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => {
@@ -265,18 +265,19 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
return true; return true;
}; };
private async _executeQueryDocumentsPage(firstItemIndex: number): Promise<any> { private _executeQueryDocumentsPage(firstItemIndex: number): Q.Promise<any> {
this.error(""); this.error("");
this.roundTrips(undefined); this.roundTrips(undefined);
if (this._iterator === undefined) { if (this._iterator == null) {
this._initIterator(); const queryIteratorPromise = this._initIterator();
return queryIteratorPromise.finally(() => this._queryDocumentsPage(firstItemIndex));
} }
await this._queryDocumentsPage(firstItemIndex); return this._queryDocumentsPage(firstItemIndex);
} }
// TODO: Position and enable spinner when request is in progress // TODO: Position and enable spinner when request is in progress
private async _queryDocumentsPage(firstItemIndex: number): Promise<void> { private _queryDocumentsPage(firstItemIndex: number): Q.Promise<any> {
this.isExecutionError(false); this.isExecutionError(false);
this._resetAggregateQueryMetrics(); this._resetAggregateQueryMetrics();
const startKey: number = TelemetryProcessor.traceStart(Action.ExecuteQuery, { const startKey: number = TelemetryProcessor.traceStart(Action.ExecuteQuery, {
@@ -288,90 +289,90 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
let options: any = {}; let options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
const queryDocuments = async (firstItemIndex: number) => const queryDocuments = (firstItemIndex: number) =>
await queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex); queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex, options);
this.isExecuting(true); this.isExecuting(true);
return QueryUtils.queryPagesUntilContentPresent(firstItemIndex, queryDocuments)
.then(
(queryResults: ViewModels.QueryResults) => {
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
const resultsMetadata: ViewModels.QueryResultsMetadata = {
hasMoreResults: queryResults.hasMoreResults,
itemCount: queryResults.itemCount,
firstItemIndex: queryResults.firstItemIndex,
lastItemIndex: queryResults.lastItemIndex
};
this.allResultsMetadata.push(resultsMetadata);
this.activityId(queryResults.activityId);
this.roundTrips(queryResults.roundTrips);
try { this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]);
const queryResults: ViewModels.QueryResults = await QueryUtils.queryPagesUntilContentPresent(
firstItemIndex,
queryDocuments
);
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
const resultsMetadata: ViewModels.QueryResultsMetadata = {
hasMoreResults: queryResults.hasMoreResults,
itemCount: queryResults.itemCount,
firstItemIndex: queryResults.firstItemIndex,
lastItemIndex: queryResults.lastItemIndex
};
this.allResultsMetadata.push(resultsMetadata);
this.activityId(queryResults.activityId);
this.roundTrips(queryResults.roundTrips);
this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]); if (queryResults.itemCount == 0 && metadata != null && metadata.itemCount >= 0) {
// we let users query for the next page because the SDK sometimes specifies there are more elements
// even though there aren't any so we should not update the prior query results.
return;
}
if (queryResults.itemCount == 0 && metadata != null && metadata.itemCount >= 0) { const documents: any[] = queryResults.documents;
// we let users query for the next page because the SDK sometimes specifies there are more elements const results = this.renderObjectForEditor(documents, null, 4);
// even though there aren't any so we should not update the prior query results.
return;
}
const documents: any[] = queryResults.documents; const resultsDisplay: string =
const results = this.renderObjectForEditor(documents, null, 4); queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`;
this.showingDocumentsDisplayText(resultsDisplay);
this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`);
const resultsDisplay: string = if (!this.queryResults() && !results) {
queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`; const errorMessage: string = JSON.stringify({
this.showingDocumentsDisplayText(resultsDisplay); error: `Returned no results after query execution`,
this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`); accountName: this.collection && this.collection.container.databaseAccount(),
databaseName: this.collection && this.collection.databaseId,
collectionName: this.collection && this.collection.id(),
sqlQuery: this.sqlStatementToExecute(),
hasMoreResults: resultsMetadata.hasMoreResults,
itemCount: resultsMetadata.itemCount,
responseHeaders: queryResults && queryResults.headers
});
Logger.logError(errorMessage, "QueryTab");
}
if (!this.queryResults() && !results) { this.queryResults(results);
const errorMessage: string = JSON.stringify({
error: `Returned no results after query execution`,
accountName: this.collection && this.collection.container.databaseAccount(),
databaseName: this.collection && this.collection.databaseId,
collectionName: this.collection && this.collection.id(),
sqlQuery: this.sqlStatementToExecute(),
hasMoreResults: resultsMetadata.hasMoreResults,
itemCount: resultsMetadata.itemCount,
responseHeaders: queryResults && queryResults.headers
});
Logger.logError(errorMessage, "QueryTab");
}
this.queryResults(results); TelemetryProcessor.traceSuccess(
Action.ExecuteQuery,
TelemetryProcessor.traceSuccess( {
Action.ExecuteQuery, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
{ defaultExperience: this.collection && this.collection.container.defaultExperience(),
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, dataExplorerArea: Constants.Areas.Tab,
defaultExperience: this.collection && this.collection.container.defaultExperience(), tabTitle: this.tabTitle()
dataExplorerArea: Constants.Areas.Tab, },
tabTitle: this.tabTitle() startKey
);
}, },
startKey (error: any) => {
); this.isExecutionError(true);
} catch (error) { const errorMessage = getErrorMessage(error);
this.isExecutionError(true); this.error(errorMessage);
const errorMessage = getErrorMessage(error); TelemetryProcessor.traceFailure(
this.error(errorMessage); Action.ExecuteQuery,
TelemetryProcessor.traceFailure( {
Action.ExecuteQuery, databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
{ defaultExperience: this.collection && this.collection.container.defaultExperience(),
databaseAccountName: this.collection && this.collection.container.databaseAccount().name, dataExplorerArea: Constants.Areas.Tab,
defaultExperience: this.collection && this.collection.container.defaultExperience(), tabTitle: this.tabTitle(),
dataExplorerArea: Constants.Areas.Tab, error: errorMessage,
tabTitle: this.tabTitle(), errorStack: getErrorStack(error)
error: errorMessage, },
errorStack: getErrorStack(error) startKey
}, );
startKey document.getElementById("error-display").focus();
); }
document.getElementById("error-display").focus(); )
} finally { .finally(() => {
this.isExecuting(false); this.isExecuting(false);
this.togglesOnFocus(); this.togglesOnFocus();
} });
} }
private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void { private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void {
@@ -476,17 +477,16 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
} }
} }
protected _initIterator(): void { protected _initIterator(): Q.Promise<MinimalQueryIterator> {
const options: any = QueryTab.getIteratorOptions(this.collection); const options: any = QueryTab.getIteratorOptions(this.collection);
if (this._resourceTokenPartitionKey) { if (this._resourceTokenPartitionKey) {
options.partitionKey = this._resourceTokenPartitionKey; options.partitionKey = this._resourceTokenPartitionKey;
} }
this._iterator = queryDocuments( return Q(
this.collection.databaseId, queryDocuments(this.collection.databaseId, this.collection.id(), this.sqlStatementToExecute(), options).then(
this.collection.id(), iterator => (this._iterator = iterator)
this.sqlStatementToExecute(), )
options
); );
} }

View File

@@ -161,16 +161,17 @@ export default class QueryTablesTab extends TabsBase {
return null; return null;
}; };
public onActivate(): void { public onActivate(): Q.Promise<any> {
super.onActivate(); return super.onActivate().then(() => {
const columns = const columns =
!!this.tableEntityListViewModel() && !!this.tableEntityListViewModel() &&
!!this.tableEntityListViewModel().table && !!this.tableEntityListViewModel().table &&
this.tableEntityListViewModel().table.columns; this.tableEntityListViewModel().table.columns;
if (!!columns) { if (!!columns) {
columns.adjust(); columns.adjust();
$(window).resize(); $(window).resize();
} }
});
} }
protected getTabsButtons(): CommandButtonComponentProps[] { protected getTabsButtons(): CommandButtonComponentProps[] {

View File

@@ -186,11 +186,12 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
this._setBaselines(); this._setBaselines();
} }
public onTabClick(): void { public onTabClick(): Q.Promise<any> {
super.onTabClick(); return super.onTabClick().then(() => {
if (this.isNew()) { if (this.isNew()) {
this.collection.selectedSubnodeKind(this.tabKind); this.collection.selectedSubnodeKind(this.tabKind);
} }
});
} }
public abstract onSaveClick: () => Promise<any>; public abstract onSaveClick: () => Promise<any>;

View File

@@ -42,49 +42,54 @@ export default class SettingsTabV2 extends TabsBase {
}); });
} }
public async onActivate(): Promise<void> { public onActivate(): Q.Promise<unknown> {
try { this.isExecuting(true);
this.isExecuting(true); this.currentCollection.loadOffer().then(
await this.currentCollection.loadOffer(); () => {
// passed in options and set by parent as "Settings" by default // passed in options and set by parent as "Settings" by default
this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings"); this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings");
this.offerRead(true);
this.options.getPendingNotification.then(
(data: DataModels.Notification) => {
this.notification = data;
this.notificationRead(true);
this.isExecuting(false);
},
error => {
const errorMessage = getErrorMessage(error);
this.notification = undefined;
this.notificationRead(true);
this.isExecuting(false);
traceFailure(
Action.Tab,
{
databaseAccountName: this.options.collection.container.databaseAccount().name,
databaseName: this.options.collection.databaseId,
collectionName: this.options.collection.id(),
defaultExperience: this.options.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle,
error: errorMessage,
errorStack: getErrorStack(error)
},
this.options.onLoadStartKey
);
logConsoleError(
`Error while fetching container settings for container ${this.options.collection.id()}: ${errorMessage}`
);
throw error;
}
);
},
() => {
this.offerRead(true);
this.isExecuting(false);
}
);
this.options.getPendingNotification.then( return super.onActivate().then(() => {
(data: DataModels.Notification) => { this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.SettingsV2);
this.notification = data; });
this.notificationRead(true);
},
error => {
const errorMessage = getErrorMessage(error);
this.notification = undefined;
this.notificationRead(true);
traceFailure(
Action.Tab,
{
databaseAccountName: this.options.collection.container.databaseAccount().name,
databaseName: this.options.collection.databaseId,
collectionName: this.options.collection.id(),
defaultExperience: this.options.collection.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: this.tabTitle,
error: errorMessage,
errorStack: getErrorStack(error)
},
this.options.onLoadStartKey
);
logConsoleError(
`Error while fetching container settings for container ${this.options.collection.id()}: ${errorMessage}`
);
throw error;
}
);
} finally {
this.offerRead(true);
this.isExecuting(false);
}
super.onActivate();
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.SettingsV2);
} }
public getSettingsTabContainer(): Explorer { public getSettingsTabContainer(): Explorer {

View File

@@ -94,8 +94,9 @@ export default class TabsBase extends WaitsForTemplateViewModel {
}); });
} }
public onTabClick(): void { public onTabClick(): Q.Promise<any> {
this.getContainer().tabsManager.activateTab(this); this.getContainer().tabsManager.activateTab(this);
return Q();
} }
protected updateSelectedNode(): void { protected updateSelectedNode(): void {
@@ -127,7 +128,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
return this.onSpaceOrEnterKeyPress(event, () => this.onCloseTabButtonClick()); return this.onSpaceOrEnterKeyPress(event, () => this.onCloseTabButtonClick());
}; };
public onActivate(): void { public onActivate(): Q.Promise<any> {
this.updateSelectedNode(); this.updateSelectedNode();
if (!!this.collection) { if (!!this.collection) {
this.collection.selectedSubnodeKind(this.tabKind); this.collection.selectedSubnodeKind(this.tabKind);
@@ -150,6 +151,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
tabTitle: this.tabTitle(), tabTitle: this.tabTitle(),
tabId: this.tabId tabId: this.tabId
}); });
return Q();
} }
public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => { public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => {

View File

@@ -10,9 +10,10 @@ describe("Collection", () => {
container: Explorer, container: Explorer,
databaseId: string, databaseId: string,
data: DataModels.Collection, data: DataModels.Collection,
quotaInfo: DataModels.CollectionQuotaInfo,
offer: DataModels.Offer offer: DataModels.Offer
): Collection { ): Collection {
return new Collection(container, databaseId, data); return new Collection(container, databaseId, data, quotaInfo, offer);
} }
function generateMockCollectionsDataModelWithPartitionKey( function generateMockCollectionsDataModelWithPartitionKey(
@@ -49,7 +50,7 @@ describe("Collection", () => {
}); });
mockContainer.deleteCollectionText = ko.observable<string>("delete collection"); mockContainer.deleteCollectionText = ko.observable<string>("delete collection");
return generateCollection(mockContainer, "abc", data, {} as DataModels.Offer); return generateCollection(mockContainer, "abc", data, {} as DataModels.CollectionQuotaInfo, {} as DataModels.Offer);
} }
describe("Partition key path parsing", () => { describe("Partition key path parsing", () => {

View File

@@ -8,8 +8,9 @@ import * as Constants from "../../Common/Constants";
import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures"; import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures";
import { readTriggers } from "../../Common/dataAccess/readTriggers"; import { readTriggers } from "../../Common/dataAccess/readTriggers";
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions"; import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
import { createDocument } from "../../Common/DocumentClientUtilityBase";
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer"; import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize"; import { readCollectionQuotaInfo } from "../../Common/dataAccess/readCollectionQuotaInfo";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
@@ -36,9 +37,9 @@ import UserDefinedFunction from "./UserDefinedFunction";
import { configContext, Platform } from "../../ConfigContext"; import { configContext, Platform } from "../../ConfigContext";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import TabsBase from "../Tabs/TabsBase";
import { fetchPortalNotifications } from "../../Common/PortalNotifications"; import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { createDocument } from "../../Common/dataAccess/createDocument";
export default class Collection implements ViewModels.Collection { export default class Collection implements ViewModels.Collection {
public nodeKind: string; public nodeKind: string;
@@ -53,8 +54,7 @@ export default class Collection implements ViewModels.Collection {
public defaultTtl: ko.Observable<number>; public defaultTtl: ko.Observable<number>;
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>; public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy; public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
public usageSizeInKB: ko.Observable<number>; public quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
public offer: ko.Observable<DataModels.Offer>; public offer: ko.Observable<DataModels.Offer>;
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>; public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>; public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
@@ -95,7 +95,13 @@ export default class Collection implements ViewModels.Collection {
public userDefinedFunctionsFocused: ko.Observable<boolean>; public userDefinedFunctionsFocused: ko.Observable<boolean>;
public triggersFocused: ko.Observable<boolean>; public triggersFocused: ko.Observable<boolean>;
constructor(container: Explorer, databaseId: string, data: DataModels.Collection) { constructor(
container: Explorer,
databaseId: string,
data: DataModels.Collection,
quotaInfo: DataModels.CollectionQuotaInfo,
offer: DataModels.Offer
) {
this.nodeKind = "Collection"; this.nodeKind = "Collection";
this.container = container; this.container = container;
this.self = data._self; this.self = data._self;
@@ -107,8 +113,8 @@ export default class Collection implements ViewModels.Collection {
this.id = ko.observable(data.id); this.id = ko.observable(data.id);
this.defaultTtl = ko.observable(data.defaultTtl); this.defaultTtl = ko.observable(data.defaultTtl);
this.indexingPolicy = ko.observable(data.indexingPolicy); this.indexingPolicy = ko.observable(data.indexingPolicy);
this.usageSizeInKB = ko.observable(); this.quotaInfo = ko.observable(quotaInfo);
this.offer = ko.observable(); this.offer = ko.observable(offer);
this.conflictResolutionPolicy = ko.observable(data.conflictResolutionPolicy); this.conflictResolutionPolicy = ko.observable(data.conflictResolutionPolicy);
this.changeFeedPolicy = ko.observable<DataModels.ChangeFeedPolicy>(data.changeFeedPolicy); this.changeFeedPolicy = ko.observable<DataModels.ChangeFeedPolicy>(data.changeFeedPolicy);
this.analyticalStorageTtl = ko.observable(data.analyticalStorageTtl); this.analyticalStorageTtl = ko.observable(data.analyticalStorageTtl);
@@ -601,6 +607,14 @@ export default class Collection implements ViewModels.Collection {
} }
}; };
private async loadCollectionQuotaInfo(): Promise<void> {
// TODO: Use the collection entity cache to get quota info
const quotaInfoWithUniqueKeyPolicy = await readCollectionQuotaInfo(this);
this.uniqueKeyPolicy = quotaInfoWithUniqueKeyPolicy.uniqueKeyPolicy;
const quotaInfo = _.omit(quotaInfoWithUniqueKeyPolicy, "uniqueKeyPolicy");
this.quotaInfo(quotaInfo);
}
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) { public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
const collection: ViewModels.Collection = source.collection || source; const collection: ViewModels.Collection = source.collection || source;
const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1; const id = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Query).length + 1;
@@ -1091,7 +1105,8 @@ export default class Collection implements ViewModels.Collection {
return deferred.promise; return deferred.promise;
} }
private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> { private _createDocumentsFromFile(fileName: string, documentContent: string): Q.Promise<UploadDetailsRecord> {
const deferred: Q.Deferred<UploadDetailsRecord> = Q.defer();
const record: UploadDetailsRecord = { const record: UploadDetailsRecord = {
fileName: fileName, fileName: fileName,
numSucceeded: 0, numSucceeded: 0,
@@ -1101,25 +1116,39 @@ export default class Collection implements ViewModels.Collection {
try { try {
const content = JSON.parse(documentContent); const content = JSON.parse(documentContent);
const promises: Array<Q.Promise<any>> = [];
const triggerCreateDocument: (documentContent: any) => Q.Promise<any> = (documentContent: any) => {
return createDocument(this, documentContent).then(
doc => {
record.numSucceeded++;
return Q.resolve();
},
error => {
record.numFailed++;
record.errors = [...record.errors, getErrorMessage(error)];
return Q.resolve();
}
);
};
if (Array.isArray(content)) { if (Array.isArray(content)) {
await Promise.all( for (let i = 0; i < content.length; i++) {
content.map(async documentContent => { promises.push(triggerCreateDocument(content[i]));
await createDocument(this, documentContent); }
record.numSucceeded++;
})
);
} else { } else {
await createDocument(this, documentContent); promises.push(triggerCreateDocument(content));
record.numSucceeded++;
} }
return record; Q.all(promises).then(() => {
} catch (error) { deferred.resolve(record);
});
} catch (e) {
record.numFailed++; record.numFailed++;
record.errors = [...record.errors, error.message]; record.errors = [...record.errors, e.message];
return record; deferred.resolve(record);
} }
return deferred.promise;
} }
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> { private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
@@ -1258,7 +1287,7 @@ export default class Collection implements ViewModels.Collection {
try { try {
this.offer(await readCollectionOffer(params)); this.offer(await readCollectionOffer(params));
this.usageSizeInKB(await getCollectionUsageSizeInKB(this.databaseId, this.id())); await this.loadCollectionQuotaInfo();
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadOffers, Action.LoadOffers,

View File

@@ -6,7 +6,7 @@ import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { extractPartitionKey } from "@azure/cosmos"; import { extractPartitionKey } from "@azure/cosmos";
import ConflictsTab from "../Tabs/ConflictsTab"; import ConflictsTab from "../Tabs/ConflictsTab";
import { readDocument } from "../../Common/dataAccess/readDocument"; import { readDocument } from "../../Common/DocumentClientUtilityBase";
export default class ConflictId { export default class ConflictId {
public container: ConflictsTab; public container: ConflictsTab;
@@ -59,42 +59,41 @@ export default class ConflictId {
return; return;
} }
public async loadConflict(): Promise<void> { public loadConflict(): Q.Promise<any> {
const conflictsTab = this.container;
this.container.selectedConflictId(this); this.container.selectedConflictId(this);
if (this.operationType === Constants.ConflictOperationType.Create) { if (this.operationType === Constants.ConflictOperationType.Create) {
this.container.initDocumentEditorForCreate(this, this.content); this.container.initDocumentEditorForCreate(this, this.content);
return; return Q();
} }
this.container.loadingConflictData(true); this.container.loadingConflictData(true);
return readDocument(this.container.collection, this.buildDocumentIdFromConflict(this.partitionKeyValue)).then(
(currentDocumentContent: any) => {
this.container.loadingConflictData(false);
if (this.operationType === Constants.ConflictOperationType.Replace) {
this.container.initDocumentEditorForReplace(this, this.content, currentDocumentContent);
} else {
this.container.initDocumentEditorForDelete(this, currentDocumentContent);
}
},
(reason: any) => {
this.container.loadingConflictData(false);
try { // Document could be deleted
const currentDocumentContent = await readDocument( if (
this.container.collection, reason &&
this.buildDocumentIdFromConflict(this.partitionKeyValue) reason.code === Constants.HttpStatusCodes.NotFound &&
); this.operationType === Constants.ConflictOperationType.Delete
) {
this.container.initDocumentEditorForNoOp(this);
return Q();
}
if (this.operationType === Constants.ConflictOperationType.Replace) { return Q.reject(reason);
this.container.initDocumentEditorForReplace(this, this.content, currentDocumentContent);
} else {
this.container.initDocumentEditorForDelete(this, currentDocumentContent);
} }
} catch (error) { );
// Document could be deleted
if (
error &&
error.code === Constants.HttpStatusCodes.NotFound &&
this.operationType === Constants.ConflictOperationType.Delete
) {
this.container.initDocumentEditorForNoOp(this);
return;
}
throw error;
} finally {
this.container.loadingConflictData(false);
}
} }
public getPartitionKeyValueAsString(): string { public getPartitionKeyValueAsString(): string {

View File

@@ -193,7 +193,7 @@ export default class Database implements ViewModels.Database {
}); });
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => { deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
const collectionVM: Collection = new Collection(this.container, this.id(), collection); const collectionVM: Collection = new Collection(this.container, this.id(), collection, null, null);
collectionVMs.push(collectionVM); collectionVMs.push(collectionVM);
}); });

View File

@@ -65,7 +65,7 @@ export default class DocumentId {
return JSON.stringify(partitionKeyValue); return JSON.stringify(partitionKeyValue);
} }
public async loadDocument(): Promise<void> { public loadDocument(): Q.Promise<any> {
await this.container.selectDocument(this); return this.container.selectDocument(this);
} }
} }

View File

@@ -229,7 +229,9 @@ const createMockCollection = (): ViewModels.Collection => {
const mockCollectionVM: ViewModels.Collection = new Collection( const mockCollectionVM: ViewModels.Collection = new Collection(
createMockContainer(), createMockContainer(),
"fakeDatabaseId", "fakeDatabaseId",
mockCollection mockCollection,
undefined,
undefined
); );
return mockCollectionVM; return mockCollectionVM;

View File

@@ -2,7 +2,7 @@ import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
import * as ko from "knockout"; import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { deleteStoredProcedure } from "../../Common/dataAccess/deleteStoredProcedure"; import { deleteStoredProcedure } from "../../Common/dataAccess/deleteStoredProcedure";
import { executeStoredProcedure } from "../../Common/dataAccess/executeStoredProcedure"; import { executeStoredProcedure } from "../../Common/DocumentClientUtilityBase";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";

View File

@@ -178,7 +178,8 @@ export class JunoClient {
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`); return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`);
} }
public async getPublicGalleryData(): Promise<IJunoResponse<IPublicGalleryData>> { // will be renamed once feature.enableCodeOfConduct flag is removed
public async fetchPublicNotebooks(): Promise<IJunoResponse<IPublicGalleryData>> {
const url = `${this.getNotebooksAccountUrl()}/gallery/public`; const url = `${this.getNotebooksAccountUrl()}/gallery/public`;
const response = await window.fetch(url, { const response = await window.fetch(url, {
method: "PATCH", method: "PATCH",
@@ -404,7 +405,7 @@ export class JunoClient {
} }
public async reportAbuse(notebookId: string, abuseCategory: string, notes: string): Promise<IJunoResponse<boolean>> { public async reportAbuse(notebookId: string, abuseCategory: string, notes: string): Promise<IJunoResponse<boolean>> {
const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/reportAbuse`, { const response = await window.fetch(`${this.getNotebooksUrl()}/avert/reportAbuse`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
notebookId, notebookId,

View File

@@ -12,8 +12,8 @@ import { getErrorMessage } from "../Common/ErrorHandlingUtils";
export class NotebookWorkspaceManager { export class NotebookWorkspaceManager {
private resourceProviderClientFactory: IResourceProviderClientFactory<any>; private resourceProviderClientFactory: IResourceProviderClientFactory<any>;
constructor() { constructor(private _armEndpoint: string) {
this.resourceProviderClientFactory = new ResourceProviderClientFactory(); this.resourceProviderClientFactory = new ResourceProviderClientFactory(this._armEndpoint);
} }
public async getNotebookWorkspacesAsync(cosmosdbResourceId: string): Promise<NotebookWorkspace[]> { public async getNotebookWorkspacesAsync(cosmosdbResourceId: string): Promise<NotebookWorkspace[]> {

View File

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

View File

@@ -268,7 +268,7 @@ export default class Main {
masterKey?: string /* master key extracted from connection string if available */, masterKey?: string /* master key extracted from connection string if available */,
account?: DatabaseAccount, account?: DatabaseAccount,
authorizationToken?: string /* access key */ authorizationToken?: string /* access key */
): void { ): Q.Promise<void> {
const serverId: string = AuthHeadersUtil.serverId; const serverId: string = AuthHeadersUtil.serverId;
const authType: string = (<any>window).authType; const authType: string = (<any>window).authType;
const accountResourceId = const accountResourceId =
@@ -373,7 +373,7 @@ export default class Main {
}); });
} }
throw new Error(`Unsupported AuthType ${authType}`); return Q.reject(`Unsupported AuthType ${authType}`);
} }
private static _instantiateExplorer(): Explorer { private static _instantiateExplorer(): Explorer {

View File

@@ -1,23 +1,9 @@
import "../../Explorer/Tables/DataTable/DataTableBindingManager"; import "../../Explorer/Tables/DataTable/DataTableBindingManager";
import Explorer from "../../Explorer/Explorer"; import Explorer from "../../Explorer/Explorer";
import { handleMessage } from "../../Controls/Heatmap/Heatmap";
export function initializeExplorer(): Explorer { export function initializeExplorer(): Explorer {
const explorer = new Explorer(); const explorer = new Explorer();
// In development mode, try to load the iframe message from session storage.
// This allows webpack hot reload to funciton properly
if (process.env.NODE_ENV === "development") {
const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage");
if (initMessage) {
const message = JSON.parse(initMessage);
console.warn("Loaded cached portal iframe message from session storage");
console.dir(message);
explorer.initDataExplorerWithFrameInputs(message);
}
}
window.addEventListener("message", explorer.handleMessage.bind(explorer), false); window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
return explorer; return explorer;
} }

View File

@@ -1,14 +1,10 @@
import { configContext } from "../ConfigContext";
import { IResourceProviderClientFactory, IResourceProviderClient } from "./IResourceProviderClient"; import { IResourceProviderClientFactory, IResourceProviderClient } from "./IResourceProviderClient";
import { ResourceProviderClient } from "./ResourceProviderClient"; import { ResourceProviderClient } from "./ResourceProviderClient";
export class ResourceProviderClientFactory implements IResourceProviderClientFactory<any> { export class ResourceProviderClientFactory implements IResourceProviderClientFactory<any> {
private armEndpoint: string;
private cachedClients: { [url: string]: IResourceProviderClient<any> } = {}; private cachedClients: { [url: string]: IResourceProviderClient<any> } = {};
constructor() { constructor(private armEndpoint: string) {}
this.armEndpoint = configContext.ARM_ENDPOINT;
}
public getOrCreate(url: string): IResourceProviderClient<any> { public getOrCreate(url: string): IResourceProviderClient<any> {
if (!url) { if (!url) {

View File

@@ -126,6 +126,7 @@ export class OfferPricing {
Standard: { Standard: {
StartingPrice: 24 / hoursInAMonth, // per hour StartingPrice: 24 / hoursInAMonth, // per hour
PricePerRU: 0.00008, PricePerRU: 0.00008,
PricePerRUPM: (10 * 2) / 1000 / hoursInAMonth, // preview price: $2 per 1000 RU/m per month -> 100 RU/s
PricePerGB: 0.25 / hoursInAMonth PricePerGB: 0.25 / hoursInAMonth
} }
}, },
@@ -138,18 +139,24 @@ export class OfferPricing {
Standard: { Standard: {
StartingPrice: OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice / hoursInAMonth, // per hour StartingPrice: OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice / hoursInAMonth, // per hour
PricePerRU: 0.00051, PricePerRU: 0.00051,
PricePerRUPM: (10 * 20) / 1000 / hoursInAMonth, // preview price: 20rmb per 1000 RU/m per month -> 100 RU/s
PricePerGB: OfferPricing.MonthlyPricing.mooncake.Standard.PricePerGB / hoursInAMonth PricePerGB: OfferPricing.MonthlyPricing.mooncake.Standard.PricePerGB / hoursInAMonth
} }
} }
}; };
} }
export class GeneralResources {
public static loadingText: string = "Loading...";
}
export class CollectionCreation { export class CollectionCreation {
// TODO generate these values based on Product\Services\Documents\ImageStore\GatewayApplication\Settings.xml // TODO generate these values based on Product\Services\Documents\ImageStore\GatewayApplication\Settings.xml
public static readonly MinRUPerPartitionBelow7Partitions: number = 400; public static readonly MinRUPerPartitionBelow7Partitions: number = 400;
public static readonly MinRU7PartitionsTo25Partitions: number = 2500; public static readonly MinRU7PartitionsTo25Partitions: number = 2500;
public static readonly MinRUPerPartitionAbove25Partitions: number = 100; public static readonly MinRUPerPartitionAbove25Partitions: number = 100;
public static readonly MaxRUPerPartition: number = 10000; public static readonly MaxRUPerPartition: number = 10000;
public static readonly MaxRUPMPerPartition: number = 5000;
public static readonly MinPartitionedCollectionRUs: number = 2500; public static readonly MinPartitionedCollectionRUs: number = 2500;
public static readonly NumberOfPartitionsInFixedCollection: number = 1; public static readonly NumberOfPartitionsInFixedCollection: number = 1;
@@ -224,6 +231,32 @@ export class IndexingPolicies {
} }
export class SubscriptionUtilMappings { export class SubscriptionUtilMappings {
// TODO: Expose this through a web API from the portal
public static SubscriptionTypeMap: { [key: string]: SubscriptionType } = {
"AAD_2015-09-01": SubscriptionType.Free,
"AzureDynamics_2014-09-01": SubscriptionType.Free,
"AzureInOpen_2014-09-01": SubscriptionType.EA,
"AzurePass_2014-09-01": SubscriptionType.Free,
"BackupStorage_2014-09-01": SubscriptionType.PAYG,
"BizSpark_2014-09-01": SubscriptionType.Benefits,
"BizSparkPlus_2014-09-01": SubscriptionType.Benefits,
"CSP_2015-05-01": SubscriptionType.EA,
"Default_2014-09-01": SubscriptionType.PAYG,
"DevEssentials_2016-01-01": SubscriptionType.Benefits,
"DreamSpark_2015-02-01": SubscriptionType.Benefits,
"EnterpriseAgreement_2014-09-01": SubscriptionType.EA,
"FreeTrial_2014-09-01": SubscriptionType.Free,
"Internal_2014-09-01": SubscriptionType.Internal,
"LegacyMonetaryCommitment_2014-09-01": SubscriptionType.EA,
"LightweightTrial_2016-09-01": SubscriptionType.Free,
"MonetaryCommitment_2015-05-01": SubscriptionType.EA,
"MPN_2014-09-01": SubscriptionType.Benefits,
"MSDN_2014-09-01": SubscriptionType.Benefits,
"MSDNDevTest_2014-09-01": SubscriptionType.Benefits,
"PayAsYouGo_2014-09-01": SubscriptionType.PAYG,
"Sponsored_2016-01-01": SubscriptionType.Benefits
};
public static FreeTierSubscriptionIds: string[] = [ public static FreeTierSubscriptionIds: string[] = [
"b8f2ff04-0a81-4cf9-95ef-5828d16981d2", "b8f2ff04-0a81-4cf9-95ef-5828d16981d2",
"39b1fdff-e5b2-4f83-adb4-33cb3aabf5ea", "39b1fdff-e5b2-4f83-adb4-33cb3aabf5ea",
@@ -234,6 +267,57 @@ export class SubscriptionUtilMappings {
]; ];
} }
export class Offers {
public static offerTypeS1: string = "S1";
public static offerTypeS2: string = "S2";
public static offerTypeS3: string = "S3";
public static offerTypeStandard: string = "Standard";
}
export class OfferThoughput {
public static offerS1Throughput: number = 250;
public static offerS2Throughput: number = 1000;
public static offerS3Throughput: number = 2500;
}
export class OfferVersions {
public static offerV1: string = "V1";
public static offerV2: string = "V2";
}
export class InvalidOffers {
public static offerTypeInvalid: string = "Invalid";
public static offerTypeError: string = "Loading Error";
}
export class SpecTypes {
public static collection: string = "DocumentDbCollection";
}
export class CurrencyCodes {
public static usd: string = "USD";
public static rmb: string = "RMB";
}
export class ColorSchemes {
public static standard: string = "mediumBlue";
public static legacy: string = "yellowGreen";
}
export class FeatureIds {
public static storage: string = "storage";
public static sla: string = "sla";
public static partitioned: string = "partitioned";
public static singlePartitioned: string = "singlePartition";
public static legacySinglePartitioned: string = "legacySinglePartition";
}
export class FeatureIconNames {
public static storage: string = "SSD";
public static sla: string = "Monitoring";
public static productionReady: string = "ProductionReadyDb";
}
export class AutopilotDocumentation { export class AutopilotDocumentation {
public static Url: string = "https://aka.ms/cosmos-autoscale-info"; public static Url: string = "https://aka.ms/cosmos-autoscale-info";
} }

View File

@@ -1,13 +1,17 @@
import * as Constants from "./Constants"; import * as Constants from "./Constants";
export function computeRUUsagePrice(serverId: string, requestUnits: number): string { export function computeRUUsagePrice(serverId: string, rupmEnabled: boolean, requestUnits: number): string {
if (serverId === "mooncake") { if (serverId === "mooncake") {
let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU; let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU,
return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency; rupmCharge = rupmEnabled ? requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRUPM : 0;
return (
calculateEstimateNumber(ruCharge + rupmCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency
);
} }
let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU; let ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU,
return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency; rupmCharge = rupmEnabled ? requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRUPM : 0;
return calculateEstimateNumber(ruCharge + rupmCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency;
} }
export function computeStorageUsagePrice(serverId: string, storageUsedRoundUpToGB: number): string { export function computeStorageUsagePrice(serverId: string, storageUsedRoundUpToGB: number): string {

View File

@@ -8,13 +8,14 @@ import { ArmApiVersions, ArmResourceTypes } from "../Common/Constants";
import { IResourceProviderClient, IResourceProviderClientFactory } from "../ResourceProvider/IResourceProviderClient"; import { IResourceProviderClient, IResourceProviderClientFactory } from "../ResourceProvider/IResourceProviderClient";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory";
import { configContext } from "../ConfigContext";
import { getErrorMessage } from "../Common/ErrorHandlingUtils"; import { getErrorMessage } from "../Common/ErrorHandlingUtils";
export class ArcadiaResourceManager { export class ArcadiaResourceManager {
private resourceProviderClientFactory: IResourceProviderClientFactory<any>; private resourceProviderClientFactory: IResourceProviderClientFactory<any>;
constructor() { constructor(private armEndpoint = configContext.ARM_ENDPOINT) {
this.resourceProviderClientFactory = new ResourceProviderClientFactory(); this.resourceProviderClientFactory = new ResourceProviderClientFactory(this.armEndpoint);
} }
public async getWorkspacesAsync(arcadiaResourceId: string): Promise<ArcadiaWorkspace[]> { public async getWorkspacesAsync(arcadiaResourceId: string): Promise<ArcadiaWorkspace[]> {

View File

@@ -23,10 +23,14 @@ const createServerSettings = (urlVars: { [key: string]: string }): ServerConnect
let headers: HeadersInit; let headers: HeadersInit;
if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) { if (urlVars.hasOwnProperty(TerminalQueryParams.TerminalEndpoint)) {
body = JSON.stringify({ body = JSON.stringify({
endpoint: urlVars[TerminalQueryParams.TerminalEndpoint] endpoint: urlVars[TerminalQueryParams.TerminalEndpoint],
account:"contoso-retail-mongodb",
port: "10255",
token:"" //tofill
}); });
headers = { headers = {
[HttpHeaders.contentType]: "application/json" [HttpHeaders.contentType]: "application/json"
//"Access-Control-Allow-Origin": "https://localhost:5001"
}; };
} }
@@ -59,8 +63,9 @@ const main = async (): Promise<void> => {
const serverSettings = createServerSettings(urlVars); const serverSettings = createServerSettings(urlVars);
const data = { baseUrl: serverSettings.baseUrl }; const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, {
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data); baseUrl: serverSettings.baseUrl
});
try { try {
if (urlVars.hasOwnProperty(TerminalQueryParams.Terminal)) { if (urlVars.hasOwnProperty(TerminalQueryParams.Terminal)) {
@@ -69,9 +74,9 @@ const main = async (): Promise<void> => {
throw new Error("Only terminal is supported"); throw new Error("Only terminal is supported");
} }
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime); TelemetryProcessor.traceSuccess(Action.OpenTerminal, startTime);
} catch (error) { } catch (error) {
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime); TelemetryProcessor.traceFailure(Action.OpenTerminal, startTime);
} }
}; };

View File

@@ -14,7 +14,6 @@ interface UserContext {
defaultExperience?: DefaultAccountExperienceType; defaultExperience?: DefaultAccountExperienceType;
useSDKOperations?: boolean; useSDKOperations?: boolean;
subscriptionType?: SubscriptionType; subscriptionType?: SubscriptionType;
quotaId?: string;
} }
const userContext: Readonly<UserContext> = {} as const; const userContext: Readonly<UserContext> = {} as const;

View File

@@ -10,7 +10,6 @@ import Explorer from "../Explorer/Explorer";
import { IChoiceGroupOption, IChoiceGroupProps } from "office-ui-fabric-react"; import { IChoiceGroupOption, IChoiceGroupProps } from "office-ui-fabric-react";
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent"; import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
import { handleError } from "../Common/ErrorHandlingUtils"; import { handleError } from "../Common/ErrorHandlingUtils";
import { HttpStatusCodes } from "../Common/Constants";
const defaultSelectedAbuseCategory = "Other"; const defaultSelectedAbuseCategory = "Other";
const abuseCategories: IChoiceGroupOption[] = [ const abuseCategories: IChoiceGroupOption[] = [
@@ -114,7 +113,7 @@ export function reportAbuse(
try { try {
const response = await junoClient.reportAbuse(notebookId, abuseCategory, additionalDetails); const response = await junoClient.reportAbuse(notebookId, abuseCategory, additionalDetails);
if (response.status !== HttpStatusCodes.Accepted) { if (!response.data) {
throw new Error(`Received HTTP ${response.status} when submitting report for ${data.name}`); throw new Error(`Received HTTP ${response.status} when submitting report for ${data.name}`);
} }

View File

@@ -25,37 +25,37 @@ describe("PricingUtils Tests", () => {
describe("computeRUUsagePriceHourly()", () => { describe("computeRUUsagePriceHourly()", () => {
it("should return 0 for NaN regions default cloud", () => { it("should return 0 for NaN regions default cloud", () => {
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, null, false); const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, null, false);
expect(value).toBe(0); expect(value).toBe(0);
}); });
it("should return 0 for -1 regions", () => { it("should return 0 for -1 regions", () => {
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, -1, false); const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, -1, false);
expect(value).toBe(0); expect(value).toBe(0);
}); });
it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster disabled", () => { it("should return 0.00008 for default cloud, rupm disabled, 1RU, 1 region, multimaster disabled", () => {
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 1, false); const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, 1, false);
expect(value).toBe(0.00008); expect(value).toBe(0.00008);
}); });
it("should return 0.00051 for Mooncake cloud, 1RU, 1 region, multimaster disabled", () => { it("should return 0.00051 for Mooncake cloud, rupm disabled, 1RU, 1 region, multimaster disabled", () => {
const value = PricingUtils.computeRUUsagePriceHourly("mooncake", 1, 1, false); const value = PricingUtils.computeRUUsagePriceHourly("mooncake", false, 1, 1, false);
expect(value).toBe(0.00051); expect(value).toBe(0.00051);
}); });
it("should return 0.00016 for default cloud, 1RU, 2 regions, multimaster disabled", () => { it("should return 0.00016 for default cloud, rupm disabled, 1RU, 2 regions, multimaster disabled", () => {
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 2, false); const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, 2, false);
expect(value).toBe(0.00016); expect(value).toBe(0.00016);
}); });
it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster enabled", () => { it("should return 0.00008 for default cloud, rupm disabled, 1RU, 1 region, multimaster enabled", () => {
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 1, true); const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, 1, true);
expect(value).toBe(0.00008); expect(value).toBe(0.00008);
}); });
it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled", () => { it("should return 0.00048 for default cloud, rupm disabled, 1RU, 2 region, multimaster enabled", () => {
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 2, true); const value = PricingUtils.computeRUUsagePriceHourly("default", false, 1, 2, true);
expect(value).toBe(0.00048); expect(value).toBe(0.00048);
}); });
}); });
@@ -150,6 +150,18 @@ describe("PricingUtils Tests", () => {
}); });
}); });
describe("getPricePerRuPm()", () => {
it("should return 0.000027397260273972603 for default clouds", () => {
const value = PricingUtils.getPricePerRuPm("default");
expect(value).toBe(0.000027397260273972603);
});
it("should return 0.00027397260273972606 for mooncake", () => {
const value = PricingUtils.getPricePerRuPm("mooncake");
expect(value).toBe(0.00027397260273972606);
});
});
describe("getRegionMultiplier()", () => { describe("getRegionMultiplier()", () => {
describe("without multimaster", () => { describe("without multimaster", () => {
it("should return 0 for null", () => { it("should return 0 for null", () => {
@@ -242,95 +254,103 @@ describe("PricingUtils Tests", () => {
}); });
describe("getEstimatedSpendHtml()", () => { describe("getEstimatedSpendHtml()", () => {
it("should return 'Cost (USD): <b>$0.000080 hourly / $0.0019 daily / $0.058 monthly </b> (1 region, 1RU/s, $0.00008/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>' for 1RU/s on default cloud, 1 region, with multimaster", () => { it("should return 'Estimated cost (USD): <b>$0.000080 hourly / $0.0019 daily / $0.058 monthly </b> (1 region, 1RU/s, $0.00008/RU)' for 1RU/s on default cloud, 1 region, with multimaster, and no rupm", () => {
const value = PricingUtils.getEstimatedSpendHtml( const value = PricingUtils.getEstimatedSpendHtml(
1 /*RU/s*/, 1 /*RU/s*/,
"default" /* cloud */, "default" /* cloud */,
1 /* region */, 1 /* region */,
true /* multimaster */ true /* multimaster */,
false /* rupm */
); );
expect(value).toBe( expect(value).toBe(
"Cost (USD): <b>$0.000080 hourly / $0.0019 daily / $0.058 monthly </b> (1 region, 1RU/s, $0.00008/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>" "Estimated cost (USD): <b>$0.000080 hourly / $0.0019 daily / $0.058 monthly </b> (1 region, 1RU/s, $0.00008/RU)"
); );
}); });
it("should return 'Cost (RMB): <b>¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly </b> (1 region, 1RU/s, ¥0.00051/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>' for 1RU/s on mooncake, 1 region, with multimaster", () => { it("should return 'Estimated cost (RMB): <b>¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly </b> (1 region, 1RU/s, ¥0.00051/RU)' for 1RU/s on mooncake, 1 region, with multimaster, and no rupm", () => {
const value = PricingUtils.getEstimatedSpendHtml( const value = PricingUtils.getEstimatedSpendHtml(
1 /*RU/s*/, 1 /*RU/s*/,
"mooncake" /* cloud */, "mooncake" /* cloud */,
1 /* region */, 1 /* region */,
true /* multimaster */ true /* multimaster */,
false /* rupm */
); );
expect(value).toBe( expect(value).toBe(
"Cost (RMB): <b>¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly </b> (1 region, 1RU/s, ¥0.00051/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>" "Estimated cost (RMB): <b>¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly </b> (1 region, 1RU/s, ¥0.00051/RU)"
); );
}); });
it("should return 'Cost (USD): <b>$0.13 hourly / $3.07 daily / $140.16 monthly </b> (2 regions, 400RU/s, $0.00016/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>' for 400RU/s on default cloud, 2 region, with multimaster", () => { it("should return 'Estimated cost (USD): <b>$0.13 hourly / $3.07 daily / $140.16 monthly </b> (2 regions, 400RU/s, $0.00016/RU)' for 400RU/s on default cloud, 2 region, with multimaster, and no rupm", () => {
const value = PricingUtils.getEstimatedSpendHtml( const value = PricingUtils.getEstimatedSpendHtml(
400 /*RU/s*/, 400 /*RU/s*/,
"default" /* cloud */, "default" /* cloud */,
2 /* region */, 2 /* region */,
true /* multimaster */ true /* multimaster */,
false /* rupm */
); );
expect(value).toBe( expect(value).toBe(
"Cost (USD): <b>$0.19 hourly / $4.61 daily / $140.16 monthly </b> (2 regions, 400RU/s, $0.00016/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>" "Estimated cost (USD): <b>$0.19 hourly / $4.61 daily / $140.16 monthly </b> (2 regions, 400RU/s, $0.00016/RU)"
); );
}); });
it("should return 'Cost (USD): <b>$0.064 hourly / $1.54 daily / $46.72 monthly </b> (2 regions, 400RU/s, $0.00008/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>' for 400RU/s on default cloud, 2 region, without multimaster", () => { it("should return 'Estimated cost (USD): <b>$0.064 hourly / $1.54 daily / $46.72 monthly </b> (2 regions, 400RU/s, $0.00008/RU)' for 400RU/s on default cloud, 2 region, without multimaster, and no rupm", () => {
const value = PricingUtils.getEstimatedSpendHtml( const value = PricingUtils.getEstimatedSpendHtml(
400 /*RU/s*/, 400 /*RU/s*/,
"default" /* cloud */, "default" /* cloud */,
2 /* region */, 2 /* region */,
false /* multimaster */ false /* multimaster */,
false /* rupm */
); );
expect(value).toBe( expect(value).toBe(
"Cost (USD): <b>$0.064 hourly / $1.54 daily / $46.72 monthly </b> (2 regions, 400RU/s, $0.00008/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>" "Estimated cost (USD): <b>$0.064 hourly / $1.54 daily / $46.72 monthly </b> (2 regions, 400RU/s, $0.00008/RU)"
); );
}); });
}); });
describe("getEstimatedSpendAcknowledgeString()", () => { describe("getEstimatedSpendAcknowledgeString()", () => {
it("should return 'I acknowledge the estimated $0.0019 daily cost for the throughput above.' for 1RU/s on default cloud, 1 region, with multimaster", () => { it("should return 'I acknowledge the estimated $0.0019 daily cost for the throughput above.' for 1RU/s on default cloud, 1 region, with multimaster, and no rupm", () => {
const value = PricingUtils.getEstimatedSpendAcknowledgeString( const value = PricingUtils.getEstimatedSpendAcknowledgeString(
1 /*RU/s*/, 1 /*RU/s*/,
"default" /* cloud */, "default" /* cloud */,
1 /* region */, 1 /* region */,
true /* multimaster */, true /* multimaster */,
false /* rupm */,
false false
); );
expect(value).toBe("I acknowledge the estimated $0.0019 daily cost for the throughput above."); expect(value).toBe("I acknowledge the estimated $0.0019 daily cost for the throughput above.");
}); });
it("should return 'I acknowledge the estimated ¥0.012 daily cost for the throughput above.' for 1RU/s on mooncake, 1 region, with multimaster", () => { it("should return 'I acknowledge the estimated ¥0.012 daily cost for the throughput above.' for 1RU/s on mooncake, 1 region, with multimaster, and no rupm", () => {
const value = PricingUtils.getEstimatedSpendAcknowledgeString( const value = PricingUtils.getEstimatedSpendAcknowledgeString(
1 /*RU/s*/, 1 /*RU/s*/,
"mooncake" /* cloud */, "mooncake" /* cloud */,
1 /* region */, 1 /* region */,
true /* multimaster */, true /* multimaster */,
false /* rupm */,
false false
); );
expect(value).toBe("I acknowledge the estimated ¥0.012 daily cost for the throughput above."); expect(value).toBe("I acknowledge the estimated ¥0.012 daily cost for the throughput above.");
}); });
it("should return 'I acknowledge the estimated $3.07 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, with multimaster", () => { it("should return 'I acknowledge the estimated $3.07 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, with multimaster, and no rupm", () => {
const value = PricingUtils.getEstimatedSpendAcknowledgeString( const value = PricingUtils.getEstimatedSpendAcknowledgeString(
400 /*RU/s*/, 400 /*RU/s*/,
"default" /* cloud */, "default" /* cloud */,
2 /* region */, 2 /* region */,
true /* multimaster */, true /* multimaster */,
false /* rupm */,
false false
); );
expect(value).toBe("I acknowledge the estimated $4.61 daily cost for the throughput above."); expect(value).toBe("I acknowledge the estimated $4.61 daily cost for the throughput above.");
}); });
it("should return 'I acknowledge the estimated $1.54 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, without multimaster", () => { it("should return 'I acknowledge the estimated $1.54 daily cost for the throughput above.' for 400RU/s on default cloud, 2 region, without multimaster, and no rupm", () => {
const value = PricingUtils.getEstimatedSpendAcknowledgeString( const value = PricingUtils.getEstimatedSpendAcknowledgeString(
400 /*RU/s*/, 400 /*RU/s*/,
"default" /* cloud */, "default" /* cloud */,
2 /* region */, 2 /* region */,
false /* multimaster */, false /* multimaster */,
false /* rupm */,
false false
); );
expect(value).toBe("I acknowledge the estimated $1.54 daily cost for the throughput above."); expect(value).toBe("I acknowledge the estimated $1.54 daily cost for the throughput above.");

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