mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 10:51:30 +00:00
Compare commits
15 Commits
package-up
...
replace_jq
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04b00be334 | ||
|
|
5374fd5b6e | ||
|
|
c3058ee5a9 | ||
|
|
b000631a0c | ||
|
|
e8f4c8f93c | ||
|
|
16bde97e47 | ||
|
|
6da43ee27b | ||
|
|
ebae484b8f | ||
|
|
dfb1b50621 | ||
|
|
f54e8eb692 | ||
|
|
ea39c1d092 | ||
|
|
c21f42159f | ||
|
|
31e4b49f11 | ||
|
|
40491ec9c5 | ||
|
|
e133df18dd |
@@ -14,7 +14,6 @@ src/Common/DataAccessUtilityBase.ts
|
||||
src/Common/DeleteFeedback.ts
|
||||
src/Common/DocumentClientUtilityBase.ts
|
||||
src/Common/EditableUtility.ts
|
||||
src/Common/EnvironmentUtility.ts
|
||||
src/Common/HashMap.test.ts
|
||||
src/Common/HashMap.ts
|
||||
src/Common/HeadersUtility.test.ts
|
||||
|
||||
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@@ -101,6 +101,7 @@ jobs:
|
||||
PLATFORM: "Emulator"
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: screenshots
|
||||
path: failed-*
|
||||
@@ -159,13 +160,14 @@ jobs:
|
||||
TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }}
|
||||
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html"
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: screenshots
|
||||
path: failed-*
|
||||
nuget:
|
||||
name: Publish Nuget
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
@@ -189,7 +191,7 @@ jobs:
|
||||
nugetmpac:
|
||||
name: Publish Nuget MPAC
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
@@ -211,3 +213,28 @@ jobs:
|
||||
name: packages
|
||||
with:
|
||||
path: "*.nupkg"
|
||||
nugetie:
|
||||
name: Publish Nuget IE
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
|
||||
steps:
|
||||
- uses: nuget/setup-nuget@v1
|
||||
with:
|
||||
nuget-api-key: ${{ secrets.NUGET_API_KEY }}
|
||||
- name: Download Dist Folder
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
- run: cp ./configs/prod.json config.json
|
||||
- run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.IE/g' DataExplorer.nuspec
|
||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT"
|
||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||
- run: nuget push -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||
- uses: actions/upload-artifact@v2
|
||||
name: packages
|
||||
with:
|
||||
path: "*.nupkg"
|
||||
|
||||
BIN
.vs/slnx.sqlite
Normal file
BIN
.vs/slnx.sqlite
Normal file
Binary file not shown.
37
README.md
37
README.md
@@ -13,29 +13,18 @@ UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), ht
|
||||
|
||||
### Watch mode
|
||||
|
||||
Run `npm run watch` to start the development server and automatically rebuild on changes
|
||||
Run `npm start` to start the development server and automatically rebuild on changes
|
||||
|
||||
### Specifying Development Platform
|
||||
### Hosted Development (https://cosmos.azure.com)
|
||||
|
||||
Setting the environment variable `PLATFORM` during the build process will force the explorer to load the specified platform. By default in development it will run in `Hosted` mode. Valid options:
|
||||
|
||||
- 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.
|
||||
- Visit: `https://localhost:1234/hostedExplorer.html`
|
||||
- Local sign in via AAD will NOT work. Connection string only in dev mode. Use the Portal if you need AAD auth.
|
||||
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
|
||||
|
||||
### Emulator Development
|
||||
|
||||
In a window environment, running `npm run build` will automatically copy the built files from `/dist` over to the default emulator install paths. In a non-windows environment you can specify an alternate endpoint using `EMULATOR_ENDPOINT` and webpack dev server will proxy requests for you.
|
||||
|
||||
`PLATFORM=Emulator EMULATOR_ENDPOINT=https://my-vm.azure.com:8081 npm run watch`
|
||||
- Start the Cosmos Emulator
|
||||
- Visit: https://localhost:1234/index.html
|
||||
|
||||
#### Setting up a Remote Emulator
|
||||
|
||||
@@ -55,16 +44,8 @@ The Cosmos emulator currently only runs in Windows environments. You can still d
|
||||
|
||||
### Portal Development
|
||||
|
||||
The Cosmos Portal that consumes this repo is not currently open source. If you have access to this project, `npm run build` will copy the built files over to the portal where they will be loaded by the portal development environment
|
||||
|
||||
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.
|
||||
- Visit: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html
|
||||
- You may have to manually visit https://localhost:1234/explorer.html first and click through any SSL certificate warnings
|
||||
|
||||
### Testing
|
||||
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -6359,7 +6359,6 @@
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
@@ -14691,6 +14690,14 @@
|
||||
"requires": {
|
||||
"nan": "2.14.1",
|
||||
"prebuild-install": "5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"nan": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
|
||||
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"killable": {
|
||||
@@ -20134,7 +20141,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
|
||||
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
|
||||
@@ -113,7 +113,6 @@ export class Features {
|
||||
public static readonly enableTtl = "enablettl";
|
||||
public static readonly enableNotebooks = "enablenotebooks";
|
||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||
public static readonly enableCodeOfConduct = "enablecodeofconduct";
|
||||
public static readonly enableLinkInjection = "enablelinkinjection";
|
||||
public static readonly enableSpark = "enablespark";
|
||||
public static readonly livyEndpoint = "livyendpoint";
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
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;
|
||||
}
|
||||
10
src/Common/DocumentUtility.ts
Normal file
10
src/Common/DocumentUtility.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
export const getEntityName = (): string => {
|
||||
if (userContext.defaultExperience === DefaultAccountExperienceType.MongoDB) {
|
||||
return "document";
|
||||
}
|
||||
|
||||
return "item";
|
||||
};
|
||||
@@ -1,8 +1,6 @@
|
||||
export default class EnvironmentUtility {
|
||||
public static normalizeArmEndpointUri(uri: string): string {
|
||||
if (uri && uri.slice(-1) !== "/") {
|
||||
return `${uri}/`;
|
||||
}
|
||||
return uri;
|
||||
export function normalizeArmEndpoint(uri: string): string {
|
||||
if (uri && uri.slice(-1) !== "/") {
|
||||
return `${uri}/`;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ describe("parseSDKOfferResponse", () => {
|
||||
autoscaleMaxThroughput: undefined,
|
||||
minimumThroughput: 400,
|
||||
id: "test",
|
||||
offerDefinition: mockOfferDefinition
|
||||
offerDefinition: mockOfferDefinition,
|
||||
offerReplacePending: false
|
||||
};
|
||||
|
||||
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
|
||||
@@ -54,7 +55,8 @@ describe("parseSDKOfferResponse", () => {
|
||||
autoscaleMaxThroughput: 5000,
|
||||
minimumThroughput: 400,
|
||||
id: "test",
|
||||
offerDefinition: mockOfferDefinition
|
||||
offerDefinition: mockOfferDefinition,
|
||||
offerReplacePending: false
|
||||
};
|
||||
|
||||
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
|
||||
import { OfferResponse } from "@azure/cosmos";
|
||||
import { HttpHeaders } from "./Constants";
|
||||
|
||||
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
|
||||
const offerDefinition: SDKOfferDefinition = offerResponse?.resource;
|
||||
@@ -18,7 +19,7 @@ export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput,
|
||||
offerDefinition,
|
||||
headers: offerResponse.headers
|
||||
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,6 +29,6 @@ export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
|
||||
manualThroughput: offerContent.offerThroughput,
|
||||
minimumThroughput,
|
||||
offerDefinition,
|
||||
headers: offerResponse.headers
|
||||
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true"
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,16 +3,18 @@ import * as _ from "underscore";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { QueryUtils } from "../Utils/QueryUtils";
|
||||
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
|
||||
import { userContext } from "../UserContext";
|
||||
import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase";
|
||||
import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage";
|
||||
import { createCollection } from "./dataAccess/createCollection";
|
||||
import { handleError } from "./ErrorHandlingUtils";
|
||||
import { createDocument } from "./dataAccess/createDocument";
|
||||
import { deleteDocument } from "./dataAccess/deleteDocument";
|
||||
import { queryDocuments } from "./dataAccess/queryDocuments";
|
||||
|
||||
export class QueriesClient {
|
||||
private static readonly PartitionKey: DataModels.PartitionKey = {
|
||||
@@ -31,10 +33,7 @@ export class QueriesClient {
|
||||
return Promise.resolve(queriesCollection.rawDataModel);
|
||||
}
|
||||
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
"Setting up account for saving queries"
|
||||
);
|
||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress("Setting up account for saving queries");
|
||||
return createCollection({
|
||||
collectionId: SavedQueries.CollectionName,
|
||||
createNewDatabase: true,
|
||||
@@ -45,10 +44,7 @@ export class QueriesClient {
|
||||
})
|
||||
.then(
|
||||
(collection: DataModels.Collection) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
"Successfully set up account for saving queries"
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleInfo("Successfully set up account for saving queries");
|
||||
return Promise.resolve(collection);
|
||||
},
|
||||
(error: any) => {
|
||||
@@ -56,17 +52,14 @@ export class QueriesClient {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||
.finally(() => clearMessage());
|
||||
}
|
||||
|
||||
public async saveQuery(query: DataModels.Query): Promise<void> {
|
||||
const queriesCollection = this.findQueriesCollection();
|
||||
if (!queriesCollection) {
|
||||
const errorMessage: string = "Account not set up to perform saved query operations";
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to save query ${query.queryName}: ${errorMessage}`
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`);
|
||||
return Promise.reject(errorMessage);
|
||||
}
|
||||
|
||||
@@ -74,25 +67,16 @@ export class QueriesClient {
|
||||
this.validateQuery(query);
|
||||
} catch (error) {
|
||||
const errorMessage: string = "Invalid query specified";
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to save query ${query.queryName}: ${errorMessage}`
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`);
|
||||
return Promise.reject(errorMessage);
|
||||
}
|
||||
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Saving query ${query.queryName}`
|
||||
);
|
||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Saving query ${query.queryName}`);
|
||||
query.id = query.queryName;
|
||||
return createDocument(queriesCollection, query)
|
||||
.then(
|
||||
(savedQuery: DataModels.Query) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully saved query ${query.queryName}`
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleInfo(`Successfully saved query ${query.queryName}`);
|
||||
return Promise.resolve();
|
||||
},
|
||||
(error: any) => {
|
||||
@@ -103,74 +87,65 @@ export class QueriesClient {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||
.finally(() => clearMessage());
|
||||
}
|
||||
|
||||
public async getQueries(): Promise<DataModels.Query[]> {
|
||||
const queriesCollection = this.findQueriesCollection();
|
||||
if (!queriesCollection) {
|
||||
const errorMessage: string = "Account not set up to perform saved query operations";
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to fetch saved queries: ${errorMessage}`
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`);
|
||||
return Promise.reject(errorMessage);
|
||||
}
|
||||
|
||||
const options: any = { enableCrossPartitionQuery: true };
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries");
|
||||
return queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options)
|
||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress("Fetching saved queries");
|
||||
const queryIterator: QueryIterator<ItemDefinition & Resource> = queryDocuments(
|
||||
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(
|
||||
(queryIterator: QueryIterator<ItemDefinition & Resource>) => {
|
||||
const fetchQueries = (firstItemIndex: number): Q.Promise<ViewModels.QueryResults> =>
|
||||
queryDocumentsPage(queriesCollection.id(), queryIterator, firstItemIndex, options);
|
||||
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);
|
||||
(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.logConsoleInfo("Successfully fetched saved queries");
|
||||
return Promise.resolve(queries);
|
||||
},
|
||||
(error: any) => {
|
||||
// should never get into this state but we handle this regardless
|
||||
handleError(error, "getSavedQueries", "Failed to fetch saved queries");
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||
.finally(() => clearMessage());
|
||||
}
|
||||
|
||||
public async deleteQuery(query: DataModels.Query): Promise<void> {
|
||||
const queriesCollection = this.findQueriesCollection();
|
||||
if (!queriesCollection) {
|
||||
const errorMessage: string = "Account not set up to perform saved query operations";
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to fetch saved queries: ${errorMessage}`
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`);
|
||||
return Promise.reject(errorMessage);
|
||||
}
|
||||
|
||||
@@ -178,16 +153,10 @@ export class QueriesClient {
|
||||
this.validateQuery(query);
|
||||
} catch (error) {
|
||||
const errorMessage: string = "Invalid query specified";
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to delete query ${query.queryName}: ${errorMessage}`
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to delete query ${query.queryName}: ${errorMessage}`);
|
||||
}
|
||||
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Deleting query ${query.queryName}`
|
||||
);
|
||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting query ${query.queryName}`);
|
||||
query.id = query.queryName;
|
||||
const documentId = new DocumentId(
|
||||
{
|
||||
@@ -201,10 +170,7 @@ export class QueriesClient {
|
||||
return deleteDocument(queriesCollection, documentId)
|
||||
.then(
|
||||
() => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully deleted query ${query.queryName}`
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleInfo(`Successfully deleted query ${query.queryName}`);
|
||||
return Promise.resolve();
|
||||
},
|
||||
(error: any) => {
|
||||
@@ -212,7 +178,7 @@ export class QueriesClient {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||
.finally(() => clearMessage());
|
||||
}
|
||||
|
||||
public getResourceId(): string {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
jest.mock("../../Utils/arm/request");
|
||||
jest.mock("../CosmosClient");
|
||||
jest.mock("../DataAccessUtilityBase");
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { CreateCollectionParams, DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
|
||||
25
src/Common/dataAccess/createDocument.ts
Normal file
25
src/Common/dataAccess/createDocument.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
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();
|
||||
}
|
||||
};
|
||||
36
src/Common/dataAccess/deleteConflict.ts
Normal file
36
src/Common/dataAccess/deleteConflict.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
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];
|
||||
};
|
||||
25
src/Common/dataAccess/deleteDocument.ts
Normal file
25
src/Common/dataAccess/deleteDocument.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
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();
|
||||
}
|
||||
};
|
||||
48
src/Common/dataAccess/executeStoredProcedure.ts
Normal file
48
src/Common/dataAccess/executeStoredProcedure.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
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();
|
||||
}
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { armRequest } from "../../Utils/arm/request";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
@@ -40,6 +41,10 @@ interface MetricsResponse {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
14
src/Common/dataAccess/queryConflicts.ts
Normal file
14
src/Common/dataAccess/queryConflicts.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
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);
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
import { getCommonQueryOptions } from "./DataAccessUtilityBase";
|
||||
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
||||
|
||||
describe("getCommonQueryOptions", () => {
|
||||
it("builds the correct default options objects", () => {
|
||||
expect(getCommonQueryOptions({})).toMatchSnapshot();
|
||||
});
|
||||
it("reads from localStorage", () => {
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37);
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17);
|
||||
expect(getCommonQueryOptions({})).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
import { getCommonQueryOptions } from "./queryDocuments";
|
||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
|
||||
describe("getCommonQueryOptions", () => {
|
||||
it("builds the correct default options objects", () => {
|
||||
expect(getCommonQueryOptions({})).toMatchSnapshot();
|
||||
});
|
||||
it("reads from localStorage", () => {
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.ActualItemPerPage, 37);
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, 17);
|
||||
expect(getCommonQueryOptions({})).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
34
src/Common/dataAccess/queryDocuments.ts
Normal file
34
src/Common/dataAccess/queryDocuments.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
};
|
||||
26
src/Common/dataAccess/queryDocumentsPage.ts
Normal file
26
src/Common/dataAccess/queryDocumentsPage.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
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();
|
||||
}
|
||||
};
|
||||
@@ -105,7 +105,8 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
id: offerId,
|
||||
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,7 +114,8 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
id: offerId,
|
||||
autoscaleMaxThroughput: undefined,
|
||||
manualThroughput: resource.throughput,
|
||||
minimumThroughput
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,8 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||
id: offerId,
|
||||
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,7 +86,8 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||
id: offerId,
|
||||
autoscaleMaxThroughput: undefined,
|
||||
manualThroughput: resource.throughput,
|
||||
minimumThroughput
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
27
src/Common/dataAccess/readDocument.ts
Normal file
27
src/Common/dataAccess/readDocument.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
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();
|
||||
}
|
||||
};
|
||||
32
src/Common/dataAccess/updateDocument.ts
Normal file
32
src/Common/dataAccess/updateDocument.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
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();
|
||||
}
|
||||
};
|
||||
@@ -214,7 +214,7 @@ export interface Offer {
|
||||
manualThroughput: number;
|
||||
minimumThroughput: number;
|
||||
offerDefinition?: SDKOfferDefinition;
|
||||
headers?: any;
|
||||
offerReplacePending: boolean;
|
||||
}
|
||||
|
||||
export interface SDKOfferDefinition extends Resource {
|
||||
|
||||
@@ -362,7 +362,7 @@ export enum CollectionTabKind {
|
||||
Gallery = 17,
|
||||
NotebookViewer = 18,
|
||||
Schema = 19,
|
||||
SettingsV2 = 19
|
||||
SettingsV2 = 20
|
||||
}
|
||||
|
||||
export enum TerminalKind {
|
||||
|
||||
@@ -48,7 +48,6 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
||||
{ key: "feature.enablettl", label: "Enable TTL", 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",
|
||||
label: "Enable Injecting Notebook Viewer Link into the first cell",
|
||||
|
||||
@@ -157,14 +157,14 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablecodeofconduct"
|
||||
label="Enable Code Of Conduct Acknowledgement"
|
||||
key="feature.enableLinkInjection"
|
||||
label="Enable Injecting Notebook Viewer Link into the first cell"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enableLinkInjection"
|
||||
label="Enable Injecting Notebook Viewer Link into the first cell"
|
||||
key="feature.canexceedmaximumvalue"
|
||||
label="Can exceed max value"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -172,12 +172,6 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
className="checkboxRow"
|
||||
horizontalAlign="space-between"
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.canexceedmaximumvalue"
|
||||
label="Can exceed max value"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablefixedcollectionwithsharedthroughput"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Dropdown,
|
||||
FocusZone,
|
||||
FontIcon,
|
||||
FontWeights,
|
||||
IDropdownOption,
|
||||
IPageSpecification,
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
Text
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
|
||||
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
||||
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
|
||||
@@ -136,7 +137,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||
const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||
|
||||
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||
tabs.push(
|
||||
@@ -146,7 +147,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
this.state.isCodeOfConductAccepted
|
||||
)
|
||||
);
|
||||
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||
|
||||
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
|
||||
// Displaying code of conduct component on gallery load should not be the default behavior.
|
||||
@@ -183,6 +184,27 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
);
|
||||
}
|
||||
|
||||
private isEmptyData = (data: IGalleryItem[]): boolean => {
|
||||
return !data || data.length === 0;
|
||||
};
|
||||
|
||||
private createEmptyTabContent = (iconName: string, line1: string, line2: string): JSX.Element => {
|
||||
return (
|
||||
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}>
|
||||
<FontIcon iconName={iconName} style={{ fontSize: 100, color: "lightgray", marginTop: 20 }} />
|
||||
<Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{line1}</Text>
|
||||
<Text>{line2}</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
private createSamplesTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||
return {
|
||||
tab,
|
||||
content: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||
};
|
||||
};
|
||||
|
||||
private createPublicGalleryTab(
|
||||
tab: GalleryTab,
|
||||
data: IGalleryItem[],
|
||||
@@ -194,17 +216,29 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
};
|
||||
}
|
||||
|
||||
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||
private createFavoritesTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||
return {
|
||||
tab,
|
||||
content: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||
content: this.isEmptyData(data)
|
||||
? this.createEmptyTabContent(
|
||||
"ContactHeart",
|
||||
"You have not liked anything",
|
||||
"Like any notebook from Official Samples or Public gallery"
|
||||
)
|
||||
: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||
};
|
||||
}
|
||||
|
||||
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||
return {
|
||||
tab,
|
||||
content: this.createPublishedNotebooksTabContent(data)
|
||||
content: this.isEmptyData(data)
|
||||
? this.createEmptyTabContent(
|
||||
"Contact",
|
||||
"You have not published anything",
|
||||
"Publish your sample notebooks to share your published work with others"
|
||||
)
|
||||
: this.createPublishedNotebooksTabContent(data)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -364,9 +398,9 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||
if (!offline) {
|
||||
try {
|
||||
let response: IJunoResponse<IPublicGalleryData> | IJunoResponse<IGalleryItem[]>;
|
||||
if (this.props.container.isCodeOfConductEnabled()) {
|
||||
response = await this.props.junoClient.fetchPublicNotebooks();
|
||||
let response: IJunoResponse<IGalleryItem[]> | IJunoResponse<IPublicGalleryData>;
|
||||
if (this.props.container) {
|
||||
response = await this.props.junoClient.getPublicGalleryData();
|
||||
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
|
||||
this.publicNotebooks = response.data?.notebooksData;
|
||||
} else {
|
||||
@@ -568,7 +602,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
|
||||
private deleteItem = async (data: IGalleryItem): Promise<void> => {
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -92,7 +92,8 @@ describe("SettingsComponent", () => {
|
||||
autoscaleMaxThroughput: 10000,
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput: 400,
|
||||
id: "test"
|
||||
id: "test",
|
||||
offerReplacePending: false
|
||||
});
|
||||
|
||||
const props = { ...baseProps };
|
||||
|
||||
@@ -295,7 +295,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
!!this.collection.conflictResolutionPolicy();
|
||||
|
||||
public isOfferReplacePending = (): boolean => {
|
||||
return !!this.collection?.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending];
|
||||
return this.collection?.offer()?.offerReplacePending;
|
||||
};
|
||||
|
||||
public onSaveClick = async (): Promise<void> => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { IColumn, Text } from "office-ui-fabric-react";
|
||||
import {
|
||||
getAutoPilotV3SpendElement,
|
||||
getEstimatedSpendElement,
|
||||
getEstimatedAutoscaleSpendElement,
|
||||
getEstimatedSpendingElement,
|
||||
manualToAutoscaleDisclaimerElement,
|
||||
ttlWarning,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
@@ -19,11 +19,37 @@ import {
|
||||
mongoIndexingPolicyDisclaimer,
|
||||
mongoIndexingPolicyAADError,
|
||||
mongoIndexTransformationRefreshingMessage,
|
||||
renderMongoIndexTransformationRefreshMessage
|
||||
renderMongoIndexTransformationRefreshMessage,
|
||||
ManualEstimatedSpendingDisplayProps,
|
||||
PriceBreakdown,
|
||||
getRuPriceBreakdown
|
||||
} from "./SettingsRenderUtils";
|
||||
|
||||
class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
public render(): JSX.Element {
|
||||
const estimatedSpendingColumns: IColumn[] = [
|
||||
{ key: "costType", name: "", fieldName: "costType", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "hourly", name: "Hourly", fieldName: "hourly", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "daily", name: "Daily", fieldName: "daily", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "monthly", name: "Monthly", fieldName: "monthly", minWidth: 100, maxWidth: 200, isResizable: true }
|
||||
];
|
||||
const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [
|
||||
{
|
||||
costType: <Text>Current Cost</Text>,
|
||||
hourly: <Text>$ 1.02</Text>,
|
||||
daily: <Text>$ 24.48</Text>,
|
||||
monthly: <Text>$ 744.6</Text>
|
||||
}
|
||||
];
|
||||
const priceBreakdown: PriceBreakdown = {
|
||||
hourlyPrice: 1.02,
|
||||
dailyPrice: 24.48,
|
||||
monthlyPrice: 744.6,
|
||||
pricePerRu: 0.00051,
|
||||
currency: "RMB",
|
||||
currencySign: "¥"
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{getAutoPilotV3SpendElement(1000, false)}
|
||||
@@ -31,9 +57,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
{getAutoPilotV3SpendElement(1000, true)}
|
||||
{getAutoPilotV3SpendElement(undefined, true)}
|
||||
|
||||
{getEstimatedSpendElement(1000, "mooncake", 2, false)}
|
||||
|
||||
{getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)}
|
||||
{getEstimatedSpendingElement(estimatedSpendingColumns, estimatedSpendingItems, 1000, 2, priceBreakdown, false)}
|
||||
|
||||
{manualToAutoscaleDisclaimerElement}
|
||||
{ttlWarning}
|
||||
@@ -69,4 +93,14 @@ describe("SettingsUtils functions", () => {
|
||||
const wrapper = shallow(<SettingsRenderUtilsTestComponent />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should return correct price breakdown for a manual RU setting of 500, 1 region, multimaster disabled", () => {
|
||||
const prices = getRuPriceBreakdown(500, "", 1, false, false);
|
||||
expect(prices.hourlyPrice).toBe(0.04);
|
||||
expect(prices.dailyPrice).toBe(0.96);
|
||||
expect(prices.monthlyPrice).toBe(29.2);
|
||||
expect(prices.pricePerRu).toBe(0.00008);
|
||||
expect(prices.currency).toBe("USD");
|
||||
expect(prices.currencySign).toBe("$");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,14 +3,13 @@ import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
|
||||
import { Urls, StyleConstants } from "../../../Common/Constants";
|
||||
import {
|
||||
computeAutoscaleUsagePriceHourly,
|
||||
getPriceCurrency,
|
||||
getCurrencySign,
|
||||
getAutoscalePricePerRu,
|
||||
getMultimasterMultiplier,
|
||||
computeRUUsagePriceHourly,
|
||||
getPricePerRu,
|
||||
calculateEstimateNumber
|
||||
estimatedCostDisclaimer
|
||||
} from "../../../Utils/PricingUtils";
|
||||
import {
|
||||
ITextFieldStyles,
|
||||
@@ -32,10 +31,41 @@ import {
|
||||
MessageBarType,
|
||||
Stack,
|
||||
Spinner,
|
||||
SpinnerSize
|
||||
SpinnerSize,
|
||||
DetailsList,
|
||||
IColumn,
|
||||
SelectionMode,
|
||||
DetailsListLayoutMode,
|
||||
IDetailsRowProps,
|
||||
DetailsRow,
|
||||
IDetailsColumnStyles
|
||||
} from "office-ui-fabric-react";
|
||||
import { isDirtyTypes, isDirty } from "./SettingsUtils";
|
||||
|
||||
export interface EstimatedSpendingDisplayProps {
|
||||
costType: JSX.Element;
|
||||
}
|
||||
|
||||
export interface ManualEstimatedSpendingDisplayProps extends EstimatedSpendingDisplayProps {
|
||||
hourly: JSX.Element;
|
||||
daily: JSX.Element;
|
||||
monthly: JSX.Element;
|
||||
}
|
||||
|
||||
export interface AutoscaleEstimatedSpendingDisplayProps extends EstimatedSpendingDisplayProps {
|
||||
minPerMonth: JSX.Element;
|
||||
maxPerMonth: JSX.Element;
|
||||
}
|
||||
|
||||
export interface PriceBreakdown {
|
||||
hourlyPrice: number;
|
||||
dailyPrice: number;
|
||||
monthlyPrice: number;
|
||||
pricePerRu: number;
|
||||
currency: string;
|
||||
currencySign: string;
|
||||
}
|
||||
|
||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
|
||||
|
||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||
@@ -104,6 +134,16 @@ export const transparentDetailsRowStyles: Partial<IDetailsRowStyles> = {
|
||||
}
|
||||
};
|
||||
|
||||
export const transparentDetailsHeaderStyle: Partial<IDetailsColumnStyles> = {
|
||||
root: {
|
||||
selectors: {
|
||||
":hover": {
|
||||
background: "transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const customDetailsListStyles: Partial<IDetailsListStyles> = {
|
||||
root: {
|
||||
selectors: {
|
||||
@@ -130,6 +170,10 @@ export const messageBarStyles: Partial<IMessageBarStyles> = { root: { marginTop:
|
||||
|
||||
export const throughputUnit = "RU/s";
|
||||
|
||||
export function onRenderRow(props: IDetailsRowProps): JSX.Element {
|
||||
return <DetailsRow {...props} styles={transparentDetailsRowStyles} />;
|
||||
}
|
||||
|
||||
export const getAutoPilotV3SpendElement = (
|
||||
maxAutoPilotThroughputSet: number,
|
||||
isDatabaseThroughput: boolean,
|
||||
@@ -165,63 +209,61 @@ export const getAutoPilotV3SpendElement = (
|
||||
);
|
||||
};
|
||||
|
||||
export const getEstimatedAutoscaleSpendElement = (
|
||||
export const getRuPriceBreakdown = (
|
||||
throughput: number,
|
||||
serverId: string,
|
||||
regions: number,
|
||||
multimaster: boolean
|
||||
): JSX.Element => {
|
||||
const hourlyPrice: number = computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster);
|
||||
const monthlyPrice: number = hourlyPrice * hoursInAMonth;
|
||||
const currency: string = getPriceCurrency(serverId);
|
||||
const currencySign: string = getCurrencySign(serverId);
|
||||
const pricePerRu =
|
||||
getAutoscalePricePerRu(serverId, getMultimasterMultiplier(regions, multimaster)) *
|
||||
getMultimasterMultiplier(regions, multimaster);
|
||||
|
||||
return (
|
||||
<Text id="autoscaleSpendElement">
|
||||
Estimated monthly cost ({currency}) is{" "}
|
||||
<b>
|
||||
{currencySign}
|
||||
{calculateEstimateNumber(monthlyPrice / 10)}
|
||||
{` - `}
|
||||
{currencySign}
|
||||
{calculateEstimateNumber(monthlyPrice)}{" "}
|
||||
</b>
|
||||
({"regions: "} {regions}, {throughput / 10} - {throughput} RU/s, {currencySign}
|
||||
{pricePerRu}/RU)
|
||||
</Text>
|
||||
);
|
||||
numberOfRegions: number,
|
||||
isMultimaster: boolean,
|
||||
isAutoscale: boolean
|
||||
): PriceBreakdown => {
|
||||
const hourlyPrice: number = computeRUUsagePriceHourly({
|
||||
serverId: serverId,
|
||||
requestUnits: throughput,
|
||||
numberOfRegions: numberOfRegions,
|
||||
multimasterEnabled: isMultimaster,
|
||||
isAutoscale: isAutoscale
|
||||
});
|
||||
const basePricePerRu: number = isAutoscale
|
||||
? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster))
|
||||
: getPricePerRu(serverId);
|
||||
return {
|
||||
hourlyPrice: hourlyPrice,
|
||||
dailyPrice: hourlyPrice * 24,
|
||||
monthlyPrice: hourlyPrice * hoursInAMonth,
|
||||
pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster),
|
||||
currency: getPriceCurrency(serverId),
|
||||
currencySign: getCurrencySign(serverId)
|
||||
};
|
||||
};
|
||||
|
||||
export const getEstimatedSpendElement = (
|
||||
export const getEstimatedSpendingElement = (
|
||||
estimatedSpendingColumns: IColumn[],
|
||||
estimatedSpendingItems: EstimatedSpendingDisplayProps[],
|
||||
throughput: number,
|
||||
serverId: string,
|
||||
regions: number,
|
||||
multimaster: boolean
|
||||
numberOfRegions: number,
|
||||
priceBreakdown: PriceBreakdown,
|
||||
isAutoscale: boolean
|
||||
): JSX.Element => {
|
||||
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster);
|
||||
const dailyPrice: number = hourlyPrice * 24;
|
||||
const monthlyPrice: number = hourlyPrice * hoursInAMonth;
|
||||
const currency: string = getPriceCurrency(serverId);
|
||||
const currencySign: string = getCurrencySign(serverId);
|
||||
const pricePerRu = getPricePerRu(serverId) * getMultimasterMultiplier(regions, multimaster);
|
||||
|
||||
const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : "";
|
||||
return (
|
||||
<Text id="throughputSpendElement">
|
||||
Estimated cost ({currency}):{" "}
|
||||
<b>
|
||||
{currencySign}
|
||||
{calculateEstimateNumber(hourlyPrice)} hourly {` / `}
|
||||
{currencySign}
|
||||
{calculateEstimateNumber(dailyPrice)} daily {` / `}
|
||||
{currencySign}
|
||||
{calculateEstimateNumber(monthlyPrice)} monthly{" "}
|
||||
</b>
|
||||
({"regions: "} {regions}, {throughput}RU/s, {currencySign}
|
||||
{pricePerRu}/RU)
|
||||
</Text>
|
||||
<Stack {...addMongoIndexStackProps} styles={mediumWidthStackStyles}>
|
||||
<DetailsList
|
||||
disableSelectionZone
|
||||
items={estimatedSpendingItems}
|
||||
columns={estimatedSpendingColumns}
|
||||
selectionMode={SelectionMode.none}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
onRenderRow={onRenderRow}
|
||||
/>
|
||||
<Text id="throughputSpendElement">
|
||||
({"regions: "} {numberOfRegions}, {ruRange}
|
||||
{throughput} RU/s, {priceBreakdown.currencySign}
|
||||
{priceBreakdown.pricePerRu}/RU)
|
||||
</Text>
|
||||
<Text>
|
||||
<em>{estimatedCostDisclaimer}</em>
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -265,6 +307,13 @@ export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const saveThroughputWarningMessage: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below
|
||||
before saving your changes
|
||||
</Text>
|
||||
);
|
||||
|
||||
const getCurrentThroughput = (
|
||||
isAutoscale: boolean,
|
||||
throughput: number,
|
||||
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
IconButton,
|
||||
Text,
|
||||
SelectionMode,
|
||||
IDetailsRowProps,
|
||||
DetailsRow,
|
||||
IColumn,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
@@ -21,11 +19,11 @@ import {
|
||||
mongoIndexingPolicyDisclaimer,
|
||||
mediumWidthStackStyles,
|
||||
subComponentStackProps,
|
||||
transparentDetailsRowStyles,
|
||||
createAndAddMongoIndexStackProps,
|
||||
separatorStyles,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
infoAndToolTipTextStyle
|
||||
infoAndToolTipTextStyle,
|
||||
onRenderRow
|
||||
} from "../../SettingsRenderUtils";
|
||||
import { MongoIndex } from "../../../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import {
|
||||
@@ -140,10 +138,6 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private onRenderRow = (props: IDetailsRowProps): JSX.Element => {
|
||||
return <DetailsRow {...props} styles={transparentDetailsRowStyles} />;
|
||||
};
|
||||
|
||||
private getActionButton = (arrayPosition: number, isCurrentIndex: boolean): JSX.Element => {
|
||||
return isCurrentIndex ? (
|
||||
<IconButton
|
||||
@@ -253,7 +247,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
items={initialIndexes}
|
||||
columns={this.initialIndexesColumns}
|
||||
selectionMode={SelectionMode.none}
|
||||
onRenderRow={this.onRenderRow}
|
||||
onRenderRow={onRenderRow}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
/>
|
||||
{this.renderIndexesToBeAdded()}
|
||||
@@ -279,7 +273,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
items={indexesToBeDropped}
|
||||
columns={this.indexesToBeDroppedColumns}
|
||||
selectionMode={SelectionMode.none}
|
||||
onRenderRow={this.onRenderRow}
|
||||
onRenderRow={onRenderRow}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("ScaleComponent", () => {
|
||||
autoscaleMaxThroughput: maxThroughput,
|
||||
minimumThroughput: 400,
|
||||
id: "offer",
|
||||
headers: { "x-ms-offer-replace-pending": true }
|
||||
offerReplacePending: true
|
||||
});
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
|
||||
@@ -116,7 +116,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
}
|
||||
|
||||
const offer = this.props.collection?.offer();
|
||||
if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) {
|
||||
if (offer?.offerReplacePending) {
|
||||
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
||||
return getThroughputApplyShortDelayMessage(
|
||||
this.props.isAutoPilotSelected,
|
||||
|
||||
@@ -54,7 +54,6 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
||||
expect(wrapper.exists("#throughputInput")).toEqual(true);
|
||||
expect(wrapper.exists("#autopilotInput")).toEqual(false);
|
||||
expect(wrapper.exists("#throughputSpendElement")).toEqual(true);
|
||||
expect(wrapper.exists("#autoscaleSpendElement")).toEqual(false);
|
||||
});
|
||||
|
||||
it("autopilot input visible", () => {
|
||||
@@ -72,8 +71,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
||||
|
||||
wrapper.setProps({ wasAutopilotOriginallySet: true });
|
||||
wrapper.update();
|
||||
expect(wrapper.exists("#autoscaleSpendElement")).toEqual(true);
|
||||
expect(wrapper.exists("#throughputSpendElement")).toEqual(false);
|
||||
expect(wrapper.exists("#throughputSpendElement")).toEqual(true);
|
||||
});
|
||||
|
||||
it("spendAck checkbox visible", () => {
|
||||
|
||||
@@ -8,10 +8,15 @@ import {
|
||||
checkBoxAndInputStackProps,
|
||||
getChoiceGroupStyles,
|
||||
messageBarStyles,
|
||||
getEstimatedSpendElement,
|
||||
getEstimatedAutoscaleSpendElement,
|
||||
getEstimatedSpendingElement,
|
||||
getAutoPilotV3SpendElement,
|
||||
manualToAutoscaleDisclaimerElement
|
||||
manualToAutoscaleDisclaimerElement,
|
||||
saveThroughputWarningMessage,
|
||||
ManualEstimatedSpendingDisplayProps,
|
||||
AutoscaleEstimatedSpendingDisplayProps,
|
||||
PriceBreakdown,
|
||||
getRuPriceBreakdown,
|
||||
transparentDetailsHeaderStyle
|
||||
} from "../../SettingsRenderUtils";
|
||||
import {
|
||||
Text,
|
||||
@@ -23,7 +28,9 @@ import {
|
||||
Label,
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarType
|
||||
MessageBarType,
|
||||
FontIcon,
|
||||
IColumn
|
||||
} from "office-ui-fabric-react";
|
||||
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
|
||||
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
|
||||
@@ -32,7 +39,7 @@ import * as DataModels from "../../../../../Contracts/DataModels";
|
||||
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
import { userContext } from "../../../../../UserContext";
|
||||
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
|
||||
import { usageInGB } from "../../../../../Utils/PricingUtils";
|
||||
import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils";
|
||||
import { Features } from "../../../../../Common/Constants";
|
||||
|
||||
export interface ThroughputInputAutoPilotV3Props {
|
||||
@@ -165,33 +172,243 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const isDirty: boolean = this.IsComponentDirty().isDiscardable;
|
||||
const serverId: string = this.props.serverId;
|
||||
const offerThroughput: number = this.props.throughput;
|
||||
|
||||
const regions = account?.properties?.readLocations?.length || 1;
|
||||
const multimaster = account?.properties?.enableMultipleWriteLocations || false;
|
||||
|
||||
let estimatedSpend: JSX.Element;
|
||||
|
||||
if (!this.props.isAutoPilotSelected) {
|
||||
estimatedSpend = getEstimatedSpendElement(
|
||||
estimatedSpend = this.getEstimatedManualSpendElement(
|
||||
// if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set...
|
||||
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : offerThroughput,
|
||||
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : this.props.throughputBaseline,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster
|
||||
multimaster,
|
||||
isDirty ? this.props.throughput : undefined
|
||||
);
|
||||
} else {
|
||||
estimatedSpend = getEstimatedAutoscaleSpendElement(
|
||||
this.props.maxAutoPilotThroughput,
|
||||
estimatedSpend = this.getEstimatedAutoscaleSpendElement(
|
||||
this.props.maxAutoPilotThroughputBaseline,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster
|
||||
multimaster,
|
||||
isDirty ? this.props.maxAutoPilotThroughput : undefined
|
||||
);
|
||||
}
|
||||
return estimatedSpend;
|
||||
};
|
||||
|
||||
private getEstimatedAutoscaleSpendElement = (
|
||||
throughput: number,
|
||||
serverId: string,
|
||||
numberOfRegions: number,
|
||||
isMultimaster: boolean,
|
||||
newThroughput?: number
|
||||
): JSX.Element => {
|
||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
|
||||
const estimatedSpendingColumns: IColumn[] = [
|
||||
{
|
||||
key: "costType",
|
||||
name: "",
|
||||
fieldName: "costType",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle
|
||||
},
|
||||
{
|
||||
key: "minPerMonth",
|
||||
name: "Min Per Month",
|
||||
fieldName: "minPerMonth",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle
|
||||
},
|
||||
{
|
||||
key: "maxPerMonth",
|
||||
name: "Max Per Month",
|
||||
fieldName: "maxPerMonth",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle
|
||||
}
|
||||
];
|
||||
const estimatedSpendingItems: AutoscaleEstimatedSpendingDisplayProps[] = [
|
||||
{
|
||||
costType: <Text>Current Cost</Text>,
|
||||
minPerMonth: (
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)}
|
||||
</Text>
|
||||
),
|
||||
maxPerMonth: (
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
if (newThroughput) {
|
||||
const newPrices: PriceBreakdown = getRuPriceBreakdown(
|
||||
newThroughput,
|
||||
serverId,
|
||||
numberOfRegions,
|
||||
isMultimaster,
|
||||
true
|
||||
);
|
||||
estimatedSpendingItems.unshift({
|
||||
costType: (
|
||||
<Text>
|
||||
<b>Updated Cost</b>
|
||||
</Text>
|
||||
),
|
||||
minPerMonth: (
|
||||
<Text>
|
||||
<b>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)}
|
||||
</b>
|
||||
</Text>
|
||||
),
|
||||
maxPerMonth: (
|
||||
<Text>
|
||||
<b>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
|
||||
</b>
|
||||
</Text>
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
return getEstimatedSpendingElement(
|
||||
estimatedSpendingColumns,
|
||||
estimatedSpendingItems,
|
||||
newThroughput ?? throughput,
|
||||
numberOfRegions,
|
||||
prices,
|
||||
true
|
||||
);
|
||||
};
|
||||
|
||||
private getEstimatedManualSpendElement = (
|
||||
throughput: number,
|
||||
serverId: string,
|
||||
numberOfRegions: number,
|
||||
isMultimaster: boolean,
|
||||
newThroughput?: number
|
||||
): JSX.Element => {
|
||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false);
|
||||
const estimatedSpendingColumns: IColumn[] = [
|
||||
{
|
||||
key: "costType",
|
||||
name: "",
|
||||
fieldName: "costType",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle
|
||||
},
|
||||
{
|
||||
key: "hourly",
|
||||
name: "Hourly",
|
||||
fieldName: "hourly",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle
|
||||
},
|
||||
{
|
||||
key: "daily",
|
||||
name: "Daily",
|
||||
fieldName: "daily",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle
|
||||
},
|
||||
{
|
||||
key: "monthly",
|
||||
name: "Monthly",
|
||||
fieldName: "monthly",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle
|
||||
}
|
||||
];
|
||||
const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [
|
||||
{
|
||||
costType: <Text>Current Cost</Text>,
|
||||
hourly: (
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}
|
||||
</Text>
|
||||
),
|
||||
daily: (
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}
|
||||
</Text>
|
||||
),
|
||||
monthly: (
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
if (newThroughput) {
|
||||
const newPrices: PriceBreakdown = getRuPriceBreakdown(
|
||||
newThroughput,
|
||||
serverId,
|
||||
numberOfRegions,
|
||||
isMultimaster,
|
||||
false
|
||||
);
|
||||
estimatedSpendingItems.unshift({
|
||||
costType: (
|
||||
<Text>
|
||||
<b>Updated Cost</b>
|
||||
</Text>
|
||||
),
|
||||
hourly: (
|
||||
<Text>
|
||||
<b>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}
|
||||
</b>
|
||||
</Text>
|
||||
),
|
||||
daily: (
|
||||
<Text>
|
||||
<b>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}
|
||||
</b>
|
||||
</Text>
|
||||
),
|
||||
monthly: (
|
||||
<Text>
|
||||
<b>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
|
||||
</b>
|
||||
</Text>
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
return getEstimatedSpendingElement(
|
||||
estimatedSpendingColumns,
|
||||
estimatedSpendingItems,
|
||||
newThroughput ?? throughput,
|
||||
numberOfRegions,
|
||||
prices,
|
||||
false
|
||||
);
|
||||
};
|
||||
|
||||
private getAutoPilotUsageCost = (): JSX.Element => {
|
||||
if (!this.props.maxAutoPilotThroughput) {
|
||||
return <></>;
|
||||
@@ -318,6 +535,12 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
|
||||
private renderThroughputInput = (): JSX.Element => (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<Text>
|
||||
Estimate your required throughput with
|
||||
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
|
||||
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />
|
||||
</Link>
|
||||
</Text>
|
||||
<TextField
|
||||
required
|
||||
type="number"
|
||||
@@ -349,13 +572,24 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
onChange={this.onSpendAckChecked}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
private renderWarningMessage = (): JSX.Element => {
|
||||
let warningMessage: JSX.Element;
|
||||
if (this.IsComponentDirty().isDiscardable) {
|
||||
warningMessage = saveThroughputWarningMessage;
|
||||
}
|
||||
|
||||
return <>{warningMessage && <MessageBar messageBarType={MessageBarType.warning}>{warningMessage}</MessageBar>}</>;
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Stack {...checkBoxAndInputStackProps}>
|
||||
{this.renderWarningMessage()}
|
||||
{this.renderThroughputModeChoices()}
|
||||
|
||||
{this.props.isAutoPilotSelected ? this.renderAutoPilotInput() : this.renderThroughputInput()}
|
||||
|
||||
@@ -8,6 +8,21 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledMessageBarBase
|
||||
messageBarType={5}
|
||||
>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below before saving your changes
|
||||
</Text>
|
||||
</StyledMessageBarBase>
|
||||
<Stack>
|
||||
<StyledLabelBase
|
||||
id="settingsV2RadioButtonLabelId"
|
||||
@@ -214,6 +229,19 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text>
|
||||
Estimate your required throughput with
|
||||
<StyledLinkBase
|
||||
href="https://cosmos.azure.com/capacitycalculator/"
|
||||
target="_blank"
|
||||
>
|
||||
capacity calculator
|
||||
|
||||
<Component
|
||||
iconName="NavigateExternalInline"
|
||||
/>
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
<StyledTextFieldBase
|
||||
disabled={false}
|
||||
id="throughputInput"
|
||||
@@ -239,38 +267,142 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
type="number"
|
||||
value="100"
|
||||
/>
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
Estimated cost (
|
||||
USD
|
||||
):
|
||||
|
||||
<b>
|
||||
$
|
||||
0.0080
|
||||
hourly
|
||||
/
|
||||
$
|
||||
0.19
|
||||
daily
|
||||
/
|
||||
$
|
||||
5.84
|
||||
monthly
|
||||
<StyledWithViewportComponent
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"fieldName": "costType",
|
||||
"isResizable": true,
|
||||
"key": "costType",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "",
|
||||
"styles": Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
":hover": Object {
|
||||
"background": "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"fieldName": "hourly",
|
||||
"isResizable": true,
|
||||
"key": "hourly",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Hourly",
|
||||
"styles": Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
":hover": Object {
|
||||
"background": "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"fieldName": "daily",
|
||||
"isResizable": true,
|
||||
"key": "daily",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Daily",
|
||||
"styles": Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
":hover": Object {
|
||||
"background": "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"fieldName": "monthly",
|
||||
"isResizable": true,
|
||||
"key": "monthly",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Monthly",
|
||||
"styles": Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
":hover": Object {
|
||||
"background": "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
disableSelectionZone={true}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"costType": <Text>
|
||||
Current Cost
|
||||
</Text>,
|
||||
"daily": <Text>
|
||||
$
|
||||
|
||||
0.19
|
||||
</Text>,
|
||||
"hourly": <Text>
|
||||
$
|
||||
|
||||
0.0080
|
||||
</Text>,
|
||||
"monthly": <Text>
|
||||
$
|
||||
|
||||
5.84
|
||||
</Text>,
|
||||
},
|
||||
]
|
||||
}
|
||||
layoutMode={1}
|
||||
onRenderRow={[Function]}
|
||||
selectionMode={0}
|
||||
/>
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
>
|
||||
(
|
||||
regions:
|
||||
|
||||
</b>
|
||||
(
|
||||
regions:
|
||||
|
||||
1
|
||||
,
|
||||
100
|
||||
RU/s,
|
||||
$
|
||||
0.00008
|
||||
/RU)
|
||||
</Text>
|
||||
1
|
||||
,
|
||||
100
|
||||
RU/s,
|
||||
$
|
||||
0.00008
|
||||
/RU)
|
||||
</Text>
|
||||
<Text>
|
||||
<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>
|
||||
</Text>
|
||||
</Stack>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
id="spendAckCheckBox"
|
||||
@@ -288,6 +420,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
}
|
||||
}
|
||||
/>
|
||||
<br />
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -369,6 +502,19 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text>
|
||||
Estimate your required throughput with
|
||||
<StyledLinkBase
|
||||
href="https://cosmos.azure.com/capacitycalculator/"
|
||||
target="_blank"
|
||||
>
|
||||
capacity calculator
|
||||
|
||||
<Component
|
||||
iconName="NavigateExternalInline"
|
||||
/>
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
<StyledTextFieldBase
|
||||
disabled={false}
|
||||
id="throughputInput"
|
||||
@@ -394,38 +540,143 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
type="number"
|
||||
value="100"
|
||||
/>
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
Estimated cost (
|
||||
USD
|
||||
):
|
||||
|
||||
<b>
|
||||
$
|
||||
0.0080
|
||||
hourly
|
||||
/
|
||||
$
|
||||
0.19
|
||||
daily
|
||||
/
|
||||
$
|
||||
5.84
|
||||
monthly
|
||||
<StyledWithViewportComponent
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"fieldName": "costType",
|
||||
"isResizable": true,
|
||||
"key": "costType",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "",
|
||||
"styles": Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
":hover": Object {
|
||||
"background": "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"fieldName": "hourly",
|
||||
"isResizable": true,
|
||||
"key": "hourly",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Hourly",
|
||||
"styles": Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
":hover": Object {
|
||||
"background": "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"fieldName": "daily",
|
||||
"isResizable": true,
|
||||
"key": "daily",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Daily",
|
||||
"styles": Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
":hover": Object {
|
||||
"background": "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"fieldName": "monthly",
|
||||
"isResizable": true,
|
||||
"key": "monthly",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Monthly",
|
||||
"styles": Object {
|
||||
"root": Object {
|
||||
"selectors": Object {
|
||||
":hover": Object {
|
||||
"background": "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
disableSelectionZone={true}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"costType": <Text>
|
||||
Current Cost
|
||||
</Text>,
|
||||
"daily": <Text>
|
||||
$
|
||||
|
||||
0.19
|
||||
</Text>,
|
||||
"hourly": <Text>
|
||||
$
|
||||
|
||||
0.0080
|
||||
</Text>,
|
||||
"monthly": <Text>
|
||||
$
|
||||
|
||||
5.84
|
||||
</Text>,
|
||||
},
|
||||
]
|
||||
}
|
||||
layoutMode={1}
|
||||
onRenderRow={[Function]}
|
||||
selectionMode={0}
|
||||
/>
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
>
|
||||
(
|
||||
regions:
|
||||
|
||||
</b>
|
||||
(
|
||||
regions:
|
||||
|
||||
1
|
||||
,
|
||||
100
|
||||
RU/s,
|
||||
$
|
||||
0.00008
|
||||
/RU)
|
||||
</Text>
|
||||
1
|
||||
,
|
||||
100
|
||||
RU/s,
|
||||
$
|
||||
0.00008
|
||||
/RU)
|
||||
</Text>
|
||||
<Text>
|
||||
<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>
|
||||
</Text>
|
||||
</Stack>
|
||||
<br />
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
@@ -23,7 +23,8 @@ export const collection = ({
|
||||
autoscaleMaxThroughput: undefined,
|
||||
manualThroughput: 10000,
|
||||
minimumThroughput: 6000,
|
||||
id: "offer"
|
||||
id: "offer",
|
||||
offerReplacePending: false
|
||||
}),
|
||||
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
|
||||
{} as DataModels.ConflictResolutionPolicy
|
||||
|
||||
@@ -731,7 +731,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
"arcadiaToken": [Function],
|
||||
"armEndpoint": [Function],
|
||||
"browseQueriesPane": BrowseQueriesPane {
|
||||
"canSaveQueries": [Function],
|
||||
"container": [Circular],
|
||||
@@ -943,7 +942,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
@@ -1024,7 +1022,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
"onToggleKeyDown": [Function],
|
||||
"parentFrameDataExplorerVersion": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -1050,7 +1047,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"titleLabel": "Select Columns",
|
||||
"visible": [Function],
|
||||
},
|
||||
"quotaId": [Function],
|
||||
"refreshDatabaseAccount": [Function],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
@@ -2006,7 +2002,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
"arcadiaToken": [Function],
|
||||
"armEndpoint": [Function],
|
||||
"browseQueriesPane": BrowseQueriesPane {
|
||||
"canSaveQueries": [Function],
|
||||
"container": [Circular],
|
||||
@@ -2218,7 +2213,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
@@ -2299,7 +2293,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
"onToggleKeyDown": [Function],
|
||||
"parentFrameDataExplorerVersion": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -2325,7 +2318,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"titleLabel": "Select Columns",
|
||||
"visible": [Function],
|
||||
},
|
||||
"quotaId": [Function],
|
||||
"refreshDatabaseAccount": [Function],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
@@ -3294,7 +3286,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
"arcadiaToken": [Function],
|
||||
"armEndpoint": [Function],
|
||||
"browseQueriesPane": BrowseQueriesPane {
|
||||
"canSaveQueries": [Function],
|
||||
"container": [Circular],
|
||||
@@ -3506,7 +3497,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
@@ -3587,7 +3577,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
"onToggleKeyDown": [Function],
|
||||
"parentFrameDataExplorerVersion": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -3613,7 +3602,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"titleLabel": "Select Columns",
|
||||
"visible": [Function],
|
||||
},
|
||||
"quotaId": [Function],
|
||||
"refreshDatabaseAccount": [Function],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
@@ -4569,7 +4557,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
"arcadiaToken": [Function],
|
||||
"armEndpoint": [Function],
|
||||
"browseQueriesPane": BrowseQueriesPane {
|
||||
"canSaveQueries": [Function],
|
||||
"container": [Circular],
|
||||
@@ -4781,7 +4768,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
@@ -4862,7 +4848,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
"onToggleKeyDown": [Function],
|
||||
"parentFrameDataExplorerVersion": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -4888,7 +4873,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"titleLabel": "Select Columns",
|
||||
"visible": [Function],
|
||||
},
|
||||
"quotaId": [Function],
|
||||
"refreshDatabaseAccount": [Function],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
|
||||
@@ -60,66 +60,100 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
</StyledLinkBase>
|
||||
.
|
||||
</Text>
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
Estimated cost (
|
||||
RMB
|
||||
):
|
||||
|
||||
<b>
|
||||
¥
|
||||
1.02
|
||||
hourly
|
||||
/
|
||||
¥
|
||||
24.48
|
||||
daily
|
||||
/
|
||||
¥
|
||||
744.60
|
||||
monthly
|
||||
<StyledWithViewportComponent
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"fieldName": "costType",
|
||||
"isResizable": true,
|
||||
"key": "costType",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "hourly",
|
||||
"isResizable": true,
|
||||
"key": "hourly",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Hourly",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "daily",
|
||||
"isResizable": true,
|
||||
"key": "daily",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Daily",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "monthly",
|
||||
"isResizable": true,
|
||||
"key": "monthly",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Monthly",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableSelectionZone={true}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"costType": <Text>
|
||||
Current Cost
|
||||
</Text>,
|
||||
"daily": <Text>
|
||||
$ 24.48
|
||||
</Text>,
|
||||
"hourly": <Text>
|
||||
$ 1.02
|
||||
</Text>,
|
||||
"monthly": <Text>
|
||||
$ 744.6
|
||||
</Text>,
|
||||
},
|
||||
]
|
||||
}
|
||||
layoutMode={1}
|
||||
onRenderRow={[Function]}
|
||||
selectionMode={0}
|
||||
/>
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
>
|
||||
(
|
||||
regions:
|
||||
|
||||
</b>
|
||||
(
|
||||
regions:
|
||||
|
||||
2
|
||||
,
|
||||
1000
|
||||
RU/s,
|
||||
¥
|
||||
0.00051
|
||||
/RU)
|
||||
</Text>
|
||||
<Text
|
||||
id="autoscaleSpendElement"
|
||||
>
|
||||
Estimated monthly cost (
|
||||
RMB
|
||||
) is
|
||||
|
||||
<b>
|
||||
2
|
||||
,
|
||||
1000
|
||||
RU/s,
|
||||
¥
|
||||
111.69
|
||||
-
|
||||
¥
|
||||
1116.90
|
||||
|
||||
</b>
|
||||
(
|
||||
regions:
|
||||
|
||||
2
|
||||
,
|
||||
100
|
||||
-
|
||||
1000
|
||||
RU/s,
|
||||
¥
|
||||
0.000765
|
||||
/RU)
|
||||
</Text>
|
||||
0.00051
|
||||
/RU)
|
||||
</Text>
|
||||
<Text>
|
||||
<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>
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text
|
||||
id="manualToAutoscaleDisclaimerElement"
|
||||
styles={
|
||||
|
||||
@@ -126,6 +126,12 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<input
|
||||
data-bind="
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
jest.mock("../../Common/DocumentClientUtilityBase");
|
||||
jest.mock("../Graph/GraphExplorerComponent/GremlinClient");
|
||||
jest.mock("../../Common/dataAccess/createCollection");
|
||||
jest.mock("../../Common/dataAccess/createDocument");
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import Q from "q";
|
||||
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
|
||||
import { createDocument } from "../../Common/DocumentClientUtilityBase";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
import Explorer from "../Explorer";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import GraphTab from ".././Tabs/GraphTab";
|
||||
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../Explorer";
|
||||
import { createDocument } from "../../Common/DocumentClientUtilityBase";
|
||||
import { createCollection } from "../../Common/dataAccess/createCollection";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
interface SampleDataFile extends DataModels.CreateCollectionParams {
|
||||
@@ -95,12 +95,15 @@ export class ContainerSampleGenerator {
|
||||
.reduce((previous, current) => previous.then(current), Promise.resolve());
|
||||
} else {
|
||||
// For SQL all queries are executed at the same time
|
||||
this.sampleDataFile.data.map(doc => {
|
||||
const subPromise = createDocument(collection, doc);
|
||||
subPromise.catch(reason => NotificationConsoleUtils.logConsoleError(reason));
|
||||
promises.push(subPromise);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
await Promise.all(
|
||||
this.sampleDataFile.data.map(async doc => {
|
||||
try {
|
||||
await createDocument(collection, doc);
|
||||
} catch (error) {
|
||||
NotificationConsoleUtils.logConsoleError(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPa
|
||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
||||
import EnvironmentUtility from "../Common/EnvironmentUtility";
|
||||
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
|
||||
import GraphStylingPane from "./Panes/GraphStylingPane";
|
||||
import hasher from "hasher";
|
||||
import NewVertexPane from "./Panes/NewVertexPane";
|
||||
@@ -121,7 +121,6 @@ export default class Explorer {
|
||||
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
|
||||
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
|
||||
public subscriptionType: ko.Observable<SubscriptionType>;
|
||||
public quotaId: ko.Observable<string>;
|
||||
public defaultExperience: ko.Observable<string>;
|
||||
public isPreferredApiDocumentDB: ko.Computed<boolean>;
|
||||
public isPreferredApiCassandra: ko.Computed<boolean>;
|
||||
@@ -135,12 +134,10 @@ export default class Explorer {
|
||||
public canSaveQueries: ko.Computed<boolean>;
|
||||
public features: ko.Observable<any>;
|
||||
public serverId: ko.Observable<string>;
|
||||
public armEndpoint: ko.Observable<string>;
|
||||
public isTryCosmosDBSubscription: ko.Observable<boolean>;
|
||||
public queriesClient: QueriesClient;
|
||||
public tableDataClient: TableDataClient;
|
||||
public splitter: Splitter;
|
||||
public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>("");
|
||||
public mostRecentActivity: MostRecentActivity.MostRecentActivity;
|
||||
|
||||
// Notification Console
|
||||
@@ -204,7 +201,6 @@ export default class Explorer {
|
||||
|
||||
// features
|
||||
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||
public isCodeOfConductEnabled: ko.Computed<boolean>;
|
||||
public isLinkInjectionEnabled: ko.Computed<boolean>;
|
||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
@@ -279,7 +275,6 @@ export default class Explorer {
|
||||
|
||||
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
|
||||
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
|
||||
this.quotaId = ko.observable<string>("");
|
||||
let firstInitialization = true;
|
||||
this.isRefreshingExplorer = ko.observable<boolean>(true);
|
||||
this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => {
|
||||
@@ -319,9 +314,9 @@ export default class Explorer {
|
||||
if (isAccountReady) {
|
||||
this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true);
|
||||
RouteHandler.getInstance().initHandler();
|
||||
this.notebookWorkspaceManager = new NotebookWorkspaceManager(this.armEndpoint());
|
||||
this.notebookWorkspaceManager = new NotebookWorkspaceManager();
|
||||
this.arcadiaWorkspaces = ko.observableArray();
|
||||
this._arcadiaManager = new ArcadiaResourceManager(this.armEndpoint());
|
||||
this._arcadiaManager = new ArcadiaResourceManager();
|
||||
this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then(isRegistered =>
|
||||
this.hasStorageAnalyticsAfecFeature(isRegistered)
|
||||
);
|
||||
@@ -371,7 +366,6 @@ export default class Explorer {
|
||||
|
||||
this.features = ko.observable();
|
||||
this.serverId = ko.observable<string>();
|
||||
this.armEndpoint = ko.observable<string>(undefined);
|
||||
this.queriesClient = new QueriesClient(this);
|
||||
this.isTryCosmosDBSubscription = ko.observable<boolean>(false);
|
||||
|
||||
@@ -404,9 +398,6 @@ export default class Explorer {
|
||||
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
||||
);
|
||||
this.isCodeOfConductEnabled = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.enableCodeOfConduct)
|
||||
);
|
||||
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
||||
);
|
||||
@@ -1016,9 +1007,7 @@ export default class Explorer {
|
||||
this.isSynapseLinkUpdating(true);
|
||||
this._closeSynapseLinkModalDialog();
|
||||
|
||||
const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(
|
||||
this.databaseAccount().id
|
||||
);
|
||||
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(this.databaseAccount().id);
|
||||
|
||||
try {
|
||||
const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync(
|
||||
@@ -1758,61 +1747,59 @@ export default class Explorer {
|
||||
inputs.extensionEndpoint = configContext.PROXY_PATH;
|
||||
}
|
||||
|
||||
const initPromise: Q.Promise<void> = inputs ? this.initDataExplorerWithFrameInputs(inputs) : Q();
|
||||
this.initDataExplorerWithFrameInputs(inputs);
|
||||
|
||||
initPromise.then(() => {
|
||||
const openAction: ActionContracts.DataExplorerAction = message.openAction;
|
||||
if (!!openAction) {
|
||||
if (this.isRefreshingExplorer()) {
|
||||
const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => {
|
||||
handleOpenAction(openAction, this.nonSystemDatabases(), this);
|
||||
subscription.dispose();
|
||||
});
|
||||
} else {
|
||||
const openAction: ActionContracts.DataExplorerAction = message.openAction;
|
||||
if (!!openAction) {
|
||||
if (this.isRefreshingExplorer()) {
|
||||
const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => {
|
||||
handleOpenAction(openAction, this.nonSystemDatabases(), this);
|
||||
}
|
||||
subscription.dispose();
|
||||
});
|
||||
} else {
|
||||
handleOpenAction(openAction, this.nonSystemDatabases(), this);
|
||||
}
|
||||
if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
|
||||
handleCachedDataMessage(message);
|
||||
return;
|
||||
}
|
||||
if (message.type) {
|
||||
switch (message.type) {
|
||||
case MessageTypes.UpdateLocationHash:
|
||||
if (!message.locationHash) {
|
||||
break;
|
||||
}
|
||||
hasher.replaceHash(message.locationHash);
|
||||
RouteHandler.getInstance().parseHash(message.locationHash);
|
||||
break;
|
||||
case MessageTypes.SendNotification:
|
||||
if (!message.message) {
|
||||
break;
|
||||
}
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
message.consoleDataType || ConsoleDataType.Info,
|
||||
message.message,
|
||||
message.id
|
||||
);
|
||||
break;
|
||||
case MessageTypes.ClearNotification:
|
||||
if (!message.id) {
|
||||
break;
|
||||
}
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(message.id);
|
||||
break;
|
||||
case MessageTypes.LoadingStatus:
|
||||
if (!message.text) {
|
||||
break;
|
||||
}
|
||||
this._setLoadingStatusText(message.text, message.title);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
|
||||
handleCachedDataMessage(message);
|
||||
return;
|
||||
}
|
||||
if (message.type) {
|
||||
switch (message.type) {
|
||||
case MessageTypes.UpdateLocationHash:
|
||||
if (!message.locationHash) {
|
||||
break;
|
||||
}
|
||||
hasher.replaceHash(message.locationHash);
|
||||
RouteHandler.getInstance().parseHash(message.locationHash);
|
||||
break;
|
||||
case MessageTypes.SendNotification:
|
||||
if (!message.message) {
|
||||
break;
|
||||
}
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
message.consoleDataType || ConsoleDataType.Info,
|
||||
message.message,
|
||||
message.id
|
||||
);
|
||||
break;
|
||||
case MessageTypes.ClearNotification:
|
||||
if (!message.id) {
|
||||
break;
|
||||
}
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(message.id);
|
||||
break;
|
||||
case MessageTypes.LoadingStatus:
|
||||
if (!message.text) {
|
||||
break;
|
||||
}
|
||||
this._setLoadingStatusText(message.text, message.title);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.splashScreenAdapter.forceRender();
|
||||
});
|
||||
this.splashScreenAdapter.forceRender();
|
||||
}
|
||||
|
||||
public findSelectedDatabase(): ViewModels.Database {
|
||||
@@ -1852,8 +1839,14 @@ export default class Explorer {
|
||||
return false;
|
||||
}
|
||||
|
||||
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): Q.Promise<void> {
|
||||
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): void {
|
||||
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 masterKey = inputs.masterKey || "";
|
||||
const databaseAccount = inputs.databaseAccount || null;
|
||||
@@ -1862,25 +1855,18 @@ export default class Explorer {
|
||||
}
|
||||
this.features(inputs.features);
|
||||
this.serverId(inputs.serverId);
|
||||
this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || configContext.ARM_ENDPOINT));
|
||||
this.databaseAccount(databaseAccount);
|
||||
this.subscriptionType(inputs.subscriptionType);
|
||||
this.quotaId(inputs.quotaId);
|
||||
this.hasWriteAccess(inputs.hasWriteAccess);
|
||||
this.flight(inputs.addCollectionDefaultFlight);
|
||||
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription);
|
||||
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken);
|
||||
this.setFeatureFlagsFromFlights(inputs.flights);
|
||||
|
||||
if (!!inputs.dataExplorerVersion) {
|
||||
this.parentFrameDataExplorerVersion(inputs.dataExplorerVersion);
|
||||
}
|
||||
|
||||
this._importExplorerConfigComplete = true;
|
||||
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: inputs.extensionEndpoint || "",
|
||||
ARM_ENDPOINT: this.armEndpoint()
|
||||
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT)
|
||||
});
|
||||
|
||||
updateUserContext({
|
||||
@@ -1889,7 +1875,8 @@ export default class Explorer {
|
||||
databaseAccount,
|
||||
resourceGroup: inputs.resourceGroup,
|
||||
subscriptionId: inputs.subscriptionId,
|
||||
subscriptionType: inputs.subscriptionType
|
||||
subscriptionType: inputs.subscriptionType,
|
||||
quotaId: inputs.quotaId
|
||||
});
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.LoadDatabaseAccount,
|
||||
@@ -1903,7 +1890,6 @@ export default class Explorer {
|
||||
|
||||
this.isAccountReady(true);
|
||||
}
|
||||
return Q();
|
||||
}
|
||||
|
||||
public setFeatureFlagsFromFlights(flights: readonly string[]): void {
|
||||
@@ -2276,7 +2262,6 @@ export default class Explorer {
|
||||
name,
|
||||
content,
|
||||
parentDomElement,
|
||||
this.isCodeOfConductEnabled(),
|
||||
this.isLinkInjectionEnabled()
|
||||
);
|
||||
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
||||
@@ -2567,7 +2552,7 @@ export default class Explorer {
|
||||
|
||||
public _refreshSparkEnabledStateForAccount = async (): Promise<void> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const armEndpoint = this.armEndpoint();
|
||||
const armEndpoint = configContext.ARM_ENDPOINT;
|
||||
const authType = window.authType as AuthType;
|
||||
if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
|
||||
// explorer is not aware of the database account yet
|
||||
@@ -2576,7 +2561,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${Constants.AfecFeatures.Spark}`;
|
||||
const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(featureUri);
|
||||
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri);
|
||||
try {
|
||||
const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync(
|
||||
featureUri,
|
||||
@@ -2596,7 +2581,7 @@ export default class Explorer {
|
||||
|
||||
public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const armEndpoint = this.armEndpoint();
|
||||
const armEndpoint = configContext.ARM_ENDPOINT;
|
||||
const authType = window.authType as AuthType;
|
||||
if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
|
||||
// explorer is not aware of the database account yet
|
||||
@@ -2604,7 +2589,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${featureName}`;
|
||||
const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(featureUri);
|
||||
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri);
|
||||
try {
|
||||
const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync(
|
||||
featureUri,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
jest.mock("../../../Common/DocumentClientUtilityBase");
|
||||
jest.mock("../../../Common/dataAccess/queryDocuments");
|
||||
jest.mock("../../../Common/dataAccess/queryDocumentsPage");
|
||||
import React from "react";
|
||||
import * as sinon from "sinon";
|
||||
import { mount, ReactWrapper } from "enzyme";
|
||||
@@ -12,7 +13,8 @@ import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as StorageUtility from "../../../Shared/StorageUtility";
|
||||
import GraphTab from "../../Tabs/GraphTab";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
|
||||
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||
|
||||
describe("Check whether query result is vertex array", () => {
|
||||
it("should reject null as vertex array", () => {
|
||||
@@ -299,12 +301,12 @@ describe("GraphExplorer", () => {
|
||||
ignoreD3Update: boolean
|
||||
): GraphExplorer => {
|
||||
(queryDocuments as jest.Mock).mockImplementation((container: any, query: string, options: any) => {
|
||||
return Q.resolve({
|
||||
return {
|
||||
_query: query,
|
||||
nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {},
|
||||
hasMoreResults: () => false,
|
||||
executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {}
|
||||
});
|
||||
};
|
||||
});
|
||||
(queryDocumentsPage as jest.Mock).mockImplementation(
|
||||
(rid: string, iterator: any, firstItemIndex: number, options: any) => {
|
||||
|
||||
@@ -28,8 +28,10 @@ import * as Constants from "../../../Common/Constants";
|
||||
import { InputProperty } from "../../../Contracts/ViewModels";
|
||||
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
|
||||
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||
import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
|
||||
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
|
||||
export interface GraphAccessor {
|
||||
applyFilter: () => void;
|
||||
@@ -725,26 +727,32 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
/**
|
||||
* Execute DocDB query and get all results
|
||||
*/
|
||||
public executeNonPagedDocDbQuery(query: string): Q.Promise<DataModels.DocumentId[]> {
|
||||
// TODO maxItemCount: this reduces throttling, but won't cap the # of results
|
||||
return queryDocuments(this.props.databaseId, this.props.collectionId, query, {
|
||||
maxItemCount: GraphExplorer.PAGE_ALL,
|
||||
enableCrossPartitionQuery:
|
||||
StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.IsCrossPartitionQueryEnabled) ===
|
||||
"true"
|
||||
}).then(
|
||||
(iterator: QueryIterator<ItemDefinition & Resource>) => {
|
||||
return iterator.fetchNext().then(response => response.resources);
|
||||
},
|
||||
(reason: any) => {
|
||||
GraphExplorer.reportToConsole(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to execute non-paged query ${query}. Reason:${reason}`,
|
||||
reason
|
||||
);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
public async executeNonPagedDocDbQuery(query: string): Promise<DataModels.DocumentId[]> {
|
||||
try {
|
||||
// TODO maxItemCount: this reduces throttling, but won't cap the # of results
|
||||
const iterator: QueryIterator<ItemDefinition & Resource> = queryDocuments(
|
||||
this.props.databaseId,
|
||||
this.props.collectionId,
|
||||
query,
|
||||
{
|
||||
maxItemCount: GraphExplorer.PAGE_ALL,
|
||||
enableCrossPartitionQuery:
|
||||
StorageUtility.LocalStorageUtility.getEntryString(
|
||||
StorageUtility.StorageKey.IsCrossPartitionQueryEnabled
|
||||
) === "true"
|
||||
} as FeedOptions
|
||||
);
|
||||
const response = await iterator.fetchNext();
|
||||
|
||||
return response?.resources;
|
||||
} catch (error) {
|
||||
GraphExplorer.reportToConsole(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to execute non-paged query ${query}. Reason:${error}`,
|
||||
error
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -864,7 +872,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
/**
|
||||
* User executes query
|
||||
*/
|
||||
public submitQuery(query: string): void {
|
||||
public async submitQuery(query: string): Promise<void> {
|
||||
// Clear any progress indicator
|
||||
this.executeCounter = 0;
|
||||
this.setState({
|
||||
@@ -882,24 +890,22 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
// Remember query
|
||||
this.pushToLatestQueryFragments(query);
|
||||
|
||||
let backendPromise;
|
||||
|
||||
if (query.toLocaleLowerCase() === "g.V()".toLocaleLowerCase()) {
|
||||
backendPromise = this.executeDocDbGVQuery();
|
||||
} else {
|
||||
backendPromise = this.executeGremlinQuery(query);
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
try {
|
||||
let result: UserQueryResult;
|
||||
if (query.toLocaleLowerCase() === "g.V()".toLocaleLowerCase()) {
|
||||
result = await this.executeDocDbGVQuery();
|
||||
} else {
|
||||
result = await this.executeGremlinQuery(query);
|
||||
}
|
||||
);
|
||||
|
||||
this.queryTotalRequestCharge = result.requestCharge;
|
||||
} catch (error) {
|
||||
const errorMsg = `Failure in submitting query: ${query}: ${getErrorMessage(error)}`;
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
|
||||
this.setState({
|
||||
filterQueryError: errorMsg
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1390,7 +1396,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
/**
|
||||
* Update possible vertices to display in UI
|
||||
*/
|
||||
private updatePossibleVertices(): Q.Promise<PossibleVertex[]> {
|
||||
private updatePossibleVertices(): Promise<PossibleVertex[]> {
|
||||
const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null;
|
||||
|
||||
const q = `SELECT c.id, c["${this.props.graphConfigUiData.nodeCaptionChoice() ||
|
||||
@@ -1721,85 +1727,81 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
);
|
||||
}
|
||||
|
||||
private executeDocDbGVQuery(): Q.Promise<UserQueryResult> {
|
||||
private async executeDocDbGVQuery(): Promise<UserQueryResult> {
|
||||
let query = "select root.id from root where IS_DEFINED(root._isEdge) = false order by root._ts desc";
|
||||
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`;
|
||||
}
|
||||
|
||||
return queryDocuments(this.props.databaseId, this.props.collectionId, query, {
|
||||
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
|
||||
enableCrossPartitionQuery: LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
|
||||
})
|
||||
.then(
|
||||
(iterator: QueryIterator<ItemDefinition & Resource>) => {
|
||||
this.currentDocDBQueryInfo = {
|
||||
iterator: iterator,
|
||||
index: 0,
|
||||
query: query
|
||||
};
|
||||
},
|
||||
(reason: any) => {
|
||||
GraphExplorer.reportToConsole(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to execute CosmosDB query: ${query} reason:${reason}`
|
||||
);
|
||||
}
|
||||
)
|
||||
.then(() => this.loadMoreRootNodes());
|
||||
try {
|
||||
const iterator: QueryIterator<ItemDefinition & Resource> = queryDocuments(
|
||||
this.props.databaseId,
|
||||
this.props.collectionId,
|
||||
query,
|
||||
{
|
||||
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
|
||||
enableCrossPartitionQuery:
|
||||
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
|
||||
} as FeedOptions
|
||||
);
|
||||
this.currentDocDBQueryInfo = {
|
||||
iterator: iterator,
|
||||
index: 0,
|
||||
query: query
|
||||
};
|
||||
return await this.loadMoreRootNodes();
|
||||
} catch (error) {
|
||||
GraphExplorer.reportToConsole(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to execute CosmosDB query: ${query} reason:${error}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private loadMoreRootNodes(): Q.Promise<UserQueryResult> {
|
||||
private async loadMoreRootNodes(): Promise<UserQueryResult> {
|
||||
if (!this.currentDocDBQueryInfo) {
|
||||
return Q.resolve(null);
|
||||
return undefined;
|
||||
}
|
||||
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
|
||||
.currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE})`;
|
||||
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`);
|
||||
|
||||
return queryDocumentsPage(
|
||||
this.props.collectionId,
|
||||
this.currentDocDBQueryInfo.iterator,
|
||||
this.currentDocDBQueryInfo.index,
|
||||
{
|
||||
enableCrossPartitionQuery:
|
||||
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
|
||||
}
|
||||
)
|
||||
.then((results: ViewModels.QueryResults) => {
|
||||
GraphExplorer.clearConsoleProgress(id);
|
||||
this.currentDocDBQueryInfo.index = results.lastItemIndex + 1;
|
||||
this.setState({ hasMoreRoots: results.hasMoreResults });
|
||||
RU = results.requestCharge.toString();
|
||||
GraphExplorer.reportToConsole(
|
||||
ConsoleDataType.Info,
|
||||
`Executed: ${queryInfoStr} ${GremlinClient.GremlinClient.getRequestChargeString(RU)}`
|
||||
);
|
||||
const documents = results.documents || [];
|
||||
return documents.map(
|
||||
(item: DataModels.DocumentId) => {
|
||||
return GraphExplorer.getPkIdFromDocumentId(item, this.props.collectionPartitionKeyProperty);
|
||||
},
|
||||
(reason: any) => {
|
||||
// Failure
|
||||
GraphExplorer.clearConsoleProgress(id);
|
||||
const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${reason}`;
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
|
||||
this.setState({
|
||||
filterQueryError: errorMsg
|
||||
});
|
||||
this.setFilterQueryStatus(FilterQueryStatus.ErrorResult);
|
||||
throw reason;
|
||||
}
|
||||
);
|
||||
})
|
||||
.then((pkIds: string[]) => {
|
||||
const arg = pkIds.join(",");
|
||||
return this.executeGremlinQuery(`g.V(${arg})`);
|
||||
})
|
||||
.then(() => ({ requestCharge: RU }));
|
||||
try {
|
||||
const results: ViewModels.QueryResults = await queryDocumentsPage(
|
||||
this.props.collectionId,
|
||||
this.currentDocDBQueryInfo.iterator,
|
||||
this.currentDocDBQueryInfo.index
|
||||
);
|
||||
|
||||
GraphExplorer.clearConsoleProgress(id);
|
||||
this.currentDocDBQueryInfo.index = results.lastItemIndex + 1;
|
||||
this.setState({ hasMoreRoots: results.hasMoreResults });
|
||||
RU = results.requestCharge.toString();
|
||||
GraphExplorer.reportToConsole(
|
||||
ConsoleDataType.Info,
|
||||
`Executed: ${queryInfoStr} ${GremlinClient.GremlinClient.getRequestChargeString(RU)}`
|
||||
);
|
||||
const pkIds: string[] = (results.documents || []).map((item: DataModels.DocumentId) =>
|
||||
GraphExplorer.getPkIdFromDocumentId(item, this.props.collectionPartitionKeyProperty)
|
||||
);
|
||||
|
||||
const arg = pkIds.join(",");
|
||||
await this.executeGremlinQuery(`g.V(${arg})`);
|
||||
|
||||
return { requestCharge: RU };
|
||||
} catch (error) {
|
||||
GraphExplorer.clearConsoleProgress(id);
|
||||
const errorMsg = `Failed to query: ${this.currentDocDBQueryInfo.query}. Reason:${getErrorMessage(error)}`;
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
|
||||
this.setState({
|
||||
filterQueryError: errorMsg
|
||||
});
|
||||
this.setFilterQueryStatus(FilterQueryStatus.ErrorResult);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private executeGremlinQuery(query: string): Q.Promise<UserQueryResult> {
|
||||
|
||||
@@ -128,17 +128,9 @@ export default class NotebookManager {
|
||||
name: string,
|
||||
content: string | ImmutableNotebook,
|
||||
parentDomElement: HTMLElement,
|
||||
isCodeOfConductEnabled: boolean,
|
||||
isLinkInjectionEnabled: boolean
|
||||
): Promise<void> {
|
||||
await this.publishNotebookPaneAdapter.open(
|
||||
name,
|
||||
getFullName(),
|
||||
content,
|
||||
parentDomElement,
|
||||
isCodeOfConductEnabled,
|
||||
isLinkInjectionEnabled
|
||||
);
|
||||
await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement, isLinkInjectionEnabled);
|
||||
}
|
||||
|
||||
public openCopyNotebookPane(name: string, content: string): void {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
|
||||
import { createCollection } from "../../Common/dataAccess/createCollection";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
|
||||
isPreferredApiTable: ko.Computed<boolean>;
|
||||
@@ -668,7 +669,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
databaseId: this.databaseId()
|
||||
}),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||
throughput: this._getThroughput(),
|
||||
@@ -770,7 +771,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||
}),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||
throughput: offerThroughput,
|
||||
@@ -844,7 +845,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||
}),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||
throughput: offerThroughput,
|
||||
@@ -878,7 +879,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||
},
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||
throughput: offerThroughput,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { createDatabase } from "../../Common/dataAccess/createDatabase";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export default class AddDatabasePane extends ContextualPaneBase {
|
||||
public defaultExperience: ko.Computed<string>;
|
||||
@@ -250,7 +251,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
throughput: this.throughput(),
|
||||
flight: this.container.flight()
|
||||
@@ -278,7 +279,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
}),
|
||||
offerThroughput,
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
flight: this.container.flight()
|
||||
},
|
||||
@@ -342,7 +343,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
}),
|
||||
offerThroughput: offerThroughput,
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
flight: this.container.flight()
|
||||
},
|
||||
@@ -366,7 +367,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
}),
|
||||
offerThroughput: offerThroughput,
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
flight: this.container.flight()
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ import { HashMap } from "../../Common/HashMap";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
public createTableQuery: ko.Observable<string>;
|
||||
@@ -299,7 +300,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
databaseId: this.keyspaceId()
|
||||
}),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: "u",
|
||||
throughput: this.throughput(),
|
||||
@@ -353,7 +354,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
}),
|
||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: "u",
|
||||
throughput: this.throughput(),
|
||||
@@ -399,7 +400,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
}),
|
||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: "u",
|
||||
throughput: this.throughput(),
|
||||
@@ -429,7 +430,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
},
|
||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
defaultsCheck: {
|
||||
storage: "u",
|
||||
throughput: this.throughput(),
|
||||
|
||||
@@ -98,26 +98,21 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||
author: string,
|
||||
notebookContent: string | ImmutableNotebook,
|
||||
parentDomElement: HTMLElement,
|
||||
isCodeOfConductEnabled: boolean,
|
||||
isLinkInjectionEnabled: boolean
|
||||
): Promise<void> {
|
||||
if (isCodeOfConductEnabled) {
|
||||
try {
|
||||
const response = await this.junoClient.isCodeOfConductAccepted();
|
||||
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;
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
"PublishNotebookPaneAdapter/isCodeOfConductAccepted",
|
||||
"Failed to check if code of conduct was accepted"
|
||||
);
|
||||
try {
|
||||
const response = await this.junoClient.isCodeOfConductAccepted();
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
|
||||
}
|
||||
} else {
|
||||
this.isCodeOfConductAccepted = true;
|
||||
|
||||
this.isCodeOfConductAccepted = response.data;
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
"PublishNotebookPaneAdapter/isCodeOfConductAccepted",
|
||||
"Failed to check if code of conduct was accepted"
|
||||
);
|
||||
}
|
||||
|
||||
this.name = name;
|
||||
|
||||
@@ -421,53 +421,47 @@ 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.
|
||||
* See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx
|
||||
*/
|
||||
private prefetchData(
|
||||
private async prefetchData(
|
||||
tableQuery: Entities.ITableQuery,
|
||||
downloadSize: number,
|
||||
currentRetry: number = 0
|
||||
): Q.Promise<any> {
|
||||
): Promise<IListTableEntitiesSegmentedResult> {
|
||||
if (!this.cache.serverCallInProgress) {
|
||||
this.cache.serverCallInProgress = true;
|
||||
this.allDownloaded = false;
|
||||
this.lastPrefetchTime = new Date().getTime();
|
||||
var time = this.lastPrefetchTime;
|
||||
const time = this.lastPrefetchTime;
|
||||
|
||||
var promise: Q.Promise<IListTableEntitiesSegmentedResult>;
|
||||
if (this._documentIterator && this.continuationToken) {
|
||||
// TODO handle Cassandra case
|
||||
const response = await this._documentIterator.fetchNext();
|
||||
const entities: Entities.ITableEntity[] = TableEntityProcessor.convertDocumentsToEntities(response?.resources);
|
||||
|
||||
promise = Q(this._documentIterator.fetchNext().then(response => response.resources)).then(
|
||||
(documents: any[]) => {
|
||||
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 {
|
||||
Results: entities,
|
||||
ContinuationToken: this._documentIterator.hasMoreResults()
|
||||
};
|
||||
}
|
||||
return promise
|
||||
.then((result: IListTableEntitiesSegmentedResult) => {
|
||||
|
||||
try {
|
||||
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) {
|
||||
this._documentIterator = result.iterator;
|
||||
this._documentIterator = documents.iterator;
|
||||
}
|
||||
var actualDownloadSize: number = 0;
|
||||
|
||||
@@ -478,11 +472,11 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
||||
return Q.resolve(null);
|
||||
}
|
||||
|
||||
var entities = result.Results;
|
||||
var entities = documents.Results;
|
||||
actualDownloadSize = entities.length;
|
||||
|
||||
// Queries can fetch no results and still return a continuation header. See prefetchAndRender() method.
|
||||
this.continuationToken = this.isCancelled ? null : result.ContinuationToken;
|
||||
this.continuationToken = this.isCancelled ? null : documents.ContinuationToken;
|
||||
|
||||
if (!this.continuationToken) {
|
||||
this.allDownloaded = true;
|
||||
@@ -514,20 +508,22 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
||||
// For #2.1, set prefetch exceeds maximum retry number and end prefetch.
|
||||
// For #2.2, go to next round prefetch.
|
||||
if (this.allDownloaded || nextDownloadSize === 0) {
|
||||
return Q.resolve(result);
|
||||
return documents;
|
||||
}
|
||||
|
||||
if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) {
|
||||
result.ExceedMaximumRetries = true;
|
||||
return Q.resolve(result);
|
||||
documents.ExceedMaximumRetries = true;
|
||||
return documents;
|
||||
}
|
||||
return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
this.cache.serverCallInProgress = false;
|
||||
return Q.reject(error);
|
||||
});
|
||||
|
||||
return await this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
|
||||
}
|
||||
} catch (error) {
|
||||
this.cache.serverCallInProgress = false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import Q from "q";
|
||||
import { displayTokenRenewalPromptForStatus, getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import * as Entities from "./Entities";
|
||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||
@@ -12,9 +13,12 @@ import * as TableConstants from "./Constants";
|
||||
import * as TableEntityProcessor from "./TableEntityProcessor";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import Explorer from "../Explorer";
|
||||
import { queryDocuments, deleteDocument, updateDocument, createDocument } from "../../Common/DocumentClientUtilityBase";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
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 {
|
||||
partitionKeys: CassandraTableKey[];
|
||||
@@ -38,19 +42,19 @@ export abstract class TableDataClient {
|
||||
collection: ViewModels.Collection,
|
||||
originalDocument: any,
|
||||
newEntity: Entities.ITableEntity
|
||||
): Q.Promise<Entities.ITableEntity>;
|
||||
): Promise<Entities.ITableEntity>;
|
||||
|
||||
public abstract queryDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
query: string,
|
||||
shouldNotify?: boolean,
|
||||
paginationToken?: string
|
||||
): Q.Promise<Entities.IListTableEntitiesResult>;
|
||||
): Promise<Entities.IListTableEntitiesResult>;
|
||||
|
||||
public abstract deleteDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
entitiesToDelete: Entities.ITableEntity[]
|
||||
): Q.Promise<any>;
|
||||
): Promise<any>;
|
||||
}
|
||||
|
||||
export class TablesAPIDataClient extends TableDataClient {
|
||||
@@ -74,77 +78,63 @@ export class TablesAPIDataClient extends TableDataClient {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public updateDocument(
|
||||
public async updateDocument(
|
||||
collection: ViewModels.Collection,
|
||||
originalDocument: any,
|
||||
entity: Entities.ITableEntity
|
||||
): Q.Promise<Entities.ITableEntity> {
|
||||
const deferred = Q.defer<Entities.ITableEntity>();
|
||||
|
||||
updateDocument(
|
||||
collection,
|
||||
originalDocument,
|
||||
TableEntityProcessor.convertEntityToNewDocument(<Entities.ITableEntityForTablesAPI>entity)
|
||||
).then(
|
||||
(newDocument: any) => {
|
||||
const newEntity = TableEntityProcessor.convertDocumentsToEntities([newDocument])[0];
|
||||
deferred.resolve(newEntity);
|
||||
},
|
||||
reason => {
|
||||
deferred.reject(reason);
|
||||
}
|
||||
);
|
||||
return deferred.promise;
|
||||
): Promise<Entities.ITableEntity> {
|
||||
try {
|
||||
const newDocument = await updateDocument(
|
||||
collection,
|
||||
originalDocument,
|
||||
TableEntityProcessor.convertEntityToNewDocument(<Entities.ITableEntityForTablesAPI>entity)
|
||||
);
|
||||
return TableEntityProcessor.convertDocumentsToEntities([newDocument])[0];
|
||||
} catch (error) {
|
||||
handleError(error, "TablesAPIDataClient/updateDocument");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public queryDocuments(
|
||||
public async queryDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
query: string
|
||||
): Q.Promise<Entities.IListTableEntitiesResult> {
|
||||
const deferred = Q.defer<Entities.IListTableEntitiesResult>();
|
||||
): Promise<Entities.IListTableEntitiesResult> {
|
||||
try {
|
||||
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);
|
||||
|
||||
let options: any = {};
|
||||
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
|
||||
queryDocuments(collection.databaseId, collection.id(), query, options).then(
|
||||
iterator => {
|
||||
iterator
|
||||
.fetchNext()
|
||||
.then(response => response.resources)
|
||||
.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;
|
||||
return {
|
||||
Results: entities,
|
||||
ContinuationToken: iterator.hasMoreResults(),
|
||||
iterator: iterator
|
||||
};
|
||||
} catch (error) {
|
||||
handleError(error, "TablesAPIDataClient/queryDocuments", "Query documents failed");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise<any> {
|
||||
let documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments(
|
||||
public async deleteDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
entitiesToDelete: Entities.ITableEntity[]
|
||||
): Promise<any> {
|
||||
const documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments(
|
||||
<Entities.ITableEntityForTablesAPI[]>entitiesToDelete,
|
||||
collection
|
||||
);
|
||||
let promiseArray: Q.Promise<any>[] = [];
|
||||
documentsToDelete &&
|
||||
documentsToDelete.forEach(document => {
|
||||
|
||||
await Promise.all(
|
||||
documentsToDelete?.map(async document => {
|
||||
document.id = ko.observable<string>(document.id);
|
||||
let promise: Q.Promise<any> = deleteDocument(collection, document);
|
||||
promiseArray.push(promise);
|
||||
});
|
||||
return Q.all(promiseArray);
|
||||
await deleteDocument(collection, document);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,10 +170,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
(data: any) => {
|
||||
entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)];
|
||||
entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString();
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully added new row to table ${collection.id()}`
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleInfo(`Successfully added new row to table ${collection.id()}`);
|
||||
deferred.resolve(entity);
|
||||
},
|
||||
error => {
|
||||
@@ -197,181 +184,159 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public updateDocument(
|
||||
public async updateDocument(
|
||||
collection: ViewModels.Collection,
|
||||
originalDocument: any,
|
||||
newEntity: Entities.ITableEntity
|
||||
): Q.Promise<Entities.ITableEntity> {
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Updating row ${originalDocument.RowKey._}`
|
||||
);
|
||||
const deferred = Q.defer<Entities.ITableEntity>();
|
||||
let promiseArray: Q.Promise<any>[] = [];
|
||||
let query = `UPDATE ${collection.databaseId}.${collection.id()}`;
|
||||
let isChange: boolean = false;
|
||||
for (let property in newEntity) {
|
||||
if (!originalDocument[property] || newEntity[property]._.toString() !== originalDocument[property]._.toString()) {
|
||||
if (this.isStringType(newEntity[property].$)) {
|
||||
query = `${query} SET ${property} = '${newEntity[property]._}',`;
|
||||
} else {
|
||||
query = `${query} SET ${property} = ${newEntity[property]._},`;
|
||||
): Promise<Entities.ITableEntity> {
|
||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Updating row ${originalDocument.RowKey._}`);
|
||||
|
||||
try {
|
||||
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;
|
||||
whereSegment += this.isStringType(keyType)
|
||||
? ` ${key} = '${newEntity[key]._}' AND`
|
||||
: ` ${key} = ${newEntity[key]._} AND`;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
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`;
|
||||
|
||||
if (isPropertyUpdated) {
|
||||
updateQuery = updateQuery.slice(0, updateQuery.length - 1);
|
||||
updateQuery += whereSegment;
|
||||
await this.queryDocuments(collection, updateQuery);
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
let deleteQuery = `DELETE `;
|
||||
let isPropertyDeleted = false;
|
||||
for (let property in originalDocument) {
|
||||
if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) {
|
||||
deleteQuery += ` ${property},`;
|
||||
isPropertyDeleted = true;
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
public queryDocuments(
|
||||
public async queryDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
query: string,
|
||||
shouldNotify?: boolean,
|
||||
paginationToken?: string
|
||||
): Q.Promise<Entities.IListTableEntitiesResult> {
|
||||
let notificationId: string;
|
||||
if (shouldNotify) {
|
||||
notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Querying rows for table ${collection.id()}`
|
||||
);
|
||||
}
|
||||
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);
|
||||
): Promise<Entities.IListTableEntitiesResult> {
|
||||
const clearMessage =
|
||||
shouldNotify && NotificationConsoleUtils.logConsoleProgress(`Querying rows for table ${collection.id()}`);
|
||||
try {
|
||||
const authType = window.authType;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestQueryApi
|
||||
: Constants.CassandraBackend.queryApi;
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
|
||||
const response = await fetch(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
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
|
||||
}),
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[Constants.HttpHeaders.contentType]: "application/json"
|
||||
}
|
||||
});
|
||||
return deferred.promise;
|
||||
|
||||
if (!response.ok) {
|
||||
displayTokenRenewalPromptForStatus(response.status);
|
||||
throw Error(`Failed to query rows for table ${collection.id()}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
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?.();
|
||||
}
|
||||
}
|
||||
|
||||
public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise<any> {
|
||||
public async deleteDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
entitiesToDelete: Entities.ITableEntity[]
|
||||
): Promise<any> {
|
||||
const query = `DELETE FROM ${collection.databaseId}.${collection.id()} WHERE `;
|
||||
let promiseArray: Q.Promise<any>[] = [];
|
||||
let partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection);
|
||||
for (let i = 0, len = entitiesToDelete.length; i < len; i++) {
|
||||
let currEntityToDelete: Entities.ITableEntity = entitiesToDelete[i];
|
||||
let currQuery = query;
|
||||
let partitionKeyValue = currEntityToDelete[partitionKeyProperty];
|
||||
if (partitionKeyValue._ != null && this.isStringType(partitionKeyValue.$)) {
|
||||
currQuery = `${currQuery}${partitionKeyProperty} = '${partitionKeyValue._}' AND `;
|
||||
} else {
|
||||
currQuery = `${currQuery}${partitionKeyProperty} = ${partitionKeyValue._} AND `;
|
||||
}
|
||||
currQuery = currQuery.slice(0, currQuery.length - 5);
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Deleting row ${currEntityToDelete.RowKey._}`
|
||||
);
|
||||
promiseArray.push(
|
||||
this.queryDocuments(collection, currQuery)
|
||||
.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);
|
||||
const partitionKeyProperty = this.getCassandraPartitionKeyProperty(collection);
|
||||
|
||||
await Promise.all(
|
||||
entitiesToDelete.map(async (currEntityToDelete: Entities.ITableEntity) => {
|
||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting row ${currEntityToDelete.RowKey._}`);
|
||||
const partitionKeyValue = currEntityToDelete[partitionKeyProperty];
|
||||
const currQuery =
|
||||
query + this.isStringType(partitionKeyValue.$)
|
||||
? `${partitionKeyProperty} = '${partitionKeyValue._}'`
|
||||
: `${partitionKeyProperty} = ${partitionKeyValue._}`;
|
||||
|
||||
try {
|
||||
await this.queryDocuments(collection, currQuery);
|
||||
NotificationConsoleUtils.logConsoleInfo(`Successfully deleted row ${currEntityToDelete.RowKey._}`);
|
||||
} catch (error) {
|
||||
handleError(error, "DeleteRowCassandra", `Error while deleting row ${currEntityToDelete.RowKey._}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public createKeyspace(
|
||||
@@ -495,9 +460,9 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||
public async getTableKeys(collection: ViewModels.Collection): Promise<CassandraTableKeys> {
|
||||
if (!!collection.cassandraKeys) {
|
||||
return Q.resolve(collection.cassandraKeys);
|
||||
return collection.cassandraKeys;
|
||||
}
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
@@ -509,45 +474,51 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
? Constants.CassandraBackend.guestKeysApi
|
||||
: Constants.CassandraBackend.keysApi;
|
||||
let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`;
|
||||
const deferred = Q.defer<CassandraTableKeys>();
|
||||
$.ajax(endpoint, {
|
||||
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()
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader,
|
||||
error: this.handleAjaxError,
|
||||
cache: false
|
||||
})
|
||||
.then(
|
||||
(data: CassandraTableKeys) => {
|
||||
collection.cassandraKeys = data;
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully fetched keys for table ${collection.id()}`
|
||||
);
|
||||
deferred.resolve(data);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||
deferred.reject(error);
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
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()
|
||||
}),
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[Constants.HttpHeaders.contentType]: "application/json"
|
||||
}
|
||||
)
|
||||
.done(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
});
|
||||
return deferred.promise;
|
||||
|
||||
if (!response.ok) {
|
||||
displayTokenRenewalPromptForStatus(response.status);
|
||||
throw Error(`Fetching keys for table ${collection.id()} failed`);
|
||||
}
|
||||
|
||||
const data: CassandraTableKeys = await response.json();
|
||||
collection.cassandraKeys = data;
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully fetched keys for table ${collection.id()}`
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
handleError(error, "FetchKeysCassandra", `Error fetching keys for table ${collection.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
}
|
||||
}
|
||||
|
||||
public getTableSchema(collection: ViewModels.Collection): Q.Promise<CassandraTableKey[]> {
|
||||
public async getTableSchema(collection: ViewModels.Collection): Promise<CassandraTableKey[]> {
|
||||
if (!!collection.cassandraSchema) {
|
||||
return Q.resolve(collection.cassandraSchema);
|
||||
return collection.cassandraSchema;
|
||||
}
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
@@ -559,74 +530,79 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
? Constants.CassandraBackend.guestSchemaApi
|
||||
: Constants.CassandraBackend.schemaApi;
|
||||
let endpoint = `${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`;
|
||||
const deferred = Q.defer<CassandraTableKey[]>();
|
||||
$.ajax(endpoint, {
|
||||
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()
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader,
|
||||
error: this.handleAjaxError,
|
||||
cache: false
|
||||
})
|
||||
.then(
|
||||
(data: any) => {
|
||||
collection.cassandraSchema = data.columns;
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully fetched schema for table ${collection.id()}`
|
||||
);
|
||||
deferred.resolve(data.columns);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||
deferred.reject(error);
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
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()
|
||||
}),
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[Constants.HttpHeaders.contentType]: "application/json"
|
||||
}
|
||||
)
|
||||
.done(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
});
|
||||
return deferred.promise;
|
||||
|
||||
if (!response.ok) {
|
||||
displayTokenRenewalPromptForStatus(response.status);
|
||||
throw Error(`Failed to fetch schema for table ${collection.id()}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
collection.cassandraSchema = data.columns;
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully fetched schema for table ${collection.id()}`
|
||||
);
|
||||
|
||||
return data.columns;
|
||||
} catch (error) {
|
||||
handleError(error, "FetchSchemaCassandra", `Error fetching schema for table ${collection.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
}
|
||||
}
|
||||
|
||||
private createOrDeleteQuery(
|
||||
private async createOrDeleteQuery(
|
||||
cassandraEndpoint: string,
|
||||
resourceId: string,
|
||||
query: string,
|
||||
explorer: Explorer
|
||||
): Q.Promise<any> {
|
||||
const deferred = Q.defer();
|
||||
): Promise<void> {
|
||||
const authType = window.authType;
|
||||
const apiEndpoint: string =
|
||||
authType === AuthType.EncryptedToken
|
||||
? Constants.CassandraBackend.guestCreateOrDeleteApi
|
||||
: Constants.CassandraBackend.createOrDeleteApi;
|
||||
$.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
|
||||
const response = await fetch(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
accountName: explorer.databaseAccount() && explorer.databaseAccount().name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint),
|
||||
resourceId: resourceId,
|
||||
query: query
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader,
|
||||
error: this.handleAjaxError,
|
||||
cache: false
|
||||
}).then(
|
||||
(data: any) => {
|
||||
deferred.resolve();
|
||||
},
|
||||
reason => {
|
||||
deferred.reject(reason);
|
||||
resourceId,
|
||||
query
|
||||
}),
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[Constants.HttpHeaders.contentType]: "application/json"
|
||||
}
|
||||
);
|
||||
return deferred.promise;
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
displayTokenRenewalPromptForStatus(response.status);
|
||||
throw Error(`Failed to create or delete keyspace/table`);
|
||||
}
|
||||
}
|
||||
|
||||
private trimCassandraEndpoint(cassandraEndpoint: string): string {
|
||||
@@ -645,13 +621,6 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
return cassandraEndpoint;
|
||||
}
|
||||
|
||||
private setAuthorizationHeader: (xhr: XMLHttpRequest) => boolean = (xhr: XMLHttpRequest): boolean => {
|
||||
const authorizationHeaderMetadata: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||
xhr.setRequestHeader(authorizationHeaderMetadata.header, authorizationHeaderMetadata.token);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
private isStringType(dataType: string): boolean {
|
||||
// TODO figure out rest of types that are considered strings by Cassandra (if any have been missed)
|
||||
return (
|
||||
@@ -665,12 +634,4 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
private getCassandraPartitionKeyProperty(collection: ViewModels.Collection): string {
|
||||
return collection.cassandraKeys.partitionKeys[0].property;
|
||||
}
|
||||
|
||||
private handleAjaxError = (xhrObj: XMLHttpRequest, textStatus: string, errorThrown: string): void => {
|
||||
if (!xhrObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
displayTokenRenewalPromptForStatus(xhrObj.status);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,18 +16,16 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
import DiscardIcon from "../../../images/discard.svg";
|
||||
import DeleteIcon from "../../../images/delete.svg";
|
||||
import { QueryIterator, ItemDefinition, Resource, ConflictDefinition } from "@azure/cosmos";
|
||||
import { QueryIterator, Resource, ConflictDefinition, FeedOptions } from "@azure/cosmos";
|
||||
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
|
||||
import Explorer from "../Explorer";
|
||||
import {
|
||||
queryConflicts,
|
||||
deleteConflict,
|
||||
deleteDocument,
|
||||
createDocument,
|
||||
updateDocument
|
||||
} from "../../Common/DocumentClientUtilityBase";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
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 {
|
||||
public selectedConflictId: ko.Observable<ConflictId>;
|
||||
@@ -225,25 +223,15 @@ export default class ConflictsTab extends TabsBase {
|
||||
});
|
||||
}
|
||||
|
||||
public refreshDocumentsGrid(): Q.Promise<any> {
|
||||
// clear documents grid
|
||||
this.conflictIds([]);
|
||||
return this.createIterator()
|
||||
.then(
|
||||
// reset iterator
|
||||
iterator => {
|
||||
this._documentsIterator = iterator;
|
||||
}
|
||||
)
|
||||
.then(
|
||||
// load documents
|
||||
() => {
|
||||
return this.loadNextPage();
|
||||
}
|
||||
)
|
||||
.catch(error => {
|
||||
window.alert(getErrorMessage(error));
|
||||
});
|
||||
public async refreshDocumentsGrid(): Promise<void> {
|
||||
try {
|
||||
// clear documents grid
|
||||
this.conflictIds([]);
|
||||
this._documentsIterator = this.createIterator();
|
||||
await this.loadNextPage();
|
||||
} catch (error) {
|
||||
window.alert(getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
@@ -265,9 +253,9 @@ export default class ConflictsTab extends TabsBase {
|
||||
return Q();
|
||||
}
|
||||
|
||||
public onAcceptChangesClick = (): Q.Promise<any> => {
|
||||
public onAcceptChangesClick = async (): Promise<void> => {
|
||||
if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) {
|
||||
return Q();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isExecutionError(false);
|
||||
@@ -285,81 +273,79 @@ export default class ConflictsTab extends TabsBase {
|
||||
conflictResourceId: selectedConflict.resourceId
|
||||
});
|
||||
|
||||
let operationPromise: Q.Promise<any> = Q();
|
||||
if (selectedConflict.operationType === Constants.ConflictOperationType.Replace) {
|
||||
const documentContent = JSON.parse(this.selectedConflictContent());
|
||||
try {
|
||||
if (selectedConflict.operationType === Constants.ConflictOperationType.Replace) {
|
||||
const documentContent = JSON.parse(this.selectedConflictContent());
|
||||
|
||||
operationPromise = updateDocument(
|
||||
this.collection,
|
||||
selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]),
|
||||
documentContent
|
||||
);
|
||||
}
|
||||
await updateDocument(
|
||||
this.collection,
|
||||
selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]),
|
||||
documentContent
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedConflict.operationType === Constants.ConflictOperationType.Create) {
|
||||
const documentContent = JSON.parse(this.selectedConflictContent());
|
||||
if (selectedConflict.operationType === Constants.ConflictOperationType.Create) {
|
||||
const documentContent = JSON.parse(this.selectedConflictContent());
|
||||
|
||||
operationPromise = createDocument(this.collection, documentContent);
|
||||
}
|
||||
await createDocument(this.collection, documentContent);
|
||||
}
|
||||
|
||||
if (selectedConflict.operationType === Constants.ConflictOperationType.Delete && !!this.selectedConflictContent()) {
|
||||
const documentContent = JSON.parse(this.selectedConflictContent());
|
||||
if (
|
||||
selectedConflict.operationType === Constants.ConflictOperationType.Delete &&
|
||||
!!this.selectedConflictContent()
|
||||
) {
|
||||
const documentContent = JSON.parse(this.selectedConflictContent());
|
||||
|
||||
operationPromise = deleteDocument(
|
||||
this.collection,
|
||||
selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty])
|
||||
);
|
||||
}
|
||||
await 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
|
||||
);
|
||||
});
|
||||
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
|
||||
},
|
||||
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));
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
public onDeleteClick = (): Q.Promise<any> => {
|
||||
public onDeleteClick = async (): Promise<void> => {
|
||||
this.isExecutionError(false);
|
||||
this.isExecuting(true);
|
||||
|
||||
@@ -375,50 +361,48 @@ export default class ConflictsTab extends TabsBase {
|
||||
conflictResourceId: selectedConflict.resourceId
|
||||
});
|
||||
|
||||
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.DeleteConflict,
|
||||
{
|
||||
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
|
||||
);
|
||||
try {
|
||||
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.DeleteConflict,
|
||||
{
|
||||
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 => {
|
||||
this.isExecutionError(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
window.alert(errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.DeleteConflict,
|
||||
{
|
||||
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));
|
||||
startKey
|
||||
);
|
||||
} catch (error) {
|
||||
this.isExecutionError(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
window.alert(errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.DeleteConflict,
|
||||
{
|
||||
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 onDiscardClick = (): Q.Promise<any> => {
|
||||
@@ -445,60 +429,47 @@ export default class ConflictsTab extends TabsBase {
|
||||
return Q();
|
||||
}
|
||||
|
||||
public onTabClick(): Q.Promise<any> {
|
||||
return super.onTabClick().then(() => {
|
||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts);
|
||||
});
|
||||
public onTabClick(): void {
|
||||
super.onTabClick();
|
||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts);
|
||||
}
|
||||
|
||||
public onActivate(): Q.Promise<any> {
|
||||
return super.onActivate().then(() => {
|
||||
if (this._documentsIterator) {
|
||||
return Q.resolve(this._documentsIterator);
|
||||
}
|
||||
public async onActivate(): Promise<void> {
|
||||
super.onActivate();
|
||||
|
||||
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;
|
||||
}
|
||||
if (!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;
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>> {
|
||||
public createIterator(): QueryIterator<ConflictDefinition & Resource> {
|
||||
// TODO: Conflict Feed does not allow filtering atm
|
||||
const query: string = undefined;
|
||||
let options: any = {};
|
||||
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
|
||||
return queryConflicts(this.collection.databaseId, this.collection.id(), query, options);
|
||||
const options = {
|
||||
enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey()
|
||||
};
|
||||
return queryConflicts(this.collection.databaseId, this.collection.id(), query, options as FeedOptions);
|
||||
}
|
||||
|
||||
public loadNextPage(): Q.Promise<any> {
|
||||
|
||||
@@ -230,9 +230,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
return this.throughputTitle() + this.requestUnitsUsageCost();
|
||||
});
|
||||
this.pendingNotification = ko.observable<DataModels.Notification>();
|
||||
this._offerReplacePending = ko.observable<boolean>(
|
||||
!!this.database.offer()?.headers?.[Constants.HttpHeaders.offerReplacePending]
|
||||
);
|
||||
this._offerReplacePending = ko.observable<boolean>(!!this.database.offer()?.offerReplacePending);
|
||||
this.notificationStatusInfo = ko.observable<string>("");
|
||||
this.shouldShowNotificationStatusPrompt = ko.computed<boolean>(() => this.notificationStatusInfo().length > 0);
|
||||
this.warningMessage = ko.computed<string>(() => {
|
||||
@@ -241,7 +239,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
}
|
||||
|
||||
const offer = this.database.offer();
|
||||
if (offer?.headers?.[Constants.HttpHeaders.offerReplacePending]) {
|
||||
if (offer?.offerReplacePending) {
|
||||
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
||||
return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id());
|
||||
}
|
||||
@@ -431,11 +429,10 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
return Q();
|
||||
};
|
||||
|
||||
public onActivate(): Q.Promise<any> {
|
||||
return super.onActivate().then(async () => {
|
||||
this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
|
||||
await this.database.loadOffer();
|
||||
});
|
||||
public async onActivate(): Promise<void> {
|
||||
super.onActivate();
|
||||
this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
|
||||
await this.database.loadOffer();
|
||||
}
|
||||
|
||||
private _setBaseline() {
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
<button
|
||||
class="filterbtnstyle queryButton"
|
||||
data-bind="
|
||||
click: onApplyFilterClick,
|
||||
click: refreshDocumentsGrid,
|
||||
enable: applyFilterButton.enabled"
|
||||
aria-label="Apply filter"
|
||||
tabindex="0"
|
||||
|
||||
@@ -19,19 +19,24 @@ import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
import DiscardIcon from "../../../images/discard.svg";
|
||||
import DeleteDocumentIcon from "../../../images/DeleteDocument.svg";
|
||||
import UploadIcon from "../../../images/Upload_16x16.svg";
|
||||
import { extractPartitionKey, PartitionKeyDefinition, QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
|
||||
import {
|
||||
extractPartitionKey,
|
||||
PartitionKeyDefinition,
|
||||
QueryIterator,
|
||||
ItemDefinition,
|
||||
Resource,
|
||||
Item
|
||||
} from "@azure/cosmos";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../Explorer";
|
||||
import {
|
||||
readDocument,
|
||||
queryDocuments,
|
||||
deleteDocument,
|
||||
updateDocument,
|
||||
createDocument
|
||||
} from "../../Common/DocumentClientUtilityBase";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
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 {
|
||||
public selectedDocumentId: ko.Observable<DocumentId>;
|
||||
@@ -369,36 +374,22 @@ export default class DocumentsTab extends TabsBase {
|
||||
return true;
|
||||
};
|
||||
|
||||
public onApplyFilterClick(): Q.Promise<any> {
|
||||
public async refreshDocumentsGrid(): Promise<void> {
|
||||
// clear documents grid
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
public refreshDocumentsGrid(): Q.Promise<any> {
|
||||
return this.onApplyFilterClick();
|
||||
try {
|
||||
// reset iterator
|
||||
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 => {
|
||||
@@ -434,7 +425,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
return Q();
|
||||
};
|
||||
|
||||
public onSaveNewDocumentClick = (): Q.Promise<any> => {
|
||||
public onSaveNewDocumentClick = (): Promise<any> => {
|
||||
this.isExecutionError(false);
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
|
||||
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
|
||||
@@ -502,7 +493,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
return Q();
|
||||
};
|
||||
|
||||
public onSaveExisitingDocumentClick = (): Q.Promise<any> => {
|
||||
public onSaveExisitingDocumentClick = (): Promise<any> => {
|
||||
const selectedDocumentId = this.selectedDocumentId();
|
||||
const documentContent = JSON.parse(this.selectedDocumentContent());
|
||||
|
||||
@@ -571,17 +562,15 @@ export default class DocumentsTab extends TabsBase {
|
||||
return Q();
|
||||
};
|
||||
|
||||
public onDeleteExisitingDocumentClick = (): Q.Promise<any> => {
|
||||
public onDeleteExisitingDocumentClick = async (): Promise<void> => {
|
||||
const selectedDocumentId = this.selectedDocumentId();
|
||||
const msg = !this.isPreferredApiMongoDB
|
||||
? "Are you sure you want to delete the selected item ?"
|
||||
: "Are you sure you want to delete the selected document ?";
|
||||
|
||||
if (window.confirm(msg)) {
|
||||
return this._deleteDocument(selectedDocumentId);
|
||||
await this._deleteDocument(selectedDocumentId);
|
||||
}
|
||||
|
||||
return Q();
|
||||
};
|
||||
|
||||
public onValidDocumentEdit(): Q.Promise<any> {
|
||||
@@ -617,63 +606,50 @@ export default class DocumentsTab extends TabsBase {
|
||||
return Q();
|
||||
}
|
||||
|
||||
public onTabClick(): Q.Promise<any> {
|
||||
return super.onTabClick().then(() => {
|
||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
|
||||
});
|
||||
public onTabClick(): void {
|
||||
super.onTabClick();
|
||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
|
||||
}
|
||||
|
||||
public onActivate(): Q.Promise<any> {
|
||||
return super.onActivate().then(() => {
|
||||
if (this._documentsIterator) {
|
||||
return Q.resolve(this._documentsIterator);
|
||||
}
|
||||
public async onActivate(): Promise<void> {
|
||||
super.onActivate();
|
||||
|
||||
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;
|
||||
}
|
||||
if (!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;
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public onRefreshClick(): Q.Promise<any> {
|
||||
return this.refreshDocumentsGrid().then(() => {
|
||||
this.selectedDocumentContent("");
|
||||
this.selectedDocumentId(null);
|
||||
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
||||
});
|
||||
}
|
||||
private _isIgnoreDirtyEditor = (): boolean => {
|
||||
var msg: string = "Changes will be lost. Do you want to continue?";
|
||||
return window.confirm(msg);
|
||||
};
|
||||
|
||||
protected __deleteDocument(documentId: DocumentId): Q.Promise<any> {
|
||||
protected __deleteDocument(documentId: DocumentId): Promise<void> {
|
||||
return deleteDocument(this.collection, documentId);
|
||||
}
|
||||
|
||||
private _deleteDocument(selectedDocumentId: DocumentId): Q.Promise<any> {
|
||||
private _deleteDocument(selectedDocumentId: DocumentId): Promise<void> {
|
||||
this.isExecutionError(false);
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, {
|
||||
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
|
||||
@@ -684,7 +660,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
this.isExecuting(true);
|
||||
return this.__deleteDocument(selectedDocumentId)
|
||||
.then(
|
||||
(result: any) => {
|
||||
() => {
|
||||
this.documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid);
|
||||
this.selectedDocumentContent("");
|
||||
this.selectedDocumentId(null);
|
||||
@@ -720,7 +696,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
.finally(() => this.isExecuting(false));
|
||||
}
|
||||
|
||||
public createIterator(): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
|
||||
public createIterator(): QueryIterator<ItemDefinition & Resource> {
|
||||
let filters = this.lastFilterContents();
|
||||
const filter: string = this.filterContent().trim();
|
||||
const query: string = this.buildQuery(filter);
|
||||
@@ -734,11 +710,10 @@ export default class DocumentsTab extends TabsBase {
|
||||
return queryDocuments(this.collection.databaseId, this.collection.id(), query, options);
|
||||
}
|
||||
|
||||
public selectDocument(documentId: DocumentId): Q.Promise<any> {
|
||||
public async selectDocument(documentId: DocumentId): Promise<void> {
|
||||
this.selectedDocumentId(documentId);
|
||||
return readDocument(this.collection, documentId).then((content: any) => {
|
||||
this.initDocumentEditor(documentId, content);
|
||||
});
|
||||
const content = await readDocument(this.collection, documentId);
|
||||
this.initDocumentEditor(documentId, content);
|
||||
}
|
||||
|
||||
public loadNextPage(): Q.Promise<any> {
|
||||
|
||||
@@ -114,10 +114,9 @@ export default class GraphTab extends TabsBase {
|
||||
: `${account.name}.graphs.azure.com:443/`;
|
||||
}
|
||||
|
||||
public onTabClick(): Q.Promise<any> {
|
||||
return super.onTabClick().then(() => {
|
||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Graph);
|
||||
});
|
||||
public onTabClick(): void {
|
||||
super.onTabClick();
|
||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Graph);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -289,7 +289,7 @@
|
||||
<button
|
||||
class="filterbtnstyle queryButton"
|
||||
data-bind="
|
||||
click: onApplyFilterClick,
|
||||
click: refreshDocumentsGrid,
|
||||
enable: applyFilterButton.enabled"
|
||||
>
|
||||
Apply Filter
|
||||
|
||||
@@ -44,7 +44,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
super.buildCommandBarOptions();
|
||||
}
|
||||
|
||||
public onSaveNewDocumentClick = (): Q.Promise<any> => {
|
||||
public onSaveNewDocumentClick = (): Promise<any> => {
|
||||
const documentContent = JSON.parse(this.selectedDocumentContent());
|
||||
this.displayedError("");
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
|
||||
@@ -78,12 +78,12 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
startKey
|
||||
);
|
||||
Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab");
|
||||
return Q.reject("Document without shard key");
|
||||
throw new Error("Document without shard key");
|
||||
}
|
||||
|
||||
this.isExecutionError(false);
|
||||
this.isExecuting(true);
|
||||
return Q(createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent))
|
||||
return createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent)
|
||||
.then(
|
||||
(savedDocument: any) => {
|
||||
let partitionKeyArray = extractPartitionKey(
|
||||
@@ -136,7 +136,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
.finally(() => this.isExecuting(false));
|
||||
};
|
||||
|
||||
public onSaveExisitingDocumentClick = (): Q.Promise<any> => {
|
||||
public onSaveExisitingDocumentClick = (): Promise<any> => {
|
||||
const selectedDocumentId = this.selectedDocumentId();
|
||||
const documentContent = this.selectedDocumentContent();
|
||||
this.isExecutionError(false);
|
||||
@@ -148,7 +148,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
tabTitle: this.tabTitle()
|
||||
});
|
||||
|
||||
return Q(updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent))
|
||||
return updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent)
|
||||
.then(
|
||||
(updatedDocument: any) => {
|
||||
let value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4);
|
||||
@@ -204,13 +204,10 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
return filter || "{}";
|
||||
}
|
||||
|
||||
public selectDocument(documentId: DocumentId): Q.Promise<any> {
|
||||
public async selectDocument(documentId: DocumentId): Promise<void> {
|
||||
this.selectedDocumentId(documentId);
|
||||
return Q(
|
||||
readDocument(this.collection.databaseId, this.collection, documentId).then((content: any) => {
|
||||
this.initDocumentEditor(documentId, content);
|
||||
})
|
||||
);
|
||||
const content = await readDocument(this.collection.databaseId, this.collection, documentId);
|
||||
this.initDocumentEditor(documentId, content);
|
||||
}
|
||||
|
||||
public loadNextPage(): Q.Promise<any> {
|
||||
@@ -330,7 +327,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
return partitionKey;
|
||||
}
|
||||
|
||||
protected __deleteDocument(documentId: DocumentId): Q.Promise<any> {
|
||||
return Q(deleteDocument(this.collection.databaseId, this.collection, documentId));
|
||||
protected __deleteDocument(documentId: DocumentId): Promise<void> {
|
||||
return deleteDocument(this.collection.databaseId, this.collection, documentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,10 +53,9 @@ export default class MongoShellTab extends TabsBase {
|
||||
// }
|
||||
}
|
||||
|
||||
public onTabClick(): Q.Promise<any> {
|
||||
return super.onTabClick().then(() => {
|
||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
|
||||
});
|
||||
public onTabClick(): void {
|
||||
super.onTabClick();
|
||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
|
||||
}
|
||||
|
||||
public handleMessage(event: MessageEvent) {
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
</div>
|
||||
<json-editor
|
||||
params="{ content: queryResults, isReadOnly: true, ariaLabel: 'Query results' }"
|
||||
data-bind="visible: queryResults().length > 0 && isResultToggled() && allResultsMetadata().length > 0 && !error()"
|
||||
data-bind="visible: queryResults() && queryResults().length > 0 && isResultToggled() && allResultsMetadata().length > 0 && !error()"
|
||||
>
|
||||
</json-editor>
|
||||
<div
|
||||
|
||||
@@ -15,9 +15,10 @@ import { QueryUtils } from "../../Utils/QueryUtils";
|
||||
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
||||
|
||||
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
|
||||
import { queryDocuments, queryDocumentsPage } from "../../Common/DocumentClientUtilityBase";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage";
|
||||
|
||||
enum ToggleState {
|
||||
Result,
|
||||
@@ -163,20 +164,19 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
this._buildCommandBarOptions();
|
||||
}
|
||||
|
||||
public onTabClick(): Q.Promise<any> {
|
||||
return super.onTabClick().then(() => {
|
||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query);
|
||||
});
|
||||
public onTabClick(): void {
|
||||
super.onTabClick();
|
||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query);
|
||||
}
|
||||
|
||||
public onExecuteQueryClick = (): Q.Promise<any> => {
|
||||
public onExecuteQueryClick = async (): Promise<void> => {
|
||||
const sqlStatement: string = this.selectedContent() || this.sqlQueryEditorContent();
|
||||
this.sqlStatementToExecute(sqlStatement);
|
||||
this.allResultsMetadata([]);
|
||||
this.queryResults("");
|
||||
this._iterator = null;
|
||||
this._iterator = undefined;
|
||||
|
||||
return this._executeQueryDocumentsPage(0);
|
||||
await this._executeQueryDocumentsPage(0);
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
public onFetchNextPageClick(): Q.Promise<any> {
|
||||
public async onFetchNextPageClick(): Promise<void> {
|
||||
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
|
||||
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
|
||||
const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1;
|
||||
const itemCount: number = (metadata && Number(metadata.itemCount)) || 0;
|
||||
|
||||
return this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1);
|
||||
await this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1);
|
||||
}
|
||||
|
||||
public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => {
|
||||
@@ -265,19 +265,18 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
return true;
|
||||
};
|
||||
|
||||
private _executeQueryDocumentsPage(firstItemIndex: number): Q.Promise<any> {
|
||||
private async _executeQueryDocumentsPage(firstItemIndex: number): Promise<any> {
|
||||
this.error("");
|
||||
this.roundTrips(undefined);
|
||||
if (this._iterator == null) {
|
||||
const queryIteratorPromise = this._initIterator();
|
||||
return queryIteratorPromise.finally(() => this._queryDocumentsPage(firstItemIndex));
|
||||
if (this._iterator === undefined) {
|
||||
this._initIterator();
|
||||
}
|
||||
|
||||
return this._queryDocumentsPage(firstItemIndex);
|
||||
await this._queryDocumentsPage(firstItemIndex);
|
||||
}
|
||||
|
||||
// TODO: Position and enable spinner when request is in progress
|
||||
private _queryDocumentsPage(firstItemIndex: number): Q.Promise<any> {
|
||||
private async _queryDocumentsPage(firstItemIndex: number): Promise<void> {
|
||||
this.isExecutionError(false);
|
||||
this._resetAggregateQueryMetrics();
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.ExecuteQuery, {
|
||||
@@ -289,90 +288,90 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
let options: any = {};
|
||||
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
|
||||
|
||||
const queryDocuments = (firstItemIndex: number) =>
|
||||
queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex, options);
|
||||
const queryDocuments = async (firstItemIndex: number) =>
|
||||
await queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex);
|
||||
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);
|
||||
|
||||
this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]);
|
||||
try {
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]);
|
||||
|
||||
const documents: any[] = queryResults.documents;
|
||||
const results = this.renderObjectForEditor(documents, null, 4);
|
||||
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;
|
||||
}
|
||||
|
||||
const resultsDisplay: string =
|
||||
queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`;
|
||||
this.showingDocumentsDisplayText(resultsDisplay);
|
||||
this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`);
|
||||
const documents: any[] = queryResults.documents;
|
||||
const results = this.renderObjectForEditor(documents, null, 4);
|
||||
|
||||
if (!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");
|
||||
}
|
||||
const resultsDisplay: string =
|
||||
queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`;
|
||||
this.showingDocumentsDisplayText(resultsDisplay);
|
||||
this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`);
|
||||
|
||||
this.queryResults(results);
|
||||
if (!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");
|
||||
}
|
||||
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.ExecuteQuery,
|
||||
{
|
||||
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
|
||||
defaultExperience: this.collection && this.collection.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle()
|
||||
},
|
||||
startKey
|
||||
);
|
||||
this.queryResults(results);
|
||||
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.ExecuteQuery,
|
||||
{
|
||||
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
|
||||
defaultExperience: this.collection && this.collection.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle()
|
||||
},
|
||||
(error: any) => {
|
||||
this.isExecutionError(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
this.error(errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.ExecuteQuery,
|
||||
{
|
||||
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
|
||||
defaultExperience: this.collection && this.collection.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error)
|
||||
},
|
||||
startKey
|
||||
);
|
||||
document.getElementById("error-display").focus();
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
this.isExecuting(false);
|
||||
this.togglesOnFocus();
|
||||
});
|
||||
startKey
|
||||
);
|
||||
} catch (error) {
|
||||
this.isExecutionError(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
this.error(errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.ExecuteQuery,
|
||||
{
|
||||
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
|
||||
defaultExperience: this.collection && this.collection.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error)
|
||||
},
|
||||
startKey
|
||||
);
|
||||
document.getElementById("error-display").focus();
|
||||
} finally {
|
||||
this.isExecuting(false);
|
||||
this.togglesOnFocus();
|
||||
}
|
||||
}
|
||||
|
||||
private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void {
|
||||
@@ -477,16 +476,17 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
}
|
||||
}
|
||||
|
||||
protected _initIterator(): Q.Promise<MinimalQueryIterator> {
|
||||
protected _initIterator(): void {
|
||||
const options: any = QueryTab.getIteratorOptions(this.collection);
|
||||
if (this._resourceTokenPartitionKey) {
|
||||
options.partitionKey = this._resourceTokenPartitionKey;
|
||||
}
|
||||
|
||||
return Q(
|
||||
queryDocuments(this.collection.databaseId, this.collection.id(), this.sqlStatementToExecute(), options).then(
|
||||
iterator => (this._iterator = iterator)
|
||||
)
|
||||
this._iterator = queryDocuments(
|
||||
this.collection.databaseId,
|
||||
this.collection.id(),
|
||||
this.sqlStatementToExecute(),
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -161,17 +161,16 @@ export default class QueryTablesTab extends TabsBase {
|
||||
return null;
|
||||
};
|
||||
|
||||
public onActivate(): Q.Promise<any> {
|
||||
return super.onActivate().then(() => {
|
||||
const columns =
|
||||
!!this.tableEntityListViewModel() &&
|
||||
!!this.tableEntityListViewModel().table &&
|
||||
this.tableEntityListViewModel().table.columns;
|
||||
if (!!columns) {
|
||||
columns.adjust();
|
||||
$(window).resize();
|
||||
}
|
||||
});
|
||||
public onActivate(): void {
|
||||
super.onActivate();
|
||||
const columns =
|
||||
!!this.tableEntityListViewModel() &&
|
||||
!!this.tableEntityListViewModel().table &&
|
||||
this.tableEntityListViewModel().table.columns;
|
||||
if (!!columns) {
|
||||
columns.adjust();
|
||||
$(window).resize();
|
||||
}
|
||||
}
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
|
||||
@@ -186,12 +186,11 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
|
||||
this._setBaselines();
|
||||
}
|
||||
|
||||
public onTabClick(): Q.Promise<any> {
|
||||
return super.onTabClick().then(() => {
|
||||
if (this.isNew()) {
|
||||
this.collection.selectedSubnodeKind(this.tabKind);
|
||||
}
|
||||
});
|
||||
public onTabClick(): void {
|
||||
super.onTabClick();
|
||||
if (this.isNew()) {
|
||||
this.collection.selectedSubnodeKind(this.tabKind);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract onSaveClick: () => Promise<any>;
|
||||
|
||||
@@ -42,54 +42,49 @@ export default class SettingsTabV2 extends TabsBase {
|
||||
});
|
||||
}
|
||||
|
||||
public onActivate(): Q.Promise<unknown> {
|
||||
this.isExecuting(true);
|
||||
this.currentCollection.loadOffer().then(
|
||||
() => {
|
||||
// passed in options and set by parent as "Settings" by default
|
||||
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);
|
||||
}
|
||||
);
|
||||
public async onActivate(): Promise<void> {
|
||||
try {
|
||||
this.isExecuting(true);
|
||||
await this.currentCollection.loadOffer();
|
||||
// passed in options and set by parent as "Settings" by default
|
||||
this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings");
|
||||
|
||||
return super.onActivate().then(() => {
|
||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.SettingsV2);
|
||||
});
|
||||
this.options.getPendingNotification.then(
|
||||
(data: DataModels.Notification) => {
|
||||
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 {
|
||||
|
||||
@@ -94,9 +94,8 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
});
|
||||
}
|
||||
|
||||
public onTabClick(): Q.Promise<any> {
|
||||
public onTabClick(): void {
|
||||
this.getContainer().tabsManager.activateTab(this);
|
||||
return Q();
|
||||
}
|
||||
|
||||
protected updateSelectedNode(): void {
|
||||
@@ -128,7 +127,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
return this.onSpaceOrEnterKeyPress(event, () => this.onCloseTabButtonClick());
|
||||
};
|
||||
|
||||
public onActivate(): Q.Promise<any> {
|
||||
public onActivate(): void {
|
||||
this.updateSelectedNode();
|
||||
if (!!this.collection) {
|
||||
this.collection.selectedSubnodeKind(this.tabKind);
|
||||
@@ -151,7 +150,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
tabTitle: this.tabTitle(),
|
||||
tabId: this.tabId
|
||||
});
|
||||
return Q();
|
||||
}
|
||||
|
||||
public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => {
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
<notebook-viewer-tab params="{data: $data}"></notebook-viewer-tab>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko if: $data.tabKind === 19 -->
|
||||
<!-- ko if: $data.tabKind === 20 -->
|
||||
<settings-tab-v2 params="{data: $data}"></settings-tab-v2>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,6 @@ import * as Constants from "../../Common/Constants";
|
||||
import { readStoredProcedures } from "../../Common/dataAccess/readStoredProcedures";
|
||||
import { readTriggers } from "../../Common/dataAccess/readTriggers";
|
||||
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
|
||||
import { createDocument } from "../../Common/DocumentClientUtilityBase";
|
||||
import { readCollectionOffer } from "../../Common/dataAccess/readCollectionOffer";
|
||||
import { getCollectionUsageSizeInKB } from "../../Common/dataAccess/getCollectionDataUsageSize";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
@@ -39,6 +38,7 @@ import Explorer from "../Explorer";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
|
||||
export default class Collection implements ViewModels.Collection {
|
||||
public nodeKind: string;
|
||||
@@ -551,7 +551,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
|
||||
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
|
||||
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
|
||||
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => {
|
||||
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.SettingsV2, tab => {
|
||||
return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id();
|
||||
});
|
||||
|
||||
@@ -1091,8 +1091,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private _createDocumentsFromFile(fileName: string, documentContent: string): Q.Promise<UploadDetailsRecord> {
|
||||
const deferred: Q.Deferred<UploadDetailsRecord> = Q.defer();
|
||||
private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> {
|
||||
const record: UploadDetailsRecord = {
|
||||
fileName: fileName,
|
||||
numSucceeded: 0,
|
||||
@@ -1102,39 +1101,25 @@ export default class Collection implements ViewModels.Collection {
|
||||
|
||||
try {
|
||||
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)) {
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
promises.push(triggerCreateDocument(content[i]));
|
||||
}
|
||||
await Promise.all(
|
||||
content.map(async documentContent => {
|
||||
await createDocument(this, documentContent);
|
||||
record.numSucceeded++;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
promises.push(triggerCreateDocument(content));
|
||||
await createDocument(this, documentContent);
|
||||
record.numSucceeded++;
|
||||
}
|
||||
|
||||
Q.all(promises).then(() => {
|
||||
deferred.resolve(record);
|
||||
});
|
||||
} catch (e) {
|
||||
return record;
|
||||
} catch (error) {
|
||||
record.numFailed++;
|
||||
record.errors = [...record.errors, e.message];
|
||||
deferred.resolve(record);
|
||||
record.errors = [...record.errors, error.message];
|
||||
return record;
|
||||
}
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { extractPartitionKey } from "@azure/cosmos";
|
||||
import ConflictsTab from "../Tabs/ConflictsTab";
|
||||
import { readDocument } from "../../Common/DocumentClientUtilityBase";
|
||||
import { readDocument } from "../../Common/dataAccess/readDocument";
|
||||
|
||||
export default class ConflictId {
|
||||
public container: ConflictsTab;
|
||||
@@ -59,41 +59,42 @@ export default class ConflictId {
|
||||
return;
|
||||
}
|
||||
|
||||
public loadConflict(): Q.Promise<any> {
|
||||
const conflictsTab = this.container;
|
||||
public async loadConflict(): Promise<void> {
|
||||
this.container.selectedConflictId(this);
|
||||
|
||||
if (this.operationType === Constants.ConflictOperationType.Create) {
|
||||
this.container.initDocumentEditorForCreate(this, this.content);
|
||||
return Q();
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Document could be deleted
|
||||
if (
|
||||
reason &&
|
||||
reason.code === Constants.HttpStatusCodes.NotFound &&
|
||||
this.operationType === Constants.ConflictOperationType.Delete
|
||||
) {
|
||||
this.container.initDocumentEditorForNoOp(this);
|
||||
return Q();
|
||||
}
|
||||
try {
|
||||
const currentDocumentContent = await readDocument(
|
||||
this.container.collection,
|
||||
this.buildDocumentIdFromConflict(this.partitionKeyValue)
|
||||
);
|
||||
|
||||
return Q.reject(reason);
|
||||
if (this.operationType === Constants.ConflictOperationType.Replace) {
|
||||
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 {
|
||||
|
||||
@@ -65,7 +65,7 @@ export default class DocumentId {
|
||||
return JSON.stringify(partitionKeyValue);
|
||||
}
|
||||
|
||||
public loadDocument(): Q.Promise<any> {
|
||||
return this.container.selectDocument(this);
|
||||
public async loadDocument(): Promise<void> {
|
||||
await this.container.selectDocument(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import * as ko from "knockout";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { deleteStoredProcedure } from "../../Common/dataAccess/deleteStoredProcedure";
|
||||
import { executeStoredProcedure } from "../../Common/DocumentClientUtilityBase";
|
||||
import { executeStoredProcedure } from "../../Common/dataAccess/executeStoredProcedure";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
@@ -178,8 +178,7 @@ export class JunoClient {
|
||||
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`);
|
||||
}
|
||||
|
||||
// will be renamed once feature.enableCodeOfConduct flag is removed
|
||||
public async fetchPublicNotebooks(): Promise<IJunoResponse<IPublicGalleryData>> {
|
||||
public async getPublicGalleryData(): Promise<IJunoResponse<IPublicGalleryData>> {
|
||||
const url = `${this.getNotebooksAccountUrl()}/gallery/public`;
|
||||
const response = await window.fetch(url, {
|
||||
method: "PATCH",
|
||||
@@ -405,7 +404,7 @@ export class JunoClient {
|
||||
}
|
||||
|
||||
public async reportAbuse(notebookId: string, abuseCategory: string, notes: string): Promise<IJunoResponse<boolean>> {
|
||||
const response = await window.fetch(`${this.getNotebooksUrl()}/avert/reportAbuse`, {
|
||||
const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/reportAbuse`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
notebookId,
|
||||
|
||||
@@ -12,8 +12,8 @@ import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
||||
export class NotebookWorkspaceManager {
|
||||
private resourceProviderClientFactory: IResourceProviderClientFactory<any>;
|
||||
|
||||
constructor(private _armEndpoint: string) {
|
||||
this.resourceProviderClientFactory = new ResourceProviderClientFactory(this._armEndpoint);
|
||||
constructor() {
|
||||
this.resourceProviderClientFactory = new ResourceProviderClientFactory();
|
||||
}
|
||||
|
||||
public async getNotebookWorkspacesAsync(cosmosdbResourceId: string): Promise<NotebookWorkspace[]> {
|
||||
|
||||
@@ -19,6 +19,7 @@ export function initializeExplorer(): Explorer {
|
||||
cassandraEndpoint: ""
|
||||
}
|
||||
});
|
||||
|
||||
explorer.isAccountReady(true);
|
||||
return explorer;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility"
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
|
||||
export default class AuthHeadersUtil {
|
||||
public static serverId: string = Constants.ServerIds.productionPortal;
|
||||
@@ -37,8 +38,7 @@ export default class AuthHeadersUtil {
|
||||
cacheLocation: window.navigator.userAgent.indexOf("Edge") > -1 ? "localStorage" : undefined
|
||||
});
|
||||
|
||||
public static getAccessInputMetadata(accessInput: string): Q.Promise<DataModels.AccessInputMetadata> {
|
||||
const deferred: Q.Deferred<DataModels.AccessInputMetadata> = Q.defer<DataModels.AccessInputMetadata>();
|
||||
public static async getAccessInputMetadata(accessInput: string): Promise<DataModels.AccessInputMetadata> {
|
||||
const url = `${configContext.BACKEND_ENDPOINT}${Constants.ApiEndpoints.guestRuntimeProxy}/accessinputmetadata`;
|
||||
const authType: string = (<any>window).authType;
|
||||
const headers: { [headerName: string]: string } = {};
|
||||
@@ -49,58 +49,55 @@ export default class AuthHeadersUtil {
|
||||
headers[Constants.HttpHeaders.connectionString] = accessInput;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "GET",
|
||||
headers: headers,
|
||||
cache: false,
|
||||
dataType: "text"
|
||||
}).then(
|
||||
(data: string, textStatus: string, xhr: JQueryXHR<any>) => {
|
||||
if (!data) {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to get access input metadata`);
|
||||
deferred.reject(`Failed to get access input metadata`);
|
||||
}
|
||||
let responseText: string;
|
||||
try {
|
||||
const timeout = setTimeout(() => {
|
||||
throw Error("Request timed out while fetching access input metadata");
|
||||
}, Constants.ClientDefaults.requestTimeoutMs);
|
||||
|
||||
try {
|
||||
const metadata: DataModels.AccessInputMetadata = JSON.parse(JSON.parse(data));
|
||||
deferred.resolve(metadata); // TODO: update to a single JSON parse once backend response is stringified exactly once
|
||||
} catch (error) {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, "Failed to parse access input metadata");
|
||||
deferred.reject("Failed to parse access input metadata");
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
(xhr: JQueryXHR<any>, textStatus: string, error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while fetching access input metadata: ${JSON.stringify(xhr.responseText)}`
|
||||
);
|
||||
deferred.reject(xhr.responseText);
|
||||
const response = await fetch(url, {
|
||||
headers,
|
||||
method: "GET"
|
||||
});
|
||||
|
||||
clearTimeout(timeout);
|
||||
|
||||
responseText = await response.text();
|
||||
if (!response.ok || !responseText) {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to get access input metadata`);
|
||||
throw Error("Failed to get access input metadata");
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMessage: string = getErrorMessage(error);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while fetching access input metadata: ${errorMessage}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
|
||||
try {
|
||||
const metadata: DataModels.AccessInputMetadata = JSON.parse(JSON.parse(responseText));
|
||||
return metadata; // TODO: update to a single JSON parse once backend response is stringified exactly once
|
||||
} catch (error) {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, "Failed to parse access input metadata");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public static generateEncryptedToken(): Q.Promise<DataModels.GenerateTokenResponse> {
|
||||
public static async generateEncryptedToken(): Promise<DataModels.GenerateTokenResponse> {
|
||||
const url = configContext.BACKEND_ENDPOINT + "/api/tokens/generateToken" + AuthHeadersUtil._generateResourceUrl();
|
||||
const explorer = window.dataExplorer;
|
||||
const headers: any = { authorization: userContext.authorizationToken };
|
||||
headers[Constants.HttpHeaders.getReadOnlyKey] = !explorer.hasWriteAccess();
|
||||
headers[Constants.HttpHeaders.contentType] = "application/json";
|
||||
|
||||
return AuthHeadersUtil._initiateGenerateTokenRequest({
|
||||
url: url,
|
||||
type: "POST",
|
||||
headers: headers,
|
||||
contentType: "application/json",
|
||||
cache: false
|
||||
});
|
||||
return await AuthHeadersUtil._initiateGenerateTokenRequest(url, "POST", headers);
|
||||
}
|
||||
|
||||
public static generateUnauthenticatedEncryptedTokenForConnectionString(
|
||||
public static async generateUnauthenticatedEncryptedTokenForConnectionString(
|
||||
connectionString: string
|
||||
): Q.Promise<DataModels.GenerateTokenResponse> {
|
||||
): Promise<DataModels.GenerateTokenResponse> {
|
||||
if (!connectionString) {
|
||||
return Q.reject("None or empty connection string specified");
|
||||
}
|
||||
@@ -108,14 +105,9 @@ export default class AuthHeadersUtil {
|
||||
const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken";
|
||||
const headers: any = {};
|
||||
headers[Constants.HttpHeaders.connectionString] = connectionString;
|
||||
headers[Constants.HttpHeaders.contentType] = "application/json";
|
||||
|
||||
return AuthHeadersUtil._initiateGenerateTokenRequest({
|
||||
url: url,
|
||||
type: "POST",
|
||||
headers: headers,
|
||||
contentType: "application/json",
|
||||
cache: false
|
||||
});
|
||||
return await AuthHeadersUtil._initiateGenerateTokenRequest(url, "POST", headers);
|
||||
}
|
||||
|
||||
public static isUserSignedIn(): boolean {
|
||||
@@ -282,24 +274,27 @@ export default class AuthHeadersUtil {
|
||||
return `?resourceUrl=${resourceUrl}&rid=${rid}&rtype=${rtype}&sid=${sid}&rg=${rg}&dba=${dba}&api=${apiKind}`;
|
||||
}
|
||||
|
||||
private static _initiateGenerateTokenRequest(
|
||||
requestSettings: JQueryAjaxSettings<any>
|
||||
): Q.Promise<DataModels.GenerateTokenResponse> {
|
||||
const deferred: Q.Deferred<DataModels.GenerateTokenResponse> = Q.defer<DataModels.GenerateTokenResponse>();
|
||||
private static async _initiateGenerateTokenRequest(
|
||||
url: string,
|
||||
method: string,
|
||||
headers: any
|
||||
): Promise<DataModels.GenerateTokenResponse> {
|
||||
const timeout = setTimeout(() => {
|
||||
throw Error("Request timed out while generating token");
|
||||
}, Constants.ClientDefaults.requestTimeoutMs);
|
||||
|
||||
$.ajax(requestSettings).then(
|
||||
(data: string, textStatus: string, xhr: JQueryXHR<any>) => {
|
||||
if (!data) {
|
||||
deferred.reject("No token generated");
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
headers,
|
||||
method
|
||||
});
|
||||
|
||||
deferred.resolve(JSON.parse(data));
|
||||
},
|
||||
(xhr: JQueryXHR<any>, textStatus: string, error: any) => {
|
||||
deferred.reject(xhr.responseText);
|
||||
}
|
||||
);
|
||||
clearTimeout(timeout);
|
||||
|
||||
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
|
||||
const token: string = await response.json();
|
||||
if (response.ok && token) {
|
||||
return JSON.parse(token);
|
||||
}
|
||||
|
||||
throw Error("No token generated");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ export default class Main {
|
||||
masterKey?: string /* master key extracted from connection string if available */,
|
||||
account?: DatabaseAccount,
|
||||
authorizationToken?: string /* access key */
|
||||
): Q.Promise<void> {
|
||||
): void {
|
||||
const serverId: string = AuthHeadersUtil.serverId;
|
||||
const authType: string = (<any>window).authType;
|
||||
const accountResourceId =
|
||||
@@ -373,7 +373,7 @@ export default class Main {
|
||||
});
|
||||
}
|
||||
|
||||
return Q.reject(`Unsupported AuthType ${authType}`);
|
||||
throw new Error(`Unsupported AuthType ${authType}`);
|
||||
}
|
||||
|
||||
private static _instantiateExplorer(): Explorer {
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
import "../../Explorer/Tables/DataTable/DataTableBindingManager";
|
||||
import Explorer from "../../Explorer/Explorer";
|
||||
import { handleMessage } from "../../Controls/Heatmap/Heatmap";
|
||||
|
||||
export function initializeExplorer(): 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);
|
||||
|
||||
return explorer;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { IResourceProviderClientFactory, IResourceProviderClient } from "./IResourceProviderClient";
|
||||
import { ResourceProviderClient } from "./ResourceProviderClient";
|
||||
|
||||
export class ResourceProviderClientFactory implements IResourceProviderClientFactory<any> {
|
||||
private armEndpoint: string;
|
||||
private cachedClients: { [url: string]: IResourceProviderClient<any> } = {};
|
||||
|
||||
constructor(private armEndpoint: string) {}
|
||||
constructor() {
|
||||
this.armEndpoint = configContext.ARM_ENDPOINT;
|
||||
}
|
||||
|
||||
public getOrCreate(url: string): IResourceProviderClient<any> {
|
||||
if (!url) {
|
||||
|
||||
@@ -17,9 +17,7 @@ export class TabRouteHandler {
|
||||
): void {
|
||||
this._initRouter();
|
||||
const parseHash = (newHash: string, oldHash: string) => this._tabRouter.parse(newHash);
|
||||
const defaultRoutedCallback = (request: string, data: { route: any; params: string[]; isFirst: boolean }) => {
|
||||
console.log(request);
|
||||
};
|
||||
const defaultRoutedCallback = (request: string, data: { route: any; params: string[]; isFirst: boolean }) => {};
|
||||
this._tabRouter.routed.add(onMatch || defaultRoutedCallback);
|
||||
hasher.initialized.add(parseHash);
|
||||
hasher.changed.add(parseHash);
|
||||
|
||||
@@ -144,10 +144,6 @@ export class OfferPricing {
|
||||
};
|
||||
}
|
||||
|
||||
export class GeneralResources {
|
||||
public static loadingText: string = "Loading...";
|
||||
}
|
||||
|
||||
export class CollectionCreation {
|
||||
// TODO generate these values based on Product\Services\Documents\ImageStore\GatewayApplication\Settings.xml
|
||||
public static readonly MinRUPerPartitionBelow7Partitions: number = 400;
|
||||
@@ -228,32 +224,6 @@ export class IndexingPolicies {
|
||||
}
|
||||
|
||||
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[] = [
|
||||
"b8f2ff04-0a81-4cf9-95ef-5828d16981d2",
|
||||
"39b1fdff-e5b2-4f83-adb4-33cb3aabf5ea",
|
||||
@@ -264,57 +234,6 @@ export class SubscriptionUtilMappings {
|
||||
];
|
||||
}
|
||||
|
||||
export class Offers {
|
||||
public static offerTypeS1: string = "S1";
|
||||
public static offerTypeS2: string = "S2";
|
||||
public static offerTypeS3: string = "S3";
|
||||
public static offerTypeStandard: string = "Standard";
|
||||
}
|
||||
|
||||
export class OfferThoughput {
|
||||
public static offerS1Throughput: number = 250;
|
||||
public static offerS2Throughput: number = 1000;
|
||||
public static offerS3Throughput: number = 2500;
|
||||
}
|
||||
|
||||
export class OfferVersions {
|
||||
public static offerV1: string = "V1";
|
||||
public static offerV2: string = "V2";
|
||||
}
|
||||
|
||||
export class InvalidOffers {
|
||||
public static offerTypeInvalid: string = "Invalid";
|
||||
public static offerTypeError: string = "Loading Error";
|
||||
}
|
||||
|
||||
export class SpecTypes {
|
||||
public static collection: string = "DocumentDbCollection";
|
||||
}
|
||||
|
||||
export class CurrencyCodes {
|
||||
public static usd: string = "USD";
|
||||
public static rmb: string = "RMB";
|
||||
}
|
||||
|
||||
export class ColorSchemes {
|
||||
public static standard: string = "mediumBlue";
|
||||
public static legacy: string = "yellowGreen";
|
||||
}
|
||||
|
||||
export class FeatureIds {
|
||||
public static storage: string = "storage";
|
||||
public static sla: string = "sla";
|
||||
public static partitioned: string = "partitioned";
|
||||
public static singlePartitioned: string = "singlePartition";
|
||||
public static legacySinglePartitioned: string = "legacySinglePartition";
|
||||
}
|
||||
|
||||
export class FeatureIconNames {
|
||||
public static storage: string = "SSD";
|
||||
public static sla: string = "Monitoring";
|
||||
public static productionReady: string = "ProductionReadyDb";
|
||||
}
|
||||
|
||||
export class AutopilotDocumentation {
|
||||
public static Url: string = "https://aka.ms/cosmos-autoscale-info";
|
||||
}
|
||||
|
||||
@@ -8,14 +8,13 @@ import { ArmApiVersions, ArmResourceTypes } from "../Common/Constants";
|
||||
import { IResourceProviderClient, IResourceProviderClientFactory } from "../ResourceProvider/IResourceProviderClient";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
||||
|
||||
export class ArcadiaResourceManager {
|
||||
private resourceProviderClientFactory: IResourceProviderClientFactory<any>;
|
||||
|
||||
constructor(private armEndpoint = configContext.ARM_ENDPOINT) {
|
||||
this.resourceProviderClientFactory = new ResourceProviderClientFactory(this.armEndpoint);
|
||||
constructor() {
|
||||
this.resourceProviderClientFactory = new ResourceProviderClientFactory();
|
||||
}
|
||||
|
||||
public async getWorkspacesAsync(arcadiaResourceId: string): Promise<ArcadiaWorkspace[]> {
|
||||
|
||||
@@ -59,9 +59,8 @@ const main = async (): Promise<void> => {
|
||||
|
||||
const serverSettings = createServerSettings(urlVars);
|
||||
|
||||
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, {
|
||||
baseUrl: serverSettings.baseUrl
|
||||
});
|
||||
const data = { baseUrl: serverSettings.baseUrl };
|
||||
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
|
||||
|
||||
try {
|
||||
if (urlVars.hasOwnProperty(TerminalQueryParams.Terminal)) {
|
||||
@@ -70,9 +69,9 @@ const main = async (): Promise<void> => {
|
||||
throw new Error("Only terminal is supported");
|
||||
}
|
||||
|
||||
TelemetryProcessor.traceSuccess(Action.OpenTerminal, startTime);
|
||||
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(Action.OpenTerminal, startTime);
|
||||
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ interface UserContext {
|
||||
defaultExperience?: DefaultAccountExperienceType;
|
||||
useSDKOperations?: boolean;
|
||||
subscriptionType?: SubscriptionType;
|
||||
quotaId?: string;
|
||||
}
|
||||
|
||||
const userContext: Readonly<UserContext> = {} as const;
|
||||
|
||||
@@ -10,6 +10,7 @@ import Explorer from "../Explorer/Explorer";
|
||||
import { IChoiceGroupOption, IChoiceGroupProps } from "office-ui-fabric-react";
|
||||
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
|
||||
import { handleError } from "../Common/ErrorHandlingUtils";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
|
||||
const defaultSelectedAbuseCategory = "Other";
|
||||
const abuseCategories: IChoiceGroupOption[] = [
|
||||
@@ -113,7 +114,7 @@ export function reportAbuse(
|
||||
|
||||
try {
|
||||
const response = await junoClient.reportAbuse(notebookId, abuseCategory, additionalDetails);
|
||||
if (!response.data) {
|
||||
if (response.status !== HttpStatusCodes.Accepted) {
|
||||
throw new Error(`Received HTTP ${response.status} when submitting report for ${data.name}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,39 +25,151 @@ describe("PricingUtils Tests", () => {
|
||||
|
||||
describe("computeRUUsagePriceHourly()", () => {
|
||||
it("should return 0 for NaN regions default cloud", () => {
|
||||
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, null, false);
|
||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||
serverId: "default",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: null,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: false
|
||||
});
|
||||
expect(value).toBe(0);
|
||||
});
|
||||
it("should return 0 for NaN regions default cloud, autoscale", () => {
|
||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||
serverId: "default",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: null,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: true
|
||||
});
|
||||
expect(value).toBe(0);
|
||||
});
|
||||
|
||||
it("should return 0 for -1 regions", () => {
|
||||
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, -1, false);
|
||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||
serverId: "default",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: -1,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: false
|
||||
});
|
||||
expect(value).toBe(0);
|
||||
});
|
||||
it("should return 0 for -1 regions, autoscale", () => {
|
||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||
serverId: "default",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: -1,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: true
|
||||
});
|
||||
expect(value).toBe(0);
|
||||
});
|
||||
|
||||
it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster disabled", () => {
|
||||
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 1, false);
|
||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||
serverId: "default",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 1,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: false
|
||||
});
|
||||
expect(value).toBe(0.00008);
|
||||
});
|
||||
it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster disabled, autoscale", () => {
|
||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||
serverId: "default",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 1,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: true
|
||||
});
|
||||
expect(value).toBe(0.00012);
|
||||
});
|
||||
|
||||
it("should return 0.00051 for Mooncake cloud, 1RU, 1 region, multimaster disabled", () => {
|
||||
const value = PricingUtils.computeRUUsagePriceHourly("mooncake", 1, 1, false);
|
||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||
serverId: "mooncake",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 1,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: false
|
||||
});
|
||||
expect(value).toBe(0.00051);
|
||||
});
|
||||
it("should return 0.00051 for Mooncake cloud, 1RU, 1 region, multimaster disabled, autoscale", () => {
|
||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||
serverId: "mooncake",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 1,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: true
|
||||
});
|
||||
expect(value).toBe(0.00076);
|
||||
});
|
||||
|
||||
it("should return 0.00016 for default cloud, 1RU, 2 regions, multimaster disabled", () => {
|
||||
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 2, false);
|
||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||
serverId: "default",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 2,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: false
|
||||
});
|
||||
expect(value).toBe(0.00016);
|
||||
});
|
||||
it("should return 0.00016 for default cloud, 1RU, 2 regions, multimaster disabled, autoscale", () => {
|
||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||
serverId: "default",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 2,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: true
|
||||
});
|
||||
expect(value).toBe(0.00024);
|
||||
});
|
||||
|
||||
it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster enabled", () => {
|
||||
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 1, true);
|
||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||
serverId: "default",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 1,
|
||||
multimasterEnabled: true,
|
||||
isAutoscale: false
|
||||
});
|
||||
expect(value).toBe(0.00008);
|
||||
});
|
||||
it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster enabled, autoscale", () => {
|
||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||
serverId: "default",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 1,
|
||||
multimasterEnabled: true,
|
||||
isAutoscale: true
|
||||
});
|
||||
expect(value).toBe(0.00012);
|
||||
});
|
||||
|
||||
it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled", () => {
|
||||
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 2, true);
|
||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||
serverId: "default",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 2,
|
||||
multimasterEnabled: true,
|
||||
isAutoscale: false
|
||||
});
|
||||
expect(value).toBe(0.00048);
|
||||
});
|
||||
it("should return 0.00048 for default cloud, 1RU, 2 region, multimaster enabled, autoscale", () => {
|
||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||
serverId: "default",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 2,
|
||||
multimasterEnabled: true,
|
||||
isAutoscale: true
|
||||
});
|
||||
expect(value).toBe(0.00096);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPriceCurrency()", () => {
|
||||
@@ -242,7 +354,7 @@ describe("PricingUtils Tests", () => {
|
||||
});
|
||||
|
||||
describe("getEstimatedSpendHtml()", () => {
|
||||
it("should return 'Estimated cost (USD): <b>$0.000080 hourly / $0.0019 daily / $0.058 monthly </b> (1 region, 1RU/s, $0.00008/RU)' for 1RU/s on default cloud, 1 region, with multimaster", () => {
|
||||
it("should return 'Cost (USD): <b>$0.000080 hourly / $0.0019 daily / $0.058 monthly </b> (1 region, 1RU/s, $0.00008/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>' for 1RU/s on default cloud, 1 region, with multimaster", () => {
|
||||
const value = PricingUtils.getEstimatedSpendHtml(
|
||||
1 /*RU/s*/,
|
||||
"default" /* cloud */,
|
||||
@@ -250,11 +362,11 @@ describe("PricingUtils Tests", () => {
|
||||
true /* multimaster */
|
||||
);
|
||||
expect(value).toBe(
|
||||
"Estimated cost (USD): <b>$0.000080 hourly / $0.0019 daily / $0.058 monthly </b> (1 region, 1RU/s, $0.00008/RU)"
|
||||
"Cost (USD): <b>$0.000080 hourly / $0.0019 daily / $0.058 monthly </b> (1 region, 1RU/s, $0.00008/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return 'Estimated cost (RMB): <b>¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly </b> (1 region, 1RU/s, ¥0.00051/RU)' for 1RU/s on mooncake, 1 region, with multimaster", () => {
|
||||
it("should return 'Cost (RMB): <b>¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly </b> (1 region, 1RU/s, ¥0.00051/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>' for 1RU/s on mooncake, 1 region, with multimaster", () => {
|
||||
const value = PricingUtils.getEstimatedSpendHtml(
|
||||
1 /*RU/s*/,
|
||||
"mooncake" /* cloud */,
|
||||
@@ -262,11 +374,11 @@ describe("PricingUtils Tests", () => {
|
||||
true /* multimaster */
|
||||
);
|
||||
expect(value).toBe(
|
||||
"Estimated cost (RMB): <b>¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly </b> (1 region, 1RU/s, ¥0.00051/RU)"
|
||||
"Cost (RMB): <b>¥0.00051 hourly / ¥0.012 daily / ¥0.37 monthly </b> (1 region, 1RU/s, ¥0.00051/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return 'Estimated cost (USD): <b>$0.13 hourly / $3.07 daily / $140.16 monthly </b> (2 regions, 400RU/s, $0.00016/RU)' for 400RU/s on default cloud, 2 region, with multimaster", () => {
|
||||
it("should return 'Cost (USD): <b>$0.13 hourly / $3.07 daily / $140.16 monthly </b> (2 regions, 400RU/s, $0.00016/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>' for 400RU/s on default cloud, 2 region, with multimaster", () => {
|
||||
const value = PricingUtils.getEstimatedSpendHtml(
|
||||
400 /*RU/s*/,
|
||||
"default" /* cloud */,
|
||||
@@ -274,11 +386,11 @@ describe("PricingUtils Tests", () => {
|
||||
true /* multimaster */
|
||||
);
|
||||
expect(value).toBe(
|
||||
"Estimated cost (USD): <b>$0.19 hourly / $4.61 daily / $140.16 monthly </b> (2 regions, 400RU/s, $0.00016/RU)"
|
||||
"Cost (USD): <b>$0.19 hourly / $4.61 daily / $140.16 monthly </b> (2 regions, 400RU/s, $0.00016/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return 'Estimated cost (USD): <b>$0.064 hourly / $1.54 daily / $46.72 monthly </b> (2 regions, 400RU/s, $0.00008/RU)' for 400RU/s on default cloud, 2 region, without multimaster", () => {
|
||||
it("should return 'Cost (USD): <b>$0.064 hourly / $1.54 daily / $46.72 monthly </b> (2 regions, 400RU/s, $0.00008/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>' for 400RU/s on default cloud, 2 region, without multimaster", () => {
|
||||
const value = PricingUtils.getEstimatedSpendHtml(
|
||||
400 /*RU/s*/,
|
||||
"default" /* cloud */,
|
||||
@@ -286,7 +398,7 @@ describe("PricingUtils Tests", () => {
|
||||
false /* multimaster */
|
||||
);
|
||||
expect(value).toBe(
|
||||
"Estimated cost (USD): <b>$0.064 hourly / $1.54 daily / $46.72 monthly </b> (2 regions, 400RU/s, $0.00008/RU)"
|
||||
"Cost (USD): <b>$0.064 hourly / $1.54 daily / $46.72 monthly </b> (2 regions, 400RU/s, $0.00008/RU)<p style='padding: 10px 0px 0px 0px;'><em>*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account</em></p>"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import * as AutoPilotUtils from "../Utils/AutoPilotUtils";
|
||||
import * as Constants from "../Shared/Constants";
|
||||
|
||||
interface ComputeRUUsagePriceHourlyArgs {
|
||||
serverId: string;
|
||||
requestUnits: number;
|
||||
numberOfRegions: number;
|
||||
multimasterEnabled: boolean;
|
||||
isAutoscale: boolean;
|
||||
}
|
||||
|
||||
export const estimatedCostDisclaimer =
|
||||
"*This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account";
|
||||
|
||||
/**
|
||||
* Anything that is not a number should return 0
|
||||
* Otherwise, return numberOfRegions
|
||||
@@ -47,15 +58,16 @@ export function getMultimasterMultiplier(numberOfRegions: number, multimasterEna
|
||||
return multimasterMultiplier;
|
||||
}
|
||||
|
||||
export function computeRUUsagePriceHourly(
|
||||
serverId: string,
|
||||
requestUnits: number,
|
||||
numberOfRegions: number,
|
||||
multimasterEnabled: boolean
|
||||
): number {
|
||||
export function computeRUUsagePriceHourly({
|
||||
serverId,
|
||||
requestUnits,
|
||||
numberOfRegions,
|
||||
multimasterEnabled,
|
||||
isAutoscale
|
||||
}: ComputeRUUsagePriceHourlyArgs): number {
|
||||
const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled);
|
||||
const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
|
||||
const pricePerRu = getPricePerRu(serverId);
|
||||
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multimasterMultiplier) : getPricePerRu(serverId);
|
||||
const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier;
|
||||
|
||||
return Number(ruCharge.toFixed(5));
|
||||
@@ -159,28 +171,19 @@ export function getAutoPilotV3SpendHtml(maxAutoPilotThroughputSet: number, isDat
|
||||
}' target='_blank' aria-label='Learn more about autoscale throughput'>Learn more</a>.`;
|
||||
}
|
||||
|
||||
export function computeAutoscaleUsagePriceHourly(
|
||||
serverId: string,
|
||||
requestUnits: number,
|
||||
numberOfRegions: number,
|
||||
multimasterEnabled: boolean
|
||||
): number {
|
||||
const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled);
|
||||
const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
|
||||
|
||||
const pricePerRu = getAutoscalePricePerRu(serverId, multimasterMultiplier);
|
||||
const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier;
|
||||
|
||||
return Number(ruCharge.toFixed(5));
|
||||
}
|
||||
|
||||
export function getEstimatedAutoscaleSpendHtml(
|
||||
throughput: number,
|
||||
serverId: string,
|
||||
regions: number,
|
||||
multimaster: boolean
|
||||
): string {
|
||||
const hourlyPrice: number = computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster);
|
||||
const hourlyPrice: number = computeRUUsagePriceHourly({
|
||||
serverId: serverId,
|
||||
requestUnits: throughput,
|
||||
numberOfRegions: regions,
|
||||
multimasterEnabled: multimaster,
|
||||
isAutoscale: true
|
||||
});
|
||||
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
|
||||
const currency: string = getPriceCurrency(serverId);
|
||||
const currencySign: string = getCurrencySign(serverId);
|
||||
@@ -203,7 +206,13 @@ export function getEstimatedSpendHtml(
|
||||
regions: number,
|
||||
multimaster: boolean
|
||||
): string {
|
||||
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster);
|
||||
const hourlyPrice: number = computeRUUsagePriceHourly({
|
||||
serverId: serverId,
|
||||
requestUnits: throughput,
|
||||
numberOfRegions: regions,
|
||||
multimasterEnabled: multimaster,
|
||||
isAutoscale: false
|
||||
});
|
||||
const dailyPrice: number = hourlyPrice * 24;
|
||||
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
|
||||
const currency: string = getPriceCurrency(serverId);
|
||||
@@ -211,11 +220,13 @@ export function getEstimatedSpendHtml(
|
||||
const pricePerRu = getPricePerRu(serverId) * getMultimasterMultiplier(regions, multimaster);
|
||||
|
||||
return (
|
||||
`Estimated cost (${currency}): <b>` +
|
||||
`Cost (${currency}): <b>` +
|
||||
`${currencySign}${calculateEstimateNumber(hourlyPrice)} hourly / ` +
|
||||
`${currencySign}${calculateEstimateNumber(dailyPrice)} daily / ` +
|
||||
`${currencySign}${calculateEstimateNumber(monthlyPrice)} monthly </b> ` +
|
||||
`(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)`
|
||||
`(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)` +
|
||||
`<p style='padding: 10px 0px 0px 0px;'>` +
|
||||
`<em>${estimatedCostDisclaimer}</em></p>`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -226,9 +237,13 @@ export function getEstimatedSpendAcknowledgeString(
|
||||
multimaster: boolean,
|
||||
isAutoscale: boolean
|
||||
): string {
|
||||
const hourlyPrice: number = isAutoscale
|
||||
? computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster)
|
||||
: computeRUUsagePriceHourly(serverId, throughput, regions, multimaster);
|
||||
const hourlyPrice: number = computeRUUsagePriceHourly({
|
||||
serverId: serverId,
|
||||
requestUnits: throughput,
|
||||
numberOfRegions: regions,
|
||||
multimasterEnabled: multimaster,
|
||||
isAutoscale: isAutoscale
|
||||
});
|
||||
const dailyPrice: number = hourlyPrice * 24;
|
||||
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
|
||||
const currencySign: string = getCurrencySign(serverId);
|
||||
|
||||
@@ -58,41 +58,36 @@ export class QueryUtils {
|
||||
return projections.join(",");
|
||||
}
|
||||
|
||||
public static queryPagesUntilContentPresent(
|
||||
public static async queryPagesUntilContentPresent(
|
||||
firstItemIndex: number,
|
||||
queryItems: (itemIndex: number) => Q.Promise<ViewModels.QueryResults>
|
||||
): Q.Promise<ViewModels.QueryResults> {
|
||||
queryItems: (itemIndex: number) => Promise<ViewModels.QueryResults>
|
||||
): Promise<ViewModels.QueryResults> {
|
||||
let roundTrips: number = 0;
|
||||
let netRequestCharge: number = 0;
|
||||
const doRequest = (itemIndex: number): Q.Promise<ViewModels.QueryResults> =>
|
||||
queryItems(itemIndex).then(
|
||||
(results: ViewModels.QueryResults) => {
|
||||
roundTrips = roundTrips + 1;
|
||||
results.roundTrips = roundTrips;
|
||||
results.requestCharge = Number(results.requestCharge) + netRequestCharge;
|
||||
netRequestCharge = Number(results.requestCharge);
|
||||
const resultsMetadata: ViewModels.QueryResultsMetadata = {
|
||||
hasMoreResults: results.hasMoreResults,
|
||||
itemCount: results.itemCount,
|
||||
firstItemIndex: results.firstItemIndex,
|
||||
lastItemIndex: results.lastItemIndex
|
||||
};
|
||||
if (resultsMetadata.itemCount === 0 && resultsMetadata.hasMoreResults) {
|
||||
return doRequest(resultsMetadata.lastItemIndex);
|
||||
}
|
||||
return Q.resolve(results);
|
||||
},
|
||||
(error: any) => {
|
||||
return Q.reject(error);
|
||||
}
|
||||
);
|
||||
const doRequest = async (itemIndex: number): Promise<ViewModels.QueryResults> => {
|
||||
const results: ViewModels.QueryResults = await queryItems(itemIndex);
|
||||
roundTrips = roundTrips + 1;
|
||||
results.roundTrips = roundTrips;
|
||||
results.requestCharge = Number(results.requestCharge) + netRequestCharge;
|
||||
netRequestCharge = Number(results.requestCharge);
|
||||
const resultsMetadata: ViewModels.QueryResultsMetadata = {
|
||||
hasMoreResults: results.hasMoreResults,
|
||||
itemCount: results.itemCount,
|
||||
firstItemIndex: results.firstItemIndex,
|
||||
lastItemIndex: results.lastItemIndex
|
||||
};
|
||||
if (resultsMetadata.itemCount === 0 && resultsMetadata.hasMoreResults) {
|
||||
return await doRequest(resultsMetadata.lastItemIndex);
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
return doRequest(firstItemIndex);
|
||||
return await doRequest(firstItemIndex);
|
||||
}
|
||||
|
||||
public static queryAllPages(
|
||||
queryItems: (itemIndex: number) => Q.Promise<ViewModels.QueryResults>
|
||||
): Q.Promise<ViewModels.QueryResults> {
|
||||
public static async queryAllPages(
|
||||
queryItems: (itemIndex: number) => Promise<ViewModels.QueryResults>
|
||||
): Promise<ViewModels.QueryResults> {
|
||||
const queryResults: ViewModels.QueryResults = {
|
||||
documents: [],
|
||||
activityId: undefined,
|
||||
@@ -103,25 +98,20 @@ export class QueryUtils {
|
||||
requestCharge: 0,
|
||||
roundTrips: 0
|
||||
};
|
||||
const doRequest = (itemIndex: number): Q.Promise<ViewModels.QueryResults> =>
|
||||
queryItems(itemIndex).then(
|
||||
(results: ViewModels.QueryResults) => {
|
||||
const { requestCharge, hasMoreResults, itemCount, lastItemIndex, documents } = results;
|
||||
queryResults.roundTrips = queryResults.roundTrips + 1;
|
||||
queryResults.requestCharge = Number(queryResults.requestCharge) + Number(requestCharge);
|
||||
queryResults.hasMoreResults = hasMoreResults;
|
||||
queryResults.itemCount = queryResults.itemCount + itemCount;
|
||||
queryResults.lastItemIndex = lastItemIndex;
|
||||
queryResults.documents = queryResults.documents.concat(documents);
|
||||
if (queryResults.hasMoreResults) {
|
||||
return doRequest(queryResults.lastItemIndex + 1);
|
||||
}
|
||||
return Q.resolve(queryResults);
|
||||
},
|
||||
(error: any) => {
|
||||
return Q.reject(error);
|
||||
}
|
||||
);
|
||||
const doRequest = async (itemIndex: number): Promise<ViewModels.QueryResults> => {
|
||||
const results: ViewModels.QueryResults = await queryItems(itemIndex);
|
||||
const { requestCharge, hasMoreResults, itemCount, lastItemIndex, documents } = results;
|
||||
queryResults.roundTrips = queryResults.roundTrips + 1;
|
||||
queryResults.requestCharge = Number(queryResults.requestCharge) + Number(requestCharge);
|
||||
queryResults.hasMoreResults = hasMoreResults;
|
||||
queryResults.itemCount = queryResults.itemCount + itemCount;
|
||||
queryResults.lastItemIndex = lastItemIndex;
|
||||
queryResults.documents = queryResults.documents.concat(documents);
|
||||
if (queryResults.hasMoreResults) {
|
||||
return doRequest(queryResults.lastItemIndex + 1);
|
||||
}
|
||||
return queryResults;
|
||||
};
|
||||
|
||||
return doRequest(0);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ describe("Collection Add and Delete SQL spec", () => {
|
||||
// validate created
|
||||
// open database menu
|
||||
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
|
||||
await frame.waitFor(LOADING_STATE_DELAY);
|
||||
await frame.waitFor(CREATE_DELAY);
|
||||
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
|
||||
const databases = await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`);
|
||||
const selectedDbId = await frame.evaluate(element => {
|
||||
|
||||
Reference in New Issue
Block a user