mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-02-04 01:24:31 +00:00
Compare commits
5 Commits
hosted-msa
...
explorer-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f8c36bbf0 | ||
|
|
661fb66f7b | ||
|
|
2eabc377b0 | ||
|
|
1aa3fb8e7b | ||
|
|
57aef782d8 |
@@ -42,6 +42,7 @@ src/Contracts/ViewModels.ts
|
||||
src/Controls/Heatmap/Heatmap.test.ts
|
||||
src/Controls/Heatmap/Heatmap.ts
|
||||
src/Controls/Heatmap/HeatmapDatatypes.ts
|
||||
src/Definitions/adal.d.ts
|
||||
src/Definitions/datatables.d.ts
|
||||
src/Definitions/gif.d.ts
|
||||
src/Definitions/globals.d.ts
|
||||
|
||||
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@@ -101,7 +101,6 @@ jobs:
|
||||
PLATFORM: "Emulator"
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: screenshots
|
||||
path: failed-*
|
||||
@@ -160,14 +159,13 @@ 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, accessibility]
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
@@ -191,7 +189,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, accessibility]
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
|
||||
@@ -213,28 +211,3 @@ 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
BIN
.vs/slnx.sqlite
Binary file not shown.
1963
externals/adal.js
vendored
Normal file
1963
externals/adal.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
27
package-lock.json
generated
27
package-lock.json
generated
@@ -275,27 +275,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@azure/msal-browser": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.8.0.tgz",
|
||||
"integrity": "sha512-I6n7EQnwsZXgKPOLlS5X48jhzUNUFwMVm180wDBA/pwEkUy8ei6zWiPMBfWaMSxz9uNx9WHaEhgAyhJy0ze3AQ==",
|
||||
"requires": {
|
||||
"@azure/msal-common": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@azure/msal-common": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-2.0.0.tgz",
|
||||
"integrity": "sha512-d1RNcJb+P1EGzMHtgbZoVlHLQWjlVfr504jywNk9YEfoq8Hw3BxJ0wepu+1w0hc64D8zG0wljcvHaIH1jTn2SA==",
|
||||
"requires": {
|
||||
"debug": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"@azure/msal-react": {
|
||||
"version": "1.0.0-alpha.1",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-react/-/msal-react-1.0.0-alpha.1.tgz",
|
||||
"integrity": "sha512-8BftMP1DyXf7/Fa7mxi14/fmHBdDGDUONmE8sm1T6w7ERJyY1RN7PZgdnUOcYcqj2xMnxfz9++8HsrzMrtMc0Q=="
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
|
||||
@@ -5469,6 +5448,12 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz",
|
||||
"integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA=="
|
||||
},
|
||||
"adal-angular": {
|
||||
"version": "1.0.15",
|
||||
"resolved": "https://registry.npmjs.org/adal-angular/-/adal-angular-1.0.15.tgz",
|
||||
"integrity": "sha1-8qnvgvNYxEToMUKs5l0yJ6RBBDs=",
|
||||
"dev": true
|
||||
},
|
||||
"agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
|
||||
@@ -6,10 +6,8 @@
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "3.9.0",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "1.1.0",
|
||||
"@azure/msal-browser": "2.8.0",
|
||||
"@azure/msal-react": "1.0.0-alpha.1",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@jupyterlab/services": "6.0.0-rc.2",
|
||||
"@jupyterlab/terminal": "3.0.0-rc.2",
|
||||
"@microsoft/applicationinsights-web": "2.5.9",
|
||||
@@ -130,6 +128,7 @@
|
||||
"@types/webfontloader": "1.6.29",
|
||||
"@typescript-eslint/eslint-plugin": "4.0.1",
|
||||
"@typescript-eslint/parser": "4.0.1",
|
||||
"adal-angular": "1.0.15",
|
||||
"axe-puppeteer": "1.1.0",
|
||||
"babel-jest": "24.9.0",
|
||||
"babel-loader": "8.1.0",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
169
src/Common/DataAccessUtilityBase.ts
Normal file
169
src/Common/DataAccessUtilityBase.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { ConflictDefinition, FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import Q from "q";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
||||
import * as Constants from "./Constants";
|
||||
import { client } from "./CosmosClient";
|
||||
|
||||
export function getCommonQueryOptions(options: FeedOptions): any {
|
||||
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
|
||||
options = options || {};
|
||||
options.populateQueryMetrics = true;
|
||||
options.enableScanInQuery = options.enableScanInQuery || true;
|
||||
if (!options.partitionKey) {
|
||||
options.forceQueryPlan = true;
|
||||
}
|
||||
options.maxItemCount =
|
||||
options.maxItemCount ||
|
||||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
|
||||
Constants.Queries.itemsPerPage;
|
||||
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function queryDocuments(
|
||||
databaseId: string,
|
||||
containerId: string,
|
||||
query: string,
|
||||
options: any
|
||||
): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
|
||||
options = getCommonQueryOptions(options);
|
||||
const documentsIterator = client()
|
||||
.database(databaseId)
|
||||
.container(containerId)
|
||||
.items.query(query, options);
|
||||
return Q(documentsIterator);
|
||||
}
|
||||
|
||||
export function getPartitionKeyHeaderForConflict(conflictId: ConflictId): Object {
|
||||
const partitionKeyDefinition: DataModels.PartitionKey = conflictId.partitionKey;
|
||||
const partitionKeyValue: any = conflictId.partitionKeyValue;
|
||||
|
||||
return getPartitionKeyHeader(partitionKeyDefinition, partitionKeyValue);
|
||||
}
|
||||
|
||||
export function getPartitionKeyHeader(partitionKeyDefinition: DataModels.PartitionKey, partitionKeyValue: any): Object {
|
||||
if (!partitionKeyDefinition) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (partitionKeyValue === undefined) {
|
||||
return [{}];
|
||||
}
|
||||
|
||||
return [partitionKeyValue];
|
||||
}
|
||||
|
||||
export function updateDocument(
|
||||
collection: ViewModels.CollectionBase,
|
||||
documentId: DocumentId,
|
||||
newDocument: any
|
||||
): Q.Promise<any> {
|
||||
const partitionKey = documentId.partitionKeyValue;
|
||||
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), partitionKey)
|
||||
.replace(newDocument)
|
||||
.then(response => response.resource)
|
||||
);
|
||||
}
|
||||
|
||||
export function executeStoredProcedure(
|
||||
collection: ViewModels.Collection,
|
||||
storedProcedure: StoredProcedure,
|
||||
partitionKeyValue: any,
|
||||
params: any[]
|
||||
): Q.Promise<any> {
|
||||
// TODO remove this deferred. Kept it because of timeout code at bottom of function
|
||||
const deferred = Q.defer<any>();
|
||||
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.storedProcedure(storedProcedure.id())
|
||||
.execute(partitionKeyValue, params, { enableScriptLogging: true })
|
||||
.then(response =>
|
||||
deferred.resolve({
|
||||
result: response.resource,
|
||||
scriptLogs: response.headers[Constants.HttpHeaders.scriptLogResults]
|
||||
})
|
||||
)
|
||||
.catch(error => deferred.reject(error));
|
||||
|
||||
return deferred.promise.timeout(
|
||||
Constants.ClientDefaults.requestTimeoutMs,
|
||||
`Request timed out while executing stored procedure ${storedProcedure.id()}`
|
||||
);
|
||||
}
|
||||
|
||||
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.items.create(newDocument)
|
||||
.then(response => response.resource)
|
||||
);
|
||||
}
|
||||
|
||||
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
|
||||
const partitionKey = documentId.partitionKeyValue;
|
||||
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), partitionKey)
|
||||
.read()
|
||||
.then(response => response.resource)
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
|
||||
const partitionKey = documentId.partitionKeyValue;
|
||||
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), partitionKey)
|
||||
.delete()
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteConflict(
|
||||
collection: ViewModels.CollectionBase,
|
||||
conflictId: ConflictId,
|
||||
options: any = {}
|
||||
): Q.Promise<any> {
|
||||
options.partitionKey = options.partitionKey || getPartitionKeyHeaderForConflict(conflictId);
|
||||
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.conflict(conflictId.id())
|
||||
.delete(options)
|
||||
);
|
||||
}
|
||||
|
||||
export function queryConflicts(
|
||||
databaseId: string,
|
||||
containerId: string,
|
||||
query: string,
|
||||
options: any
|
||||
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
|
||||
const documentsIterator = client()
|
||||
.database(databaseId)
|
||||
.container(containerId)
|
||||
.conflicts.query(query, options);
|
||||
return Q(documentsIterator);
|
||||
}
|
||||
217
src/Common/DocumentClientUtilityBase.ts
Normal file
217
src/Common/DocumentClientUtilityBase.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import Q from "q";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||
import * as Constants from "./Constants";
|
||||
import * as DataAccessUtilityBase from "./DataAccessUtilityBase";
|
||||
import { MinimalQueryIterator, nextPage } from "./IteratorUtilities";
|
||||
import { handleError } from "./ErrorHandlingUtils";
|
||||
|
||||
// TODO: Log all promise resolutions and errors with verbosity levels
|
||||
export function queryDocuments(
|
||||
databaseId: string,
|
||||
containerId: string,
|
||||
query: string,
|
||||
options: any
|
||||
): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
|
||||
return DataAccessUtilityBase.queryDocuments(databaseId, containerId, query, options);
|
||||
}
|
||||
|
||||
export function queryConflicts(
|
||||
databaseId: string,
|
||||
containerId: string,
|
||||
query: string,
|
||||
options: any
|
||||
): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
|
||||
return DataAccessUtilityBase.queryConflicts(databaseId, containerId, query, options);
|
||||
}
|
||||
|
||||
export function getEntityName() {
|
||||
const defaultExperience =
|
||||
window.dataExplorer && window.dataExplorer.defaultExperience && window.dataExplorer.defaultExperience();
|
||||
if (defaultExperience === Constants.DefaultAccountExperience.MongoDB) {
|
||||
return "document";
|
||||
}
|
||||
return "item";
|
||||
}
|
||||
|
||||
export function executeStoredProcedure(
|
||||
collection: ViewModels.Collection,
|
||||
storedProcedure: StoredProcedure,
|
||||
partitionKeyValue: any,
|
||||
params: any[]
|
||||
): Q.Promise<any> {
|
||||
var deferred = Q.defer<any>();
|
||||
|
||||
const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`);
|
||||
DataAccessUtilityBase.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params)
|
||||
.then(
|
||||
(response: any) => {
|
||||
deferred.resolve(response);
|
||||
logConsoleInfo(
|
||||
`Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
|
||||
);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(
|
||||
error,
|
||||
"ExecuteStoredProcedure",
|
||||
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
|
||||
);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function queryDocumentsPage(
|
||||
resourceName: string,
|
||||
documentsIterator: MinimalQueryIterator,
|
||||
firstItemIndex: number,
|
||||
options: any
|
||||
): Q.Promise<ViewModels.QueryResults> {
|
||||
var deferred = Q.defer<ViewModels.QueryResults>();
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
|
||||
Q(nextPage(documentsIterator, firstItemIndex))
|
||||
.then(
|
||||
(result: ViewModels.QueryResults) => {
|
||||
const itemCount = (result.documents && result.documents.length) || 0;
|
||||
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
||||
deferred.resolve(result);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function readDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
|
||||
var deferred = Q.defer<any>();
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`);
|
||||
DataAccessUtilityBase.readDocument(collection, documentId)
|
||||
.then(
|
||||
(document: any) => {
|
||||
deferred.resolve(document);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function updateDocument(
|
||||
collection: ViewModels.CollectionBase,
|
||||
documentId: DocumentId,
|
||||
newDocument: any
|
||||
): Q.Promise<any> {
|
||||
var deferred = Q.defer<any>();
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`);
|
||||
DataAccessUtilityBase.updateDocument(collection, documentId, newDocument)
|
||||
.then(
|
||||
(updatedDocument: any) => {
|
||||
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
|
||||
deferred.resolve(updatedDocument);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function createDocument(collection: ViewModels.CollectionBase, newDocument: any): Q.Promise<any> {
|
||||
var deferred = Q.defer<any>();
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`);
|
||||
DataAccessUtilityBase.createDocument(collection, newDocument)
|
||||
.then(
|
||||
(savedDocument: any) => {
|
||||
logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`);
|
||||
deferred.resolve(savedDocument);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
|
||||
var deferred = Q.defer<any>();
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`);
|
||||
DataAccessUtilityBase.deleteDocument(collection, documentId)
|
||||
.then(
|
||||
(response: any) => {
|
||||
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
|
||||
deferred.resolve(response);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function deleteConflict(
|
||||
collection: ViewModels.CollectionBase,
|
||||
conflictId: ConflictId,
|
||||
options?: any
|
||||
): Q.Promise<any> {
|
||||
var deferred = Q.defer<any>();
|
||||
|
||||
const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`);
|
||||
DataAccessUtilityBase.deleteConflict(collection, conflictId, options)
|
||||
.then(
|
||||
(response: any) => {
|
||||
logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`);
|
||||
deferred.resolve(response);
|
||||
},
|
||||
(error: any) => {
|
||||
handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
export const getEntityName = (): string => {
|
||||
if (userContext.defaultExperience === DefaultAccountExperienceType.MongoDB) {
|
||||
return "document";
|
||||
}
|
||||
|
||||
return "item";
|
||||
};
|
||||
@@ -3,18 +3,16 @@ 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 { queryDocumentsPage } from "./dataAccess/queryDocumentsPage";
|
||||
import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase";
|
||||
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 = {
|
||||
@@ -33,7 +31,10 @@ export class QueriesClient {
|
||||
return Promise.resolve(queriesCollection.rawDataModel);
|
||||
}
|
||||
|
||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress("Setting up account for saving queries");
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
"Setting up account for saving queries"
|
||||
);
|
||||
return createCollection({
|
||||
collectionId: SavedQueries.CollectionName,
|
||||
createNewDatabase: true,
|
||||
@@ -44,7 +45,10 @@ export class QueriesClient {
|
||||
})
|
||||
.then(
|
||||
(collection: DataModels.Collection) => {
|
||||
NotificationConsoleUtils.logConsoleInfo("Successfully set up account for saving queries");
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
"Successfully set up account for saving queries"
|
||||
);
|
||||
return Promise.resolve(collection);
|
||||
},
|
||||
(error: any) => {
|
||||
@@ -52,14 +56,17 @@ export class QueriesClient {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => clearMessage());
|
||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||
}
|
||||
|
||||
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.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to save query ${query.queryName}: ${errorMessage}`
|
||||
);
|
||||
return Promise.reject(errorMessage);
|
||||
}
|
||||
|
||||
@@ -67,16 +74,25 @@ export class QueriesClient {
|
||||
this.validateQuery(query);
|
||||
} catch (error) {
|
||||
const errorMessage: string = "Invalid query specified";
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to save query ${query.queryName}: ${errorMessage}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to save query ${query.queryName}: ${errorMessage}`
|
||||
);
|
||||
return Promise.reject(errorMessage);
|
||||
}
|
||||
|
||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Saving query ${query.queryName}`);
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Saving query ${query.queryName}`
|
||||
);
|
||||
query.id = query.queryName;
|
||||
return createDocument(queriesCollection, query)
|
||||
.then(
|
||||
(savedQuery: DataModels.Query) => {
|
||||
NotificationConsoleUtils.logConsoleInfo(`Successfully saved query ${query.queryName}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully saved query ${query.queryName}`
|
||||
);
|
||||
return Promise.resolve();
|
||||
},
|
||||
(error: any) => {
|
||||
@@ -87,65 +103,74 @@ export class QueriesClient {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => clearMessage());
|
||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||
}
|
||||
|
||||
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.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to fetch saved queries: ${errorMessage}`
|
||||
);
|
||||
return Promise.reject(errorMessage);
|
||||
}
|
||||
|
||||
const options: any = { enableCrossPartitionQuery: true };
|
||||
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)
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Fetching saved queries");
|
||||
return queryDocuments(SavedQueries.DatabaseName, SavedQueries.CollectionName, this.fetchQueriesQuery(), options)
|
||||
.then(
|
||||
(results: ViewModels.QueryResults) => {
|
||||
let queries: DataModels.Query[] = _.map(results.documents, (document: DataModels.Query) => {
|
||||
if (!document) {
|
||||
return undefined;
|
||||
(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);
|
||||
}
|
||||
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(() => clearMessage());
|
||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||
}
|
||||
|
||||
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.logConsoleError(`Failed to fetch saved queries: ${errorMessage}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to fetch saved queries: ${errorMessage}`
|
||||
);
|
||||
return Promise.reject(errorMessage);
|
||||
}
|
||||
|
||||
@@ -153,10 +178,16 @@ export class QueriesClient {
|
||||
this.validateQuery(query);
|
||||
} catch (error) {
|
||||
const errorMessage: string = "Invalid query specified";
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to delete query ${query.queryName}: ${errorMessage}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to delete query ${query.queryName}: ${errorMessage}`
|
||||
);
|
||||
}
|
||||
|
||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Deleting query ${query.queryName}`);
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Deleting query ${query.queryName}`
|
||||
);
|
||||
query.id = query.queryName;
|
||||
const documentId = new DocumentId(
|
||||
{
|
||||
@@ -170,7 +201,10 @@ export class QueriesClient {
|
||||
return deleteDocument(queriesCollection, documentId)
|
||||
.then(
|
||||
() => {
|
||||
NotificationConsoleUtils.logConsoleInfo(`Successfully deleted query ${query.queryName}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully deleted query ${query.queryName}`
|
||||
);
|
||||
return Promise.resolve();
|
||||
},
|
||||
(error: any) => {
|
||||
@@ -178,7 +212,7 @@ export class QueriesClient {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => clearMessage());
|
||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||
}
|
||||
|
||||
public getResourceId(): string {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
|
||||
export const createDocument = async (collection: CollectionBase, newDocument: unknown): Promise<unknown> => {
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Creating new ${entityName} for container ${collection.id()}`);
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.items.create(newDocument);
|
||||
|
||||
logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`);
|
||||
return response?.resource;
|
||||
} catch (error) {
|
||||
handleError(error, "CreateDocument", `Error while creating new ${entityName} for container ${collection.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import ConflictId from "../../Explorer/Tree/ConflictId";
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import { RequestOptions } from "@azure/cosmos";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
|
||||
export const deleteConflict = async (collection: CollectionBase, conflictId: ConflictId): Promise<void> => {
|
||||
const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`);
|
||||
|
||||
try {
|
||||
const options = {
|
||||
partitionKey: getPartitionKeyHeaderForConflict(conflictId)
|
||||
};
|
||||
|
||||
await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.conflict(conflictId.id())
|
||||
.delete(options as RequestOptions);
|
||||
logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`);
|
||||
} catch (error) {
|
||||
handleError(error, "DeleteConflict", `Error while deleting conflict ${conflictId.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
|
||||
const getPartitionKeyHeaderForConflict = (conflictId: ConflictId): unknown => {
|
||||
if (!conflictId.partitionKey) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return conflictId.partitionKeyValue === undefined ? [{}] : [conflictId.partitionKeyValue];
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
|
||||
export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<void> => {
|
||||
const entityName: string = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Deleting ${entityName} ${documentId.id()}`);
|
||||
|
||||
try {
|
||||
await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), documentId.partitionKeyValue)
|
||||
.delete();
|
||||
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
|
||||
} catch (error) {
|
||||
handleError(error, "DeleteDocument", `Error while deleting ${entityName} ${documentId.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Collection } from "../../Contracts/ViewModels";
|
||||
import { ClientDefaults, HttpHeaders } from "../Constants";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
import StoredProcedure from "../../Explorer/Tree/StoredProcedure";
|
||||
|
||||
export interface ExecuteSprocResult {
|
||||
result: StoredProcedure;
|
||||
scriptLogs: string;
|
||||
}
|
||||
|
||||
export const executeStoredProcedure = async (
|
||||
collection: Collection,
|
||||
storedProcedure: StoredProcedure,
|
||||
partitionKeyValue: string,
|
||||
params: string[]
|
||||
): Promise<ExecuteSprocResult> => {
|
||||
const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`);
|
||||
const timeout = setTimeout(() => {
|
||||
throw Error(`Request timed out while executing stored procedure ${storedProcedure.id()}`);
|
||||
}, ClientDefaults.requestTimeoutMs);
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.storedProcedure(storedProcedure.id())
|
||||
.execute(partitionKeyValue, params, { enableScriptLogging: true });
|
||||
clearTimeout(timeout);
|
||||
logConsoleInfo(
|
||||
`Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
|
||||
);
|
||||
return {
|
||||
result: response.resource,
|
||||
scriptLogs: response.headers[HttpHeaders.scriptLogResults] as string
|
||||
};
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
"ExecuteStoredProcedure",
|
||||
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import { ConflictDefinition, FeedOptions, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { client } from "../CosmosClient";
|
||||
|
||||
export const queryConflicts = (
|
||||
databaseId: string,
|
||||
containerId: string,
|
||||
query: string,
|
||||
options: FeedOptions
|
||||
): QueryIterator<ConflictDefinition & Resource> => {
|
||||
return client()
|
||||
.database(databaseId)
|
||||
.container(containerId)
|
||||
.conflicts.query(query, options);
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Queries } from "../Constants";
|
||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
import { client } from "../CosmosClient";
|
||||
|
||||
export const queryDocuments = (
|
||||
databaseId: string,
|
||||
containerId: string,
|
||||
query: string,
|
||||
options: FeedOptions
|
||||
): QueryIterator<ItemDefinition & Resource> => {
|
||||
options = getCommonQueryOptions(options);
|
||||
return client()
|
||||
.database(databaseId)
|
||||
.container(containerId)
|
||||
.items.query(query, options);
|
||||
};
|
||||
|
||||
export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
|
||||
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
|
||||
options = options || {};
|
||||
options.populateQueryMetrics = true;
|
||||
options.enableScanInQuery = options.enableScanInQuery || true;
|
||||
if (!options.partitionKey) {
|
||||
options.forceQueryPlan = true;
|
||||
}
|
||||
options.maxItemCount =
|
||||
options.maxItemCount ||
|
||||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
|
||||
Queries.itemsPerPage;
|
||||
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
||||
|
||||
return options;
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import { QueryResults } from "../../Contracts/ViewModels";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { MinimalQueryIterator, nextPage } from "../IteratorUtilities";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
|
||||
export const queryDocumentsPage = async (
|
||||
resourceName: string,
|
||||
documentsIterator: MinimalQueryIterator,
|
||||
firstItemIndex: number
|
||||
): Promise<QueryResults> => {
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
|
||||
|
||||
try {
|
||||
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex);
|
||||
const itemCount = (result.documents && result.documents.length) || 0;
|
||||
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Item } from "@azure/cosmos";
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
|
||||
export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<Item> => {
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Reading ${entityName} ${documentId.id()}`);
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), documentId.partitionKeyValue)
|
||||
.read();
|
||||
|
||||
return response?.resource;
|
||||
} catch (error) {
|
||||
handleError(error, "ReadDocument", `Failed to read ${entityName} ${documentId.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { CollectionBase } from "../../Contracts/ViewModels";
|
||||
import { Item } from "@azure/cosmos";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
|
||||
export const updateDocument = async (
|
||||
collection: CollectionBase,
|
||||
documentId: DocumentId,
|
||||
newDocument: Item
|
||||
): Promise<Item> => {
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`);
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), documentId.partitionKeyValue)
|
||||
.replace(newDocument);
|
||||
|
||||
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
|
||||
return response?.resource;
|
||||
} catch (error) {
|
||||
handleError(error, "UpdateDocument", `Failed to update ${entityName} ${documentId.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -26,6 +26,7 @@ interface ConfigContext {
|
||||
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
||||
hostedExplorerURL: string;
|
||||
armAPIVersion?: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
// Default configuration
|
||||
|
||||
@@ -362,7 +362,7 @@ export enum CollectionTabKind {
|
||||
Gallery = 17,
|
||||
NotebookViewer = 18,
|
||||
Schema = 19,
|
||||
SettingsV2 = 20
|
||||
SettingsV2 = 19
|
||||
}
|
||||
|
||||
export enum TerminalKind {
|
||||
|
||||
383
src/Definitions/adal.d.ts
vendored
Normal file
383
src/Definitions/adal.d.ts
vendored
Normal file
@@ -0,0 +1,383 @@
|
||||
// Type definitions for adal-angular 1.0.1.1
|
||||
// Project: https://github.com/AzureAD/azure-activedirectory-library-for-js#readme
|
||||
// Definitions by: Daniel Perez Alvarez <https://github.com/unindented>
|
||||
// Anthony Ciccarello <https://github.com/aciccarello>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
|
||||
//This is a customized version of adal on top of version 1.0.1 which does not support multi tenant
|
||||
// Customized version add tenantId to stored tokens so when tenant change, adal will refetch instead of read from sessionStorage
|
||||
|
||||
// In module contexts the class constructor function is the exported object
|
||||
// export = AuthenticationContext;
|
||||
|
||||
// This class is defined globally in not in a module context
|
||||
declare class AuthenticationContext {
|
||||
instance: string;
|
||||
config: AuthenticationContext.Options;
|
||||
callback: AuthenticationContext.TokenCallback;
|
||||
popUp: boolean;
|
||||
isAngular: boolean;
|
||||
|
||||
/**
|
||||
* Enum for request type
|
||||
*/
|
||||
REQUEST_TYPE: AuthenticationContext.RequestType;
|
||||
RESPONSE_TYPE: AuthenticationContext.ResponseType;
|
||||
CONSTANTS: AuthenticationContext.Constants;
|
||||
|
||||
constructor(options: AuthenticationContext.Options);
|
||||
/**
|
||||
* Initiates the login process by redirecting the user to Azure AD authorization endpoint.
|
||||
*/
|
||||
login(): void;
|
||||
/**
|
||||
* Returns whether a login is in progress.
|
||||
*/
|
||||
loginInProgress(): boolean;
|
||||
/**
|
||||
* Gets token for the specified resource from the cache.
|
||||
* @param resource A URI that identifies the resource for which the token is requested.
|
||||
* @param tenantId tenant Id.
|
||||
*/
|
||||
getCachedToken(resource: string, tenantId: string): string;
|
||||
/**
|
||||
* If user object exists, returns it. Else creates a new user object by decoding `id_token` from the cache.
|
||||
*/
|
||||
getCachedUser(): AuthenticationContext.UserInfo;
|
||||
/**
|
||||
* Adds the passed callback to the array of callbacks for the specified resource.
|
||||
* @param resource A URI that identifies the resource for which the token is requested.
|
||||
* @param expectedState A unique identifier (guid).
|
||||
* @param callback The callback provided by the caller. It will be called with token or error.
|
||||
*/
|
||||
registerCallback(
|
||||
expectedState: string,
|
||||
resource: string,
|
||||
callback: AuthenticationContext.TokenCallback,
|
||||
tenantId: string
|
||||
): void;
|
||||
/**
|
||||
* Acquires token from the cache if it is not expired. Otherwise sends request to AAD to obtain a new token.
|
||||
* @param resource Resource URI identifying the target resource.
|
||||
* @param callback The callback provided by the caller. It will be called with token or error.
|
||||
*/
|
||||
acquireToken(resource: string, tenantId: string, callback: AuthenticationContext.TokenCallback): void;
|
||||
/**
|
||||
* Acquires token (interactive flow using a popup window) by sending request to AAD to obtain a new token.
|
||||
* @param resource Resource URI identifying the target resource.
|
||||
* @param extraQueryParameters Query parameters to add to the authentication request.
|
||||
* @param claims Claims to add to the authentication request.
|
||||
* @param callback The callback provided by the caller. It will be called with token or error.
|
||||
*/
|
||||
acquireTokenPopup(
|
||||
resource: string,
|
||||
tenantId: string,
|
||||
extraQueryParameters: string | null | undefined,
|
||||
claims: string | null | undefined,
|
||||
callback: AuthenticationContext.TokenCallback
|
||||
): void;
|
||||
/**
|
||||
* Acquires token (interactive flow using a redirect) by sending request to AAD to obtain a new token. In this case the callback passed in the authentication request constructor will be called.
|
||||
* @param resource Resource URI identifying the target resource.
|
||||
* @param extraQueryParameters Query parameters to add to the authentication request.
|
||||
* @param claims Claims to add to the authentication request.
|
||||
*/
|
||||
acquireTokenRedirect(
|
||||
resource: string,
|
||||
tenantId: string,
|
||||
extraQueryParameters?: string | null,
|
||||
claims?: string | null
|
||||
): void;
|
||||
/**
|
||||
* Redirects the browser to Azure AD authorization endpoint.
|
||||
* @param urlNavigate URL of the authorization endpoint.
|
||||
*/
|
||||
promptUser(urlNavigate: string): void;
|
||||
/**
|
||||
* Clears cache items.
|
||||
*/
|
||||
clearCache(): void;
|
||||
/**
|
||||
* Clears cache items for a given resource.
|
||||
* @param resource Resource URI identifying the target resource.
|
||||
*/
|
||||
clearCacheForResource(resource: string): void;
|
||||
/**
|
||||
* Redirects user to logout endpoint. After logout, it will redirect to `postLogoutRedirectUri` if added as a property on the config object.
|
||||
*/
|
||||
logOut(): void;
|
||||
/**
|
||||
* Calls the passed in callback with the user object or error message related to the user.
|
||||
* @param callback The callback provided by the caller. It will be called with user or error.
|
||||
*/
|
||||
getUser(callback: AuthenticationContext.UserCallback): void;
|
||||
/**
|
||||
* Checks if the URL fragment contains access token, id token or error description.
|
||||
* @param hash Hash passed from redirect page.
|
||||
*/
|
||||
isCallback(hash: string): boolean;
|
||||
/**
|
||||
* Gets login error.
|
||||
*/
|
||||
getLoginError(): string;
|
||||
/**
|
||||
* Creates a request info object from the URL fragment and returns it.
|
||||
*/
|
||||
getRequestInfo(hash: string): AuthenticationContext.RequestInfo;
|
||||
/**
|
||||
* Saves token or error received in the response from AAD in the cache. In case of `id_token`, it also creates the user object.
|
||||
*/
|
||||
saveTokenFromHash(requestInfo: AuthenticationContext.RequestInfo): void;
|
||||
/**
|
||||
* Gets resource for given endpoint if mapping is provided with config.
|
||||
* @param endpoint Resource URI identifying the target resource.
|
||||
*/
|
||||
getResourceForEndpoint(resource: string): string;
|
||||
/**
|
||||
* This method must be called for processing the response received from AAD. It extracts the hash, processes the token or error, saves it in the cache and calls the callbacks with the result.
|
||||
* @param hash Hash fragment of URL. Defaults to `window.location.hash`.
|
||||
*/
|
||||
handleWindowCallback(hash?: string): void;
|
||||
|
||||
/**
|
||||
* Checks the logging Level, constructs the log message and logs it. Users need to implement/override this method to turn on logging.
|
||||
* @param level Level can be set 0, 1, 2 and 3 which turns on 'error', 'warning', 'info' or 'verbose' level logging respectively.
|
||||
* @param message Message to log.
|
||||
* @param error Error to log.
|
||||
*/
|
||||
log(level: AuthenticationContext.LoggingLevel, message: string, error: any): void;
|
||||
/**
|
||||
* Logs messages when logging level is set to 0.
|
||||
* @param message Message to log.
|
||||
* @param error Error to log.
|
||||
*/
|
||||
error(message: string, error: any): void;
|
||||
/**
|
||||
* Logs messages when logging level is set to 1.
|
||||
* @param message Message to log.
|
||||
*/
|
||||
warn(message: string): void;
|
||||
/**
|
||||
* Logs messages when logging level is set to 2.
|
||||
* @param message Message to log.
|
||||
*/
|
||||
info(message: string): void;
|
||||
/**
|
||||
* Logs messages when logging level is set to 3.
|
||||
* @param message Message to log.
|
||||
*/
|
||||
verbose(message: string): void;
|
||||
|
||||
/**
|
||||
* Logs Pii messages when Logging Level is set to 0 and window.piiLoggingEnabled is set to true.
|
||||
* @param message Message to log.
|
||||
* @param error Error to log.
|
||||
*/
|
||||
errorPii(message: string, error: any): void;
|
||||
|
||||
/**
|
||||
* Logs Pii messages when Logging Level is set to 1 and window.piiLoggingEnabled is set to true.
|
||||
* @param message Message to log.
|
||||
*/
|
||||
warnPii(message: string): void;
|
||||
|
||||
/**
|
||||
* Logs messages when Logging Level is set to 2 and window.piiLoggingEnabled is set to true.
|
||||
* @param message Message to log.
|
||||
*/
|
||||
infoPii(message: string): void;
|
||||
|
||||
/**
|
||||
* Logs messages when Logging Level is set to 3 and window.piiLoggingEnabled is set to true.
|
||||
* @param message Message to log.
|
||||
*/
|
||||
verbosePii(message: string): void;
|
||||
}
|
||||
|
||||
declare namespace AuthenticationContext {
|
||||
function inject(config: Options): AuthenticationContext;
|
||||
|
||||
type LoggingLevel = 0 | 1 | 2 | 3;
|
||||
|
||||
type RequestType = "LOGIN" | "RENEW_TOKEN" | "UNKNOWN";
|
||||
|
||||
type ResponseType = "id_token token" | "token";
|
||||
|
||||
interface RequestInfo {
|
||||
/**
|
||||
* Object comprising of fields such as id_token/error, session_state, state, e.t.c.
|
||||
*/
|
||||
parameters: any;
|
||||
/**
|
||||
* Request type.
|
||||
*/
|
||||
requestType: RequestType;
|
||||
/**
|
||||
* Whether state is valid.
|
||||
*/
|
||||
stateMatch: boolean;
|
||||
/**
|
||||
* Unique guid used to match the response with the request.
|
||||
*/
|
||||
stateResponse: string;
|
||||
/**
|
||||
* Whether `requestType` contains `id_token`, `access_token` or error.
|
||||
*/
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
interface UserInfo {
|
||||
/**
|
||||
* Username assigned from UPN or email.
|
||||
*/
|
||||
userName: string;
|
||||
/**
|
||||
* Properties parsed from `id_token`.
|
||||
*/
|
||||
profile: any;
|
||||
}
|
||||
|
||||
type TokenCallback = (errorDesc: string | null, token: string | null, error: any) => void;
|
||||
|
||||
type UserCallback = (errorDesc: string | null, user: UserInfo | null) => void;
|
||||
|
||||
/**
|
||||
* Configuration options for Authentication Context
|
||||
*/
|
||||
interface Options {
|
||||
/**
|
||||
* Client ID assigned to your app by Azure Active Directory.
|
||||
*/
|
||||
clientId: string;
|
||||
/**
|
||||
* Endpoint at which you expect to receive tokens.Defaults to `window.location.href`.
|
||||
*/
|
||||
redirectUri?: string;
|
||||
/**
|
||||
* Azure Active Directory instance. Defaults to `https://login.microsoftonline.com/`.
|
||||
*/
|
||||
instance?: string;
|
||||
/**
|
||||
* Your target tenant. Defaults to `common`.
|
||||
*/
|
||||
tenant?: string;
|
||||
/**
|
||||
* Query parameters to add to the authentication request.
|
||||
*/
|
||||
extraQueryParameter?: string;
|
||||
/**
|
||||
* Unique identifier used to map the request with the response. Defaults to RFC4122 version 4 guid (128 bits).
|
||||
*/
|
||||
correlationId?: string;
|
||||
/**
|
||||
* User defined function of handling the navigation to Azure AD authorization endpoint in case of login.
|
||||
*/
|
||||
displayCall?: (url: string) => void;
|
||||
/**
|
||||
* Set this to true to enable login in a popup winodow instead of a full redirect. Defaults to `false`.
|
||||
*/
|
||||
popUp?: boolean;
|
||||
/**
|
||||
* Set this to the resource to request on login. Defaults to `clientId`.
|
||||
*/
|
||||
loginResource?: string;
|
||||
/**
|
||||
* Set this to redirect the user to a custom login page.
|
||||
*/
|
||||
localLoginUrl?: string;
|
||||
/**
|
||||
* Redirects to start page after login. Defaults to `true`.
|
||||
*/
|
||||
navigateToLoginRequestUrl?: boolean;
|
||||
/**
|
||||
* Set this to redirect the user to a custom logout page.
|
||||
*/
|
||||
logOutUri?: string;
|
||||
/**
|
||||
* Redirects the user to postLogoutRedirectUri after logout. Defaults to `redirectUri`.
|
||||
*/
|
||||
postLogoutRedirectUri?: string;
|
||||
/**
|
||||
* Sets browser storage to either 'localStorage' or sessionStorage'. Defaults to `sessionStorage`.
|
||||
*/
|
||||
cacheLocation?: "localStorage" | "sessionStorage";
|
||||
/**
|
||||
* Array of keywords or URIs. Adal will attach a token to outgoing requests that have these keywords or URIs.
|
||||
*/
|
||||
endpoints?: { [resource: string]: string };
|
||||
/**
|
||||
* Array of keywords or URIs. Adal will not attach a token to outgoing requests that have these keywords or URIs.
|
||||
*/
|
||||
anonymousEndpoints?: string[];
|
||||
/**
|
||||
* If the cached token is about to be expired in the expireOffsetSeconds (in seconds), Adal will renew the token instead of using the cached token. Defaults to 300 seconds.
|
||||
*/
|
||||
expireOffsetSeconds?: number;
|
||||
/**
|
||||
* The number of milliseconds of inactivity before a token renewal response from AAD should be considered timed out. Defaults to 6 seconds.
|
||||
*/
|
||||
loadFrameTimeout?: number;
|
||||
/**
|
||||
* Callback to be invoked when a token is acquired.
|
||||
*/
|
||||
callback?: TokenCallback;
|
||||
}
|
||||
|
||||
interface LoggingConfig {
|
||||
level: LoggingLevel;
|
||||
log: (message: string) => void;
|
||||
piiLoggingEnabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum for storage constants
|
||||
*/
|
||||
interface Constants {
|
||||
ACCESS_TOKEN: "access_token";
|
||||
EXPIRES_IN: "expires_in";
|
||||
ID_TOKEN: "id_token";
|
||||
ERROR_DESCRIPTION: "error_description";
|
||||
SESSION_STATE: "session_state";
|
||||
STORAGE: {
|
||||
TOKEN_KEYS: "adal.token.keys";
|
||||
ACCESS_TOKEN_KEY: "adal.access.token.key";
|
||||
EXPIRATION_KEY: "adal.expiration.key";
|
||||
STATE_LOGIN: "adal.state.login";
|
||||
STATE_RENEW: "adal.state.renew";
|
||||
NONCE_IDTOKEN: "adal.nonce.idtoken";
|
||||
SESSION_STATE: "adal.session.state";
|
||||
USERNAME: "adal.username";
|
||||
IDTOKEN: "adal.idtoken";
|
||||
ERROR: "adal.error";
|
||||
ERROR_DESCRIPTION: "adal.error.description";
|
||||
LOGIN_REQUEST: "adal.login.request";
|
||||
LOGIN_ERROR: "adal.login.error";
|
||||
RENEW_STATUS: "adal.token.renew.status";
|
||||
};
|
||||
RESOURCE_DELIMETER: "|";
|
||||
LOADFRAME_TIMEOUT: "6000";
|
||||
TOKEN_RENEW_STATUS_CANCELED: "Canceled";
|
||||
TOKEN_RENEW_STATUS_COMPLETED: "Completed";
|
||||
TOKEN_RENEW_STATUS_IN_PROGRESS: "In Progress";
|
||||
LOGGING_LEVEL: {
|
||||
ERROR: 0;
|
||||
WARN: 1;
|
||||
INFO: 2;
|
||||
VERBOSE: 3;
|
||||
};
|
||||
LEVEL_STRING_MAP: {
|
||||
0: "ERROR:";
|
||||
1: "WARNING:";
|
||||
2: "INFO:";
|
||||
3: "VERBOSE:";
|
||||
};
|
||||
POPUP_WIDTH: 483;
|
||||
POPUP_HEIGHT: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// declare global {
|
||||
// interface Window {
|
||||
// Logging: AuthenticationContext.LoggingConfig;
|
||||
// }
|
||||
// }
|
||||
@@ -38,7 +38,7 @@ export interface CommandButtonComponentProps {
|
||||
/**
|
||||
* Label for the button
|
||||
*/
|
||||
commandButtonLabel?: string;
|
||||
commandButtonLabel: string;
|
||||
|
||||
/**
|
||||
* True if this button opens a tab or pane, false otherwise.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { IColumn, Text } from "office-ui-fabric-react";
|
||||
import {
|
||||
getAutoPilotV3SpendElement,
|
||||
getEstimatedSpendingElement,
|
||||
getEstimatedSpendElement,
|
||||
getEstimatedAutoscaleSpendElement,
|
||||
manualToAutoscaleDisclaimerElement,
|
||||
ttlWarning,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
@@ -19,37 +19,11 @@ import {
|
||||
mongoIndexingPolicyDisclaimer,
|
||||
mongoIndexingPolicyAADError,
|
||||
mongoIndexTransformationRefreshingMessage,
|
||||
renderMongoIndexTransformationRefreshMessage,
|
||||
ManualEstimatedSpendingDisplayProps,
|
||||
PriceBreakdown,
|
||||
getRuPriceBreakdown
|
||||
renderMongoIndexTransformationRefreshMessage
|
||||
} 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)}
|
||||
@@ -57,7 +31,9 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
{getAutoPilotV3SpendElement(1000, true)}
|
||||
{getAutoPilotV3SpendElement(undefined, true)}
|
||||
|
||||
{getEstimatedSpendingElement(estimatedSpendingColumns, estimatedSpendingItems, 1000, 2, priceBreakdown, false)}
|
||||
{getEstimatedSpendElement(1000, "mooncake", 2, false)}
|
||||
|
||||
{getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)}
|
||||
|
||||
{manualToAutoscaleDisclaimerElement}
|
||||
{ttlWarning}
|
||||
@@ -93,14 +69,4 @@ 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,13 +3,14 @@ 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,
|
||||
estimatedCostDisclaimer
|
||||
calculateEstimateNumber
|
||||
} from "../../../Utils/PricingUtils";
|
||||
import {
|
||||
ITextFieldStyles,
|
||||
@@ -31,41 +32,10 @@ import {
|
||||
MessageBarType,
|
||||
Stack,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
DetailsList,
|
||||
IColumn,
|
||||
SelectionMode,
|
||||
DetailsListLayoutMode,
|
||||
IDetailsRowProps,
|
||||
DetailsRow,
|
||||
IDetailsColumnStyles
|
||||
SpinnerSize
|
||||
} 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 = {
|
||||
@@ -134,16 +104,6 @@ export const transparentDetailsRowStyles: Partial<IDetailsRowStyles> = {
|
||||
}
|
||||
};
|
||||
|
||||
export const transparentDetailsHeaderStyle: Partial<IDetailsColumnStyles> = {
|
||||
root: {
|
||||
selectors: {
|
||||
":hover": {
|
||||
background: "transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const customDetailsListStyles: Partial<IDetailsListStyles> = {
|
||||
root: {
|
||||
selectors: {
|
||||
@@ -170,10 +130,6 @@ 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,
|
||||
@@ -209,61 +165,63 @@ export const getAutoPilotV3SpendElement = (
|
||||
);
|
||||
};
|
||||
|
||||
export const getRuPriceBreakdown = (
|
||||
export const getEstimatedAutoscaleSpendElement = (
|
||||
throughput: number,
|
||||
serverId: string,
|
||||
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)
|
||||
};
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export const getEstimatedSpendingElement = (
|
||||
estimatedSpendingColumns: IColumn[],
|
||||
estimatedSpendingItems: EstimatedSpendingDisplayProps[],
|
||||
export const getEstimatedSpendElement = (
|
||||
throughput: number,
|
||||
numberOfRegions: number,
|
||||
priceBreakdown: PriceBreakdown,
|
||||
isAutoscale: boolean
|
||||
serverId: string,
|
||||
regions: number,
|
||||
multimaster: boolean
|
||||
): JSX.Element => {
|
||||
const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : "";
|
||||
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);
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -307,13 +265,6 @@ 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,6 +6,8 @@ import {
|
||||
IconButton,
|
||||
Text,
|
||||
SelectionMode,
|
||||
IDetailsRowProps,
|
||||
DetailsRow,
|
||||
IColumn,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
@@ -19,11 +21,11 @@ import {
|
||||
mongoIndexingPolicyDisclaimer,
|
||||
mediumWidthStackStyles,
|
||||
subComponentStackProps,
|
||||
transparentDetailsRowStyles,
|
||||
createAndAddMongoIndexStackProps,
|
||||
separatorStyles,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
infoAndToolTipTextStyle,
|
||||
onRenderRow
|
||||
infoAndToolTipTextStyle
|
||||
} from "../../SettingsRenderUtils";
|
||||
import { MongoIndex } from "../../../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import {
|
||||
@@ -138,6 +140,10 @@ 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
|
||||
@@ -247,7 +253,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
items={initialIndexes}
|
||||
columns={this.initialIndexesColumns}
|
||||
selectionMode={SelectionMode.none}
|
||||
onRenderRow={onRenderRow}
|
||||
onRenderRow={this.onRenderRow}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
/>
|
||||
{this.renderIndexesToBeAdded()}
|
||||
@@ -273,7 +279,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
items={indexesToBeDropped}
|
||||
columns={this.indexesToBeDroppedColumns}
|
||||
selectionMode={SelectionMode.none}
|
||||
onRenderRow={onRenderRow}
|
||||
onRenderRow={this.onRenderRow}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -165,7 +165,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
private getThroughputInputComponent = (): JSX.Element => (
|
||||
<ThroughputInputAutoPilotV3Component
|
||||
databaseAccount={this.props.container.databaseAccount()}
|
||||
serverId={this.props.container.serverId()}
|
||||
serverId={configContext.serverId}
|
||||
throughput={this.props.throughput}
|
||||
throughputBaseline={this.props.throughputBaseline}
|
||||
onThroughputChange={this.props.onThroughputChange}
|
||||
|
||||
@@ -54,6 +54,7 @@ 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", () => {
|
||||
@@ -71,7 +72,8 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
||||
|
||||
wrapper.setProps({ wasAutopilotOriginallySet: true });
|
||||
wrapper.update();
|
||||
expect(wrapper.exists("#throughputSpendElement")).toEqual(true);
|
||||
expect(wrapper.exists("#autoscaleSpendElement")).toEqual(true);
|
||||
expect(wrapper.exists("#throughputSpendElement")).toEqual(false);
|
||||
});
|
||||
|
||||
it("spendAck checkbox visible", () => {
|
||||
|
||||
@@ -8,15 +8,10 @@ import {
|
||||
checkBoxAndInputStackProps,
|
||||
getChoiceGroupStyles,
|
||||
messageBarStyles,
|
||||
getEstimatedSpendingElement,
|
||||
getEstimatedSpendElement,
|
||||
getEstimatedAutoscaleSpendElement,
|
||||
getAutoPilotV3SpendElement,
|
||||
manualToAutoscaleDisclaimerElement,
|
||||
saveThroughputWarningMessage,
|
||||
ManualEstimatedSpendingDisplayProps,
|
||||
AutoscaleEstimatedSpendingDisplayProps,
|
||||
PriceBreakdown,
|
||||
getRuPriceBreakdown,
|
||||
transparentDetailsHeaderStyle
|
||||
manualToAutoscaleDisclaimerElement
|
||||
} from "../../SettingsRenderUtils";
|
||||
import {
|
||||
Text,
|
||||
@@ -28,9 +23,7 @@ import {
|
||||
Label,
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
FontIcon,
|
||||
IColumn
|
||||
MessageBarType
|
||||
} from "office-ui-fabric-react";
|
||||
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
|
||||
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
|
||||
@@ -39,7 +32,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, calculateEstimateNumber } from "../../../../../Utils/PricingUtils";
|
||||
import { usageInGB } from "../../../../../Utils/PricingUtils";
|
||||
import { Features } from "../../../../../Common/Constants";
|
||||
|
||||
export interface ThroughputInputAutoPilotV3Props {
|
||||
@@ -172,243 +165,33 @@ 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 = this.getEstimatedManualSpendElement(
|
||||
estimatedSpend = getEstimatedSpendElement(
|
||||
// if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set...
|
||||
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : this.props.throughputBaseline,
|
||||
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : offerThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
isDirty ? this.props.throughput : undefined
|
||||
multimaster
|
||||
);
|
||||
} else {
|
||||
estimatedSpend = this.getEstimatedAutoscaleSpendElement(
|
||||
this.props.maxAutoPilotThroughputBaseline,
|
||||
estimatedSpend = getEstimatedAutoscaleSpendElement(
|
||||
this.props.maxAutoPilotThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
isDirty ? this.props.maxAutoPilotThroughput : undefined
|
||||
multimaster
|
||||
);
|
||||
}
|
||||
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 <></>;
|
||||
@@ -535,12 +318,6 @@ 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"
|
||||
@@ -572,24 +349,13 @@ 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,21 +8,6 @@ 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"
|
||||
@@ -229,19 +214,6 @@ 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"
|
||||
@@ -267,142 +239,38 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
type="number"
|
||||
value="100"
|
||||
/>
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
>
|
||||
<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:
|
||||
|
||||
1
|
||||
,
|
||||
100
|
||||
RU/s,
|
||||
Estimated cost (
|
||||
USD
|
||||
):
|
||||
|
||||
<b>
|
||||
$
|
||||
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>
|
||||
0.0080
|
||||
hourly
|
||||
/
|
||||
$
|
||||
0.19
|
||||
daily
|
||||
/
|
||||
$
|
||||
5.84
|
||||
monthly
|
||||
|
||||
</b>
|
||||
(
|
||||
regions:
|
||||
|
||||
1
|
||||
,
|
||||
100
|
||||
RU/s,
|
||||
$
|
||||
0.00008
|
||||
/RU)
|
||||
</Text>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
id="spendAckCheckBox"
|
||||
@@ -420,7 +288,6 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
}
|
||||
}
|
||||
/>
|
||||
<br />
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -502,19 +369,6 @@ 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"
|
||||
@@ -540,143 +394,38 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
type="number"
|
||||
value="100"
|
||||
/>
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
>
|
||||
<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:
|
||||
|
||||
1
|
||||
,
|
||||
100
|
||||
RU/s,
|
||||
Estimated cost (
|
||||
USD
|
||||
):
|
||||
|
||||
<b>
|
||||
$
|
||||
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 />
|
||||
0.0080
|
||||
hourly
|
||||
/
|
||||
$
|
||||
0.19
|
||||
daily
|
||||
/
|
||||
$
|
||||
5.84
|
||||
monthly
|
||||
|
||||
</b>
|
||||
(
|
||||
regions:
|
||||
|
||||
1
|
||||
,
|
||||
100
|
||||
RU/s,
|
||||
$
|
||||
0.00008
|
||||
/RU)
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
@@ -60,100 +60,66 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
</StyledLinkBase>
|
||||
.
|
||||
</Text>
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
>
|
||||
<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:
|
||||
|
||||
2
|
||||
,
|
||||
1000
|
||||
RU/s,
|
||||
Estimated cost (
|
||||
RMB
|
||||
):
|
||||
|
||||
<b>
|
||||
¥
|
||||
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>
|
||||
1.02
|
||||
hourly
|
||||
/
|
||||
¥
|
||||
24.48
|
||||
daily
|
||||
/
|
||||
¥
|
||||
744.60
|
||||
monthly
|
||||
|
||||
</b>
|
||||
(
|
||||
regions:
|
||||
|
||||
2
|
||||
,
|
||||
1000
|
||||
RU/s,
|
||||
¥
|
||||
0.00051
|
||||
/RU)
|
||||
</Text>
|
||||
<Text
|
||||
id="autoscaleSpendElement"
|
||||
>
|
||||
Estimated monthly cost (
|
||||
RMB
|
||||
) is
|
||||
|
||||
<b>
|
||||
¥
|
||||
111.69
|
||||
-
|
||||
¥
|
||||
1116.90
|
||||
|
||||
</b>
|
||||
(
|
||||
regions:
|
||||
|
||||
2
|
||||
,
|
||||
100
|
||||
-
|
||||
1000
|
||||
RU/s,
|
||||
¥
|
||||
0.000765
|
||||
/RU)
|
||||
</Text>
|
||||
<Text
|
||||
id="manualToAutoscaleDisclaimerElement"
|
||||
styles={
|
||||
|
||||
@@ -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/dataAccess/createDocument";
|
||||
import { createDocument } from "../../Common/DocumentClientUtilityBase";
|
||||
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,15 +95,12 @@ export class ContainerSampleGenerator {
|
||||
.reduce((previous, current) => previous.then(current), Promise.resolve());
|
||||
} else {
|
||||
// For SQL all queries are executed at the same time
|
||||
await Promise.all(
|
||||
this.sampleDataFile.data.map(async doc => {
|
||||
try {
|
||||
await createDocument(collection, doc);
|
||||
} catch (error) {
|
||||
NotificationConsoleUtils.logConsoleError(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
this.sampleDataFile.data.map(doc => {
|
||||
const subPromise = createDocument(collection, doc);
|
||||
subPromise.catch(reason => NotificationConsoleUtils.logConsoleError(reason));
|
||||
promises.push(subPromise);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,6 @@ export default class Explorer {
|
||||
public isAccountReady: ko.Observable<boolean>;
|
||||
public canSaveQueries: ko.Computed<boolean>;
|
||||
public features: ko.Observable<any>;
|
||||
public serverId: ko.Observable<string>;
|
||||
public isTryCosmosDBSubscription: ko.Observable<boolean>;
|
||||
public queriesClient: QueriesClient;
|
||||
public tableDataClient: TableDataClient;
|
||||
@@ -365,7 +364,6 @@ export default class Explorer {
|
||||
this.memoryUsageInfo = ko.observable<DataModels.MemoryUsageInfo>();
|
||||
|
||||
this.features = ko.observable();
|
||||
this.serverId = ko.observable<string>();
|
||||
this.queriesClient = new QueriesClient(this);
|
||||
this.isTryCosmosDBSubscription = ko.observable<boolean>(false);
|
||||
|
||||
@@ -1854,7 +1852,6 @@ export default class Explorer {
|
||||
this.collectionCreationDefaults = inputs.defaultCollectionThroughput;
|
||||
}
|
||||
this.features(inputs.features);
|
||||
this.serverId(inputs.serverId);
|
||||
this.databaseAccount(databaseAccount);
|
||||
this.subscriptionType(inputs.subscriptionType);
|
||||
this.hasWriteAccess(inputs.hasWriteAccess);
|
||||
@@ -1866,7 +1863,8 @@ export default class Explorer {
|
||||
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: inputs.extensionEndpoint || "",
|
||||
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT)
|
||||
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
||||
serverId: inputs.serverId
|
||||
});
|
||||
|
||||
updateUserContext({
|
||||
@@ -1953,9 +1951,9 @@ export default class Explorer {
|
||||
|
||||
public isRunningOnNationalCloud(): boolean {
|
||||
return (
|
||||
this.serverId() === Constants.ServerIds.blackforest ||
|
||||
this.serverId() === Constants.ServerIds.fairfax ||
|
||||
this.serverId() === Constants.ServerIds.mooncake
|
||||
userContext === Constants.ServerIds.blackforest ||
|
||||
userContext === Constants.ServerIds.fairfax ||
|
||||
userContext === Constants.ServerIds.mooncake
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
jest.mock("../../../Common/dataAccess/queryDocuments");
|
||||
jest.mock("../../../Common/dataAccess/queryDocumentsPage");
|
||||
jest.mock("../../../Common/DocumentClientUtilityBase");
|
||||
import React from "react";
|
||||
import * as sinon from "sinon";
|
||||
import { mount, ReactWrapper } from "enzyme";
|
||||
@@ -13,8 +12,7 @@ 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 } from "../../../Common/dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||
import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
|
||||
|
||||
describe("Check whether query result is vertex array", () => {
|
||||
it("should reject null as vertex array", () => {
|
||||
@@ -301,12 +299,12 @@ describe("GraphExplorer", () => {
|
||||
ignoreD3Update: boolean
|
||||
): GraphExplorer => {
|
||||
(queryDocuments as jest.Mock).mockImplementation((container: any, query: string, options: any) => {
|
||||
return {
|
||||
return Q.resolve({
|
||||
_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,10 +28,8 @@ 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 } from "../../../Common/dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||
import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
|
||||
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
|
||||
export interface GraphAccessor {
|
||||
applyFilter: () => void;
|
||||
@@ -727,32 +725,26 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
/**
|
||||
* Execute DocDB query and get all results
|
||||
*/
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -872,7 +864,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
/**
|
||||
* User executes query
|
||||
*/
|
||||
public async submitQuery(query: string): Promise<void> {
|
||||
public submitQuery(query: string): void {
|
||||
// Clear any progress indicator
|
||||
this.executeCounter = 0;
|
||||
this.setState({
|
||||
@@ -890,22 +882,24 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
// Remember query
|
||||
this.pushToLatestQueryFragments(query);
|
||||
|
||||
try {
|
||||
let result: UserQueryResult;
|
||||
if (query.toLocaleLowerCase() === "g.V()".toLocaleLowerCase()) {
|
||||
result = await this.executeDocDbGVQuery();
|
||||
} else {
|
||||
result = await this.executeGremlinQuery(query);
|
||||
}
|
||||
let backendPromise;
|
||||
|
||||
this.queryTotalRequestCharge = result.requestCharge;
|
||||
} catch (error) {
|
||||
const errorMsg = `Failure in submitting query: ${query}: ${getErrorMessage(error)}`;
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
|
||||
this.setState({
|
||||
filterQueryError: errorMsg
|
||||
});
|
||||
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
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1396,7 +1390,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
/**
|
||||
* Update possible vertices to display in UI
|
||||
*/
|
||||
private updatePossibleVertices(): Promise<PossibleVertex[]> {
|
||||
private updatePossibleVertices(): Q.Promise<PossibleVertex[]> {
|
||||
const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null;
|
||||
|
||||
const q = `SELECT c.id, c["${this.props.graphConfigUiData.nodeCaptionChoice() ||
|
||||
@@ -1727,81 +1721,85 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
);
|
||||
}
|
||||
|
||||
private async executeDocDbGVQuery(): Promise<UserQueryResult> {
|
||||
private executeDocDbGVQuery(): Q.Promise<UserQueryResult> {
|
||||
let query = "select root.id from root where IS_DEFINED(root._isEdge) = false order by root._ts desc";
|
||||
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`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
private async loadMoreRootNodes(): Promise<UserQueryResult> {
|
||||
private loadMoreRootNodes(): Q.Promise<UserQueryResult> {
|
||||
if (!this.currentDocDBQueryInfo) {
|
||||
return undefined;
|
||||
return Q.resolve(null);
|
||||
}
|
||||
|
||||
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}`);
|
||||
|
||||
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;
|
||||
}
|
||||
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 }));
|
||||
}
|
||||
|
||||
private executeGremlinQuery(query: string): Q.Promise<UserQueryResult> {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "./MeControlComponent.less";
|
||||
import * as React from "react";
|
||||
import { DefaultButton, BaseButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
|
||||
import { DirectionalHint, IContextualMenuProps } from "office-ui-fabric-react/lib/ContextualMenu";
|
||||
|
||||
@@ -186,7 +186,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
return "";
|
||||
}
|
||||
|
||||
const serverId: string = this.container.serverId();
|
||||
const serverId = configContext.serverId;
|
||||
const regions =
|
||||
(account &&
|
||||
account.properties &&
|
||||
@@ -200,7 +200,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
if (!this.isSharedAutoPilotSelected()) {
|
||||
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||
offerThroughput,
|
||||
serverId,
|
||||
configContext.serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
this.isSharedAutoPilotSelected()
|
||||
@@ -240,7 +240,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
return "";
|
||||
}
|
||||
|
||||
const serverId: string = this.container.serverId();
|
||||
const serverId: string = configContext.serverId;
|
||||
const regions =
|
||||
(account &&
|
||||
account.properties &&
|
||||
@@ -482,7 +482,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
});
|
||||
|
||||
this.upsellMessage = ko.pureComputed<string>(() => {
|
||||
return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount());
|
||||
return PricingUtils.getUpsellMessage(configContext.serverId, this.isFreeTierAccount());
|
||||
});
|
||||
|
||||
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
|
||||
|
||||
@@ -122,7 +122,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
return "";
|
||||
}
|
||||
|
||||
const serverId = this.container.serverId();
|
||||
const serverId = configContext.serverId;
|
||||
const regions =
|
||||
(account &&
|
||||
account.properties &&
|
||||
@@ -220,7 +220,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
});
|
||||
|
||||
this.upsellMessage = ko.pureComputed<string>(() => {
|
||||
return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount());
|
||||
return PricingUtils.getUpsellMessage(configContext.serverId, this.isFreeTierAccount());
|
||||
});
|
||||
|
||||
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
|
||||
|
||||
@@ -127,7 +127,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
return "";
|
||||
}
|
||||
|
||||
const serverId = this.container.serverId();
|
||||
const serverId = configContext.serverId;
|
||||
const regions =
|
||||
(account &&
|
||||
account.properties &&
|
||||
@@ -172,7 +172,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
return "";
|
||||
}
|
||||
|
||||
const serverId = this.container.serverId();
|
||||
const serverId = configContext.serverId;
|
||||
const regions =
|
||||
(account &&
|
||||
account.properties &&
|
||||
|
||||
@@ -421,47 +421,53 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
||||
* Note that this also means that we can get less entities than the requested download size in a successful call.
|
||||
* See Microsoft Azure API Documentation at: https://msdn.microsoft.com/en-us/library/azure/dd135718.aspx
|
||||
*/
|
||||
private async prefetchData(
|
||||
private prefetchData(
|
||||
tableQuery: Entities.ITableQuery,
|
||||
downloadSize: number,
|
||||
currentRetry: number = 0
|
||||
): Promise<IListTableEntitiesSegmentedResult> {
|
||||
): Q.Promise<any> {
|
||||
if (!this.cache.serverCallInProgress) {
|
||||
this.cache.serverCallInProgress = true;
|
||||
this.allDownloaded = false;
|
||||
this.lastPrefetchTime = new Date().getTime();
|
||||
const time = this.lastPrefetchTime;
|
||||
var 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);
|
||||
|
||||
return {
|
||||
Results: entities,
|
||||
ContinuationToken: this._documentIterator.hasMoreResults()
|
||||
};
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
return promise
|
||||
.then((result: IListTableEntitiesSegmentedResult) => {
|
||||
if (!this._documentIterator) {
|
||||
this._documentIterator = documents.iterator;
|
||||
this._documentIterator = result.iterator;
|
||||
}
|
||||
var actualDownloadSize: number = 0;
|
||||
|
||||
@@ -472,11 +478,11 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
||||
return Q.resolve(null);
|
||||
}
|
||||
|
||||
var entities = documents.Results;
|
||||
var entities = result.Results;
|
||||
actualDownloadSize = entities.length;
|
||||
|
||||
// Queries can fetch no results and still return a continuation header. See prefetchAndRender() method.
|
||||
this.continuationToken = this.isCancelled ? null : documents.ContinuationToken;
|
||||
this.continuationToken = this.isCancelled ? null : result.ContinuationToken;
|
||||
|
||||
if (!this.continuationToken) {
|
||||
this.allDownloaded = true;
|
||||
@@ -508,22 +514,20 @@ export default class TableEntityListViewModel extends DataTableViewModel {
|
||||
// For #2.1, set prefetch exceeds maximum retry number and end prefetch.
|
||||
// For #2.2, go to next round prefetch.
|
||||
if (this.allDownloaded || nextDownloadSize === 0) {
|
||||
return documents;
|
||||
return Q.resolve(result);
|
||||
}
|
||||
|
||||
if (currentRetry >= TableEntityListViewModel._maximumNumberOfPrefetchRetries) {
|
||||
documents.ExceedMaximumRetries = true;
|
||||
return documents;
|
||||
result.ExceedMaximumRetries = true;
|
||||
return Q.resolve(result);
|
||||
}
|
||||
|
||||
return await this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
|
||||
}
|
||||
} catch (error) {
|
||||
this.cache.serverCallInProgress = false;
|
||||
throw error;
|
||||
}
|
||||
return this.prefetchData(tableQuery, nextDownloadSize, currentRetry + 1);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
this.cache.serverCallInProgress = false;
|
||||
return Q.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ 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";
|
||||
@@ -13,12 +12,9 @@ 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[];
|
||||
@@ -42,19 +38,19 @@ export abstract class TableDataClient {
|
||||
collection: ViewModels.Collection,
|
||||
originalDocument: any,
|
||||
newEntity: Entities.ITableEntity
|
||||
): Promise<Entities.ITableEntity>;
|
||||
): Q.Promise<Entities.ITableEntity>;
|
||||
|
||||
public abstract queryDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
query: string,
|
||||
shouldNotify?: boolean,
|
||||
paginationToken?: string
|
||||
): Promise<Entities.IListTableEntitiesResult>;
|
||||
): Q.Promise<Entities.IListTableEntitiesResult>;
|
||||
|
||||
public abstract deleteDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
entitiesToDelete: Entities.ITableEntity[]
|
||||
): Promise<any>;
|
||||
): Q.Promise<any>;
|
||||
}
|
||||
|
||||
export class TablesAPIDataClient extends TableDataClient {
|
||||
@@ -78,63 +74,77 @@ export class TablesAPIDataClient extends TableDataClient {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public async updateDocument(
|
||||
public updateDocument(
|
||||
collection: ViewModels.Collection,
|
||||
originalDocument: any,
|
||||
entity: Entities.ITableEntity
|
||||
): 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;
|
||||
}
|
||||
): 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;
|
||||
}
|
||||
|
||||
public async queryDocuments(
|
||||
public queryDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
query: string
|
||||
): 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);
|
||||
): Q.Promise<Entities.IListTableEntitiesResult> {
|
||||
const deferred = Q.defer<Entities.IListTableEntitiesResult>();
|
||||
|
||||
return {
|
||||
Results: entities,
|
||||
ContinuationToken: iterator.hasMoreResults(),
|
||||
iterator: iterator
|
||||
};
|
||||
} catch (error) {
|
||||
handleError(error, "TablesAPIDataClient/queryDocuments", "Query documents failed");
|
||||
throw error;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
public async deleteDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
entitiesToDelete: Entities.ITableEntity[]
|
||||
): Promise<any> {
|
||||
const documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments(
|
||||
public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise<any> {
|
||||
let documentsToDelete: any[] = TableEntityProcessor.convertEntitiesToDocuments(
|
||||
<Entities.ITableEntityForTablesAPI[]>entitiesToDelete,
|
||||
collection
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
documentsToDelete?.map(async document => {
|
||||
let promiseArray: Q.Promise<any>[] = [];
|
||||
documentsToDelete &&
|
||||
documentsToDelete.forEach(document => {
|
||||
document.id = ko.observable<string>(document.id);
|
||||
await deleteDocument(collection, document);
|
||||
})
|
||||
);
|
||||
let promise: Q.Promise<any> = deleteDocument(collection, document);
|
||||
promiseArray.push(promise);
|
||||
});
|
||||
return Q.all(promiseArray);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +180,10 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
(data: any) => {
|
||||
entity[TableConstants.EntityKeyNames.RowKey] = entity[this.getCassandraPartitionKeyProperty(collection)];
|
||||
entity[TableConstants.EntityKeyNames.RowKey]._ = entity[TableConstants.EntityKeyNames.RowKey]._.toString();
|
||||
NotificationConsoleUtils.logConsoleInfo(`Successfully added new row to table ${collection.id()}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully added new row to table ${collection.id()}`
|
||||
);
|
||||
deferred.resolve(entity);
|
||||
},
|
||||
error => {
|
||||
@@ -184,149 +197,181 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public async updateDocument(
|
||||
public updateDocument(
|
||||
collection: ViewModels.Collection,
|
||||
originalDocument: any,
|
||||
newEntity: Entities.ITableEntity
|
||||
): 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;
|
||||
): 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]._},`;
|
||||
}
|
||||
isChange = true;
|
||||
}
|
||||
|
||||
if (isPropertyUpdated) {
|
||||
updateQuery = updateQuery.slice(0, updateQuery.length - 1);
|
||||
updateQuery += whereSegment;
|
||||
await this.queryDocuments(collection, updateQuery);
|
||||
}
|
||||
|
||||
let deleteQuery = `DELETE `;
|
||||
let isPropertyDeleted = false;
|
||||
for (let property in originalDocument) {
|
||||
if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) {
|
||||
deleteQuery += ` ${property},`;
|
||||
isPropertyDeleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPropertyDeleted) {
|
||||
deleteQuery = deleteQuery.slice(0, deleteQuery.length - 1);
|
||||
deleteQuery += ` FROM ${collection.databaseId}.${collection.id()}${whereSegment}`;
|
||||
await this.queryDocuments(collection, deleteQuery);
|
||||
}
|
||||
|
||||
newEntity[TableConstants.EntityKeyNames.RowKey] = originalDocument[TableConstants.EntityKeyNames.RowKey];
|
||||
NotificationConsoleUtils.logConsoleInfo(`Successfully updated row ${newEntity.RowKey._}`);
|
||||
return newEntity;
|
||||
} catch (error) {
|
||||
handleError(error, "UpdateRowCassandra", "Failed to update row ${newEntity.RowKey._}");
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
query = query.slice(0, query.length - 1);
|
||||
let whereSegment = " WHERE";
|
||||
let keys: CassandraTableKey[] = collection.cassandraKeys.partitionKeys.concat(
|
||||
collection.cassandraKeys.clusteringKeys
|
||||
);
|
||||
for (let keyIndex in keys) {
|
||||
const key = keys[keyIndex].property;
|
||||
const keyType = keys[keyIndex].type;
|
||||
if (this.isStringType(keyType)) {
|
||||
whereSegment = `${whereSegment} ${key} = '${newEntity[key]._}' AND`;
|
||||
} else {
|
||||
whereSegment = `${whereSegment} ${key} = ${newEntity[key]._} AND`;
|
||||
}
|
||||
}
|
||||
whereSegment = whereSegment.slice(0, whereSegment.length - 4);
|
||||
query = query + whereSegment;
|
||||
if (isChange) {
|
||||
promiseArray.push(this.queryDocuments(collection, query));
|
||||
}
|
||||
query = `DELETE `;
|
||||
for (let property in originalDocument) {
|
||||
if (property !== TableConstants.EntityKeyNames.RowKey && !newEntity[property] && !!originalDocument[property]) {
|
||||
query = `${query} ${property},`;
|
||||
}
|
||||
}
|
||||
if (query.length > 7) {
|
||||
query = query.slice(0, query.length - 1);
|
||||
query = `${query} FROM ${collection.databaseId}.${collection.id()}${whereSegment}`;
|
||||
promiseArray.push(this.queryDocuments(collection, query));
|
||||
}
|
||||
Q.all(promiseArray)
|
||||
.then(
|
||||
(data: any) => {
|
||||
newEntity[TableConstants.EntityKeyNames.RowKey] = originalDocument[TableConstants.EntityKeyNames.RowKey];
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully updated row ${newEntity.RowKey._}`
|
||||
);
|
||||
deferred.resolve(newEntity);
|
||||
},
|
||||
error => {
|
||||
handleError(error, "UpdateRowCassandra", `Failed to update row ${newEntity.RowKey._}`);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public async queryDocuments(
|
||||
public queryDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
query: string,
|
||||
shouldNotify?: boolean,
|
||||
paginationToken?: string
|
||||
): 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 data: any = await $.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
accountName:
|
||||
collection && collection.container.databaseAccount && collection.container.databaseAccount().name,
|
||||
cassandraEndpoint: this.trimCassandraEndpoint(
|
||||
collection.container.databaseAccount().properties.cassandraEndpoint
|
||||
),
|
||||
resourceId: collection.container.databaseAccount().id,
|
||||
keyspaceId: collection.databaseId,
|
||||
tableId: collection.id(),
|
||||
query,
|
||||
paginationToken
|
||||
},
|
||||
beforeSend: this.setAuthorizationHeader,
|
||||
error: this.handleAjaxError,
|
||||
cache: false
|
||||
});
|
||||
shouldNotify &&
|
||||
NotificationConsoleUtils.logConsoleInfo(
|
||||
`Successfully fetched ${data.result.length} rows for table ${collection.id()}`
|
||||
);
|
||||
return {
|
||||
Results: data.result,
|
||||
ContinuationToken: data.paginationToken
|
||||
};
|
||||
} catch (error) {
|
||||
shouldNotify &&
|
||||
handleError(error, "QueryDocumentsCassandra", `Failed to query rows for table ${collection.id()}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage?.();
|
||||
): 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);
|
||||
}
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
public async deleteDocuments(
|
||||
collection: ViewModels.Collection,
|
||||
entitiesToDelete: Entities.ITableEntity[]
|
||||
): Promise<any> {
|
||||
public deleteDocuments(collection: ViewModels.Collection, entitiesToDelete: Entities.ITableEntity[]): Q.Promise<any> {
|
||||
const query = `DELETE FROM ${collection.databaseId}.${collection.id()} WHERE `;
|
||||
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();
|
||||
}
|
||||
})
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
public createKeyspace(
|
||||
|
||||
@@ -16,16 +16,18 @@ 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, Resource, ConflictDefinition, FeedOptions } from "@azure/cosmos";
|
||||
import { QueryIterator, ItemDefinition, Resource, ConflictDefinition } 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>;
|
||||
@@ -223,15 +225,25 @@ export default class ConflictsTab extends TabsBase {
|
||||
});
|
||||
}
|
||||
|
||||
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 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 onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
@@ -253,9 +265,9 @@ export default class ConflictsTab extends TabsBase {
|
||||
return Q();
|
||||
}
|
||||
|
||||
public onAcceptChangesClick = async (): Promise<void> => {
|
||||
public onAcceptChangesClick = (): Q.Promise<any> => {
|
||||
if (this.isEditorDirty() && !this._isIgnoreDirtyEditor()) {
|
||||
return;
|
||||
return Q();
|
||||
}
|
||||
|
||||
this.isExecutionError(false);
|
||||
@@ -273,79 +285,81 @@ export default class ConflictsTab extends TabsBase {
|
||||
conflictResourceId: selectedConflict.resourceId
|
||||
});
|
||||
|
||||
try {
|
||||
if (selectedConflict.operationType === Constants.ConflictOperationType.Replace) {
|
||||
const documentContent = JSON.parse(this.selectedConflictContent());
|
||||
let operationPromise: Q.Promise<any> = Q();
|
||||
if (selectedConflict.operationType === Constants.ConflictOperationType.Replace) {
|
||||
const documentContent = JSON.parse(this.selectedConflictContent());
|
||||
|
||||
await updateDocument(
|
||||
this.collection,
|
||||
selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]),
|
||||
documentContent
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedConflict.operationType === Constants.ConflictOperationType.Create) {
|
||||
const documentContent = JSON.parse(this.selectedConflictContent());
|
||||
|
||||
await createDocument(this.collection, documentContent);
|
||||
}
|
||||
|
||||
if (
|
||||
selectedConflict.operationType === Constants.ConflictOperationType.Delete &&
|
||||
!!this.selectedConflictContent()
|
||||
) {
|
||||
const documentContent = JSON.parse(this.selectedConflictContent());
|
||||
|
||||
await deleteDocument(
|
||||
this.collection,
|
||||
selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty])
|
||||
);
|
||||
}
|
||||
|
||||
await deleteConflict(this.collection, selectedConflict);
|
||||
this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid);
|
||||
this.selectedConflictContent("");
|
||||
this.selectedConflictCurrent("");
|
||||
this.selectedConflictId(null);
|
||||
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.ResolveConflict,
|
||||
{
|
||||
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
|
||||
defaultExperience: this.collection && this.collection.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
conflictResourceType: selectedConflict.resourceType,
|
||||
conflictOperationType: selectedConflict.operationType,
|
||||
conflictResourceId: selectedConflict.resourceId
|
||||
},
|
||||
startKey
|
||||
operationPromise = updateDocument(
|
||||
this.collection,
|
||||
selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty]),
|
||||
documentContent
|
||||
);
|
||||
} catch (error) {
|
||||
this.isExecutionError(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
window.alert(errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.ResolveConflict,
|
||||
{
|
||||
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
|
||||
defaultExperience: this.collection && this.collection.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
conflictResourceType: selectedConflict.resourceType,
|
||||
conflictOperationType: selectedConflict.operationType,
|
||||
conflictResourceId: selectedConflict.resourceId,
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error)
|
||||
},
|
||||
startKey
|
||||
);
|
||||
} finally {
|
||||
this.isExecuting(false);
|
||||
}
|
||||
|
||||
if (selectedConflict.operationType === Constants.ConflictOperationType.Create) {
|
||||
const documentContent = JSON.parse(this.selectedConflictContent());
|
||||
|
||||
operationPromise = createDocument(this.collection, documentContent);
|
||||
}
|
||||
|
||||
if (selectedConflict.operationType === Constants.ConflictOperationType.Delete && !!this.selectedConflictContent()) {
|
||||
const documentContent = JSON.parse(this.selectedConflictContent());
|
||||
|
||||
operationPromise = deleteDocument(
|
||||
this.collection,
|
||||
selectedConflict.buildDocumentIdFromConflict(documentContent[selectedConflict.partitionKeyProperty])
|
||||
);
|
||||
}
|
||||
|
||||
return operationPromise
|
||||
.then(
|
||||
() => {
|
||||
return deleteConflict(this.collection, selectedConflict).then(() => {
|
||||
this.conflictIds.remove((conflictId: ConflictId) => conflictId.rid === selectedConflict.rid);
|
||||
this.selectedConflictContent("");
|
||||
this.selectedConflictCurrent("");
|
||||
this.selectedConflictId(null);
|
||||
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.ResolveConflict,
|
||||
{
|
||||
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
|
||||
defaultExperience: this.collection && this.collection.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
conflictResourceType: selectedConflict.resourceType,
|
||||
conflictOperationType: selectedConflict.operationType,
|
||||
conflictResourceId: selectedConflict.resourceId
|
||||
},
|
||||
startKey
|
||||
);
|
||||
});
|
||||
},
|
||||
error => {
|
||||
this.isExecutionError(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
window.alert(errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.ResolveConflict,
|
||||
{
|
||||
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
|
||||
defaultExperience: this.collection && this.collection.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
conflictResourceType: selectedConflict.resourceType,
|
||||
conflictOperationType: selectedConflict.operationType,
|
||||
conflictResourceId: selectedConflict.resourceId,
|
||||
error: errorMessage,
|
||||
errorStack: getErrorStack(error)
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
)
|
||||
.finally(() => this.isExecuting(false));
|
||||
};
|
||||
|
||||
public onDeleteClick = async (): Promise<void> => {
|
||||
public onDeleteClick = (): Q.Promise<any> => {
|
||||
this.isExecutionError(false);
|
||||
this.isExecuting(true);
|
||||
|
||||
@@ -361,48 +375,50 @@ export default class ConflictsTab extends TabsBase {
|
||||
conflictResourceId: selectedConflict.resourceId
|
||||
});
|
||||
|
||||
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
|
||||
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
|
||||
);
|
||||
},
|
||||
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);
|
||||
}
|
||||
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> => {
|
||||
@@ -429,47 +445,60 @@ export default class ConflictsTab extends TabsBase {
|
||||
return Q();
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
super.onTabClick();
|
||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts);
|
||||
public onTabClick(): Q.Promise<any> {
|
||||
return super.onTabClick().then(() => {
|
||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Conflicts);
|
||||
});
|
||||
}
|
||||
|
||||
public async onActivate(): Promise<void> {
|
||||
super.onActivate();
|
||||
|
||||
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 onActivate(): Q.Promise<any> {
|
||||
return super.onActivate().then(() => {
|
||||
if (this._documentsIterator) {
|
||||
return Q.resolve(this._documentsIterator);
|
||||
}
|
||||
}
|
||||
|
||||
return this.createIterator().then(
|
||||
(iterator: QueryIterator<ItemDefinition & Resource>) => {
|
||||
this._documentsIterator = iterator;
|
||||
return this.loadNextPage();
|
||||
},
|
||||
error => {
|
||||
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseAccountName: this.collection.container.databaseAccount().name,
|
||||
databaseName: this.collection.databaseId,
|
||||
collectionName: this.collection.id(),
|
||||
defaultExperience: this.collection.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error)
|
||||
},
|
||||
this.onLoadStartKey
|
||||
);
|
||||
this.onLoadStartKey = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public createIterator(): QueryIterator<ConflictDefinition & Resource> {
|
||||
public onRefreshClick(): Q.Promise<any> {
|
||||
return this.refreshDocumentsGrid().then(() => {
|
||||
this.selectedConflictContent("");
|
||||
this.selectedConflictId(null);
|
||||
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
||||
});
|
||||
}
|
||||
|
||||
public createIterator(): Q.Promise<QueryIterator<ConflictDefinition & Resource>> {
|
||||
// TODO: Conflict Feed does not allow filtering atm
|
||||
const query: string = undefined;
|
||||
const options = {
|
||||
enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey()
|
||||
};
|
||||
return queryConflicts(this.collection.databaseId, this.collection.id(), query, options as FeedOptions);
|
||||
let options: any = {};
|
||||
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
|
||||
return queryConflicts(this.collection.databaseId, this.collection.id(), query, options);
|
||||
}
|
||||
|
||||
public loadNextPage(): Q.Promise<any> {
|
||||
|
||||
@@ -139,7 +139,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
return "";
|
||||
}
|
||||
|
||||
const serverId = this.container.serverId();
|
||||
const serverId = configContext.serverId;
|
||||
const regions =
|
||||
(account &&
|
||||
account.properties &&
|
||||
@@ -429,10 +429,11 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
return Q();
|
||||
};
|
||||
|
||||
public async onActivate(): Promise<void> {
|
||||
super.onActivate();
|
||||
this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
|
||||
await this.database.loadOffer();
|
||||
public onActivate(): Q.Promise<any> {
|
||||
return super.onActivate().then(async () => {
|
||||
this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettings);
|
||||
await this.database.loadOffer();
|
||||
});
|
||||
}
|
||||
|
||||
private _setBaseline() {
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
<button
|
||||
class="filterbtnstyle queryButton"
|
||||
data-bind="
|
||||
click: refreshDocumentsGrid,
|
||||
click: onApplyFilterClick,
|
||||
enable: applyFilterButton.enabled"
|
||||
aria-label="Apply filter"
|
||||
tabindex="0"
|
||||
|
||||
@@ -19,24 +19,19 @@ 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,
|
||||
Item
|
||||
} from "@azure/cosmos";
|
||||
import { extractPartitionKey, PartitionKeyDefinition, QueryIterator, ItemDefinition, Resource } 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>;
|
||||
@@ -374,22 +369,36 @@ export default class DocumentsTab extends TabsBase {
|
||||
return true;
|
||||
};
|
||||
|
||||
public async refreshDocumentsGrid(): Promise<void> {
|
||||
public onApplyFilterClick(): Q.Promise<any> {
|
||||
// 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));
|
||||
});
|
||||
}
|
||||
|
||||
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 refreshDocumentsGrid(): Q.Promise<any> {
|
||||
return this.onApplyFilterClick();
|
||||
}
|
||||
|
||||
public onRefreshButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
@@ -425,7 +434,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
return Q();
|
||||
};
|
||||
|
||||
public onSaveNewDocumentClick = (): Promise<any> => {
|
||||
public onSaveNewDocumentClick = (): Q.Promise<any> => {
|
||||
this.isExecutionError(false);
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
|
||||
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
|
||||
@@ -493,7 +502,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
return Q();
|
||||
};
|
||||
|
||||
public onSaveExisitingDocumentClick = (): Promise<any> => {
|
||||
public onSaveExisitingDocumentClick = (): Q.Promise<any> => {
|
||||
const selectedDocumentId = this.selectedDocumentId();
|
||||
const documentContent = JSON.parse(this.selectedDocumentContent());
|
||||
|
||||
@@ -562,15 +571,17 @@ export default class DocumentsTab extends TabsBase {
|
||||
return Q();
|
||||
};
|
||||
|
||||
public onDeleteExisitingDocumentClick = async (): Promise<void> => {
|
||||
public onDeleteExisitingDocumentClick = (): Q.Promise<any> => {
|
||||
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)) {
|
||||
await this._deleteDocument(selectedDocumentId);
|
||||
return this._deleteDocument(selectedDocumentId);
|
||||
}
|
||||
|
||||
return Q();
|
||||
};
|
||||
|
||||
public onValidDocumentEdit(): Q.Promise<any> {
|
||||
@@ -606,50 +617,63 @@ export default class DocumentsTab extends TabsBase {
|
||||
return Q();
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
super.onTabClick();
|
||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
|
||||
public onTabClick(): Q.Promise<any> {
|
||||
return super.onTabClick().then(() => {
|
||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
|
||||
});
|
||||
}
|
||||
|
||||
public async onActivate(): Promise<void> {
|
||||
super.onActivate();
|
||||
|
||||
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 onActivate(): Q.Promise<any> {
|
||||
return super.onActivate().then(() => {
|
||||
if (this._documentsIterator) {
|
||||
return Q.resolve(this._documentsIterator);
|
||||
}
|
||||
}
|
||||
|
||||
return this.createIterator().then(
|
||||
(iterator: QueryIterator<ItemDefinition & Resource>) => {
|
||||
this._documentsIterator = iterator;
|
||||
return this.loadNextPage();
|
||||
},
|
||||
error => {
|
||||
if (this.onLoadStartKey != null && this.onLoadStartKey != undefined) {
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseAccountName: this.collection.container.databaseAccount().name,
|
||||
databaseName: this.collection.databaseId,
|
||||
collectionName: this.collection.id(),
|
||||
defaultExperience: this.collection.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error)
|
||||
},
|
||||
this.onLoadStartKey
|
||||
);
|
||||
this.onLoadStartKey = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public onRefreshClick(): Q.Promise<any> {
|
||||
return this.refreshDocumentsGrid().then(() => {
|
||||
this.selectedDocumentContent("");
|
||||
this.selectedDocumentId(null);
|
||||
this.editorState(ViewModels.DocumentExplorerState.noDocumentSelected);
|
||||
});
|
||||
}
|
||||
private _isIgnoreDirtyEditor = (): boolean => {
|
||||
var msg: string = "Changes will be lost. Do you want to continue?";
|
||||
return window.confirm(msg);
|
||||
};
|
||||
|
||||
protected __deleteDocument(documentId: DocumentId): Promise<void> {
|
||||
protected __deleteDocument(documentId: DocumentId): Q.Promise<any> {
|
||||
return deleteDocument(this.collection, documentId);
|
||||
}
|
||||
|
||||
private _deleteDocument(selectedDocumentId: DocumentId): Promise<void> {
|
||||
private _deleteDocument(selectedDocumentId: DocumentId): Q.Promise<any> {
|
||||
this.isExecutionError(false);
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, {
|
||||
databaseAccountName: this.collection && this.collection.container.databaseAccount().name,
|
||||
@@ -660,7 +684,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);
|
||||
@@ -696,7 +720,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
.finally(() => this.isExecuting(false));
|
||||
}
|
||||
|
||||
public createIterator(): QueryIterator<ItemDefinition & Resource> {
|
||||
public createIterator(): Q.Promise<QueryIterator<ItemDefinition & Resource>> {
|
||||
let filters = this.lastFilterContents();
|
||||
const filter: string = this.filterContent().trim();
|
||||
const query: string = this.buildQuery(filter);
|
||||
@@ -710,10 +734,11 @@ export default class DocumentsTab extends TabsBase {
|
||||
return queryDocuments(this.collection.databaseId, this.collection.id(), query, options);
|
||||
}
|
||||
|
||||
public async selectDocument(documentId: DocumentId): Promise<void> {
|
||||
public selectDocument(documentId: DocumentId): Q.Promise<any> {
|
||||
this.selectedDocumentId(documentId);
|
||||
const content = await readDocument(this.collection, documentId);
|
||||
this.initDocumentEditor(documentId, content);
|
||||
return readDocument(this.collection, documentId).then((content: any) => {
|
||||
this.initDocumentEditor(documentId, content);
|
||||
});
|
||||
}
|
||||
|
||||
public loadNextPage(): Q.Promise<any> {
|
||||
|
||||
@@ -114,9 +114,10 @@ export default class GraphTab extends TabsBase {
|
||||
: `${account.name}.graphs.azure.com:443/`;
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
super.onTabClick();
|
||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Graph);
|
||||
public onTabClick(): Q.Promise<any> {
|
||||
return super.onTabClick().then(() => {
|
||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Graph);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -289,7 +289,7 @@
|
||||
<button
|
||||
class="filterbtnstyle queryButton"
|
||||
data-bind="
|
||||
click: refreshDocumentsGrid,
|
||||
click: onApplyFilterClick,
|
||||
enable: applyFilterButton.enabled"
|
||||
>
|
||||
Apply Filter
|
||||
|
||||
@@ -44,7 +44,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
super.buildCommandBarOptions();
|
||||
}
|
||||
|
||||
public onSaveNewDocumentClick = (): Promise<any> => {
|
||||
public onSaveNewDocumentClick = (): Q.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");
|
||||
throw new Error("Document without shard key");
|
||||
return Q.reject("Document without shard key");
|
||||
}
|
||||
|
||||
this.isExecutionError(false);
|
||||
this.isExecuting(true);
|
||||
return createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent)
|
||||
return Q(createDocument(this.collection.databaseId, this.collection, this.partitionKeyProperty, documentContent))
|
||||
.then(
|
||||
(savedDocument: any) => {
|
||||
let partitionKeyArray = extractPartitionKey(
|
||||
@@ -136,7 +136,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
.finally(() => this.isExecuting(false));
|
||||
};
|
||||
|
||||
public onSaveExisitingDocumentClick = (): Promise<any> => {
|
||||
public onSaveExisitingDocumentClick = (): Q.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 updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent)
|
||||
return Q(updateDocument(this.collection.databaseId, this.collection, selectedDocumentId, documentContent))
|
||||
.then(
|
||||
(updatedDocument: any) => {
|
||||
let value: string = this.renderObjectForEditor(updatedDocument || {}, null, 4);
|
||||
@@ -204,10 +204,13 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
return filter || "{}";
|
||||
}
|
||||
|
||||
public async selectDocument(documentId: DocumentId): Promise<void> {
|
||||
public selectDocument(documentId: DocumentId): Q.Promise<any> {
|
||||
this.selectedDocumentId(documentId);
|
||||
const content = await readDocument(this.collection.databaseId, this.collection, documentId);
|
||||
this.initDocumentEditor(documentId, content);
|
||||
return Q(
|
||||
readDocument(this.collection.databaseId, this.collection, documentId).then((content: any) => {
|
||||
this.initDocumentEditor(documentId, content);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public loadNextPage(): Q.Promise<any> {
|
||||
@@ -327,7 +330,7 @@ export default class MongoDocumentsTab extends DocumentsTab {
|
||||
return partitionKey;
|
||||
}
|
||||
|
||||
protected __deleteDocument(documentId: DocumentId): Promise<void> {
|
||||
return deleteDocument(this.collection.databaseId, this.collection, documentId);
|
||||
protected __deleteDocument(documentId: DocumentId): Q.Promise<any> {
|
||||
return Q(deleteDocument(this.collection.databaseId, this.collection, documentId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export default class MongoShellTab extends TabsBase {
|
||||
this._runtimeEndpoint = configContext.platform === Platform.Hosted ? configContext.BACKEND_ENDPOINT : "";
|
||||
const extensionEndpoint: string = configContext.BACKEND_ENDPOINT || this._runtimeEndpoint || "";
|
||||
let baseUrl = "/content/mongoshell/dist/";
|
||||
if (this._container.serverId() === "localhost") {
|
||||
if (configContext.serverId === "localhost") {
|
||||
baseUrl = "/content/mongoshell/";
|
||||
}
|
||||
|
||||
@@ -53,9 +53,10 @@ export default class MongoShellTab extends TabsBase {
|
||||
// }
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
super.onTabClick();
|
||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
|
||||
public onTabClick(): Q.Promise<any> {
|
||||
return super.onTabClick().then(() => {
|
||||
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() && queryResults().length > 0 && isResultToggled() && allResultsMetadata().length > 0 && !error()"
|
||||
data-bind="visible: queryResults().length > 0 && isResultToggled() && allResultsMetadata().length > 0 && !error()"
|
||||
>
|
||||
</json-editor>
|
||||
<div
|
||||
|
||||
@@ -15,10 +15,9 @@ 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,
|
||||
@@ -164,19 +163,20 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
this._buildCommandBarOptions();
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
super.onTabClick();
|
||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query);
|
||||
public onTabClick(): Q.Promise<any> {
|
||||
return super.onTabClick().then(() => {
|
||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query);
|
||||
});
|
||||
}
|
||||
|
||||
public onExecuteQueryClick = async (): Promise<void> => {
|
||||
public onExecuteQueryClick = (): Q.Promise<any> => {
|
||||
const sqlStatement: string = this.selectedContent() || this.sqlQueryEditorContent();
|
||||
this.sqlStatementToExecute(sqlStatement);
|
||||
this.allResultsMetadata([]);
|
||||
this.queryResults("");
|
||||
this._iterator = undefined;
|
||||
this._iterator = null;
|
||||
|
||||
await this._executeQueryDocumentsPage(0);
|
||||
return 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 async onFetchNextPageClick(): Promise<void> {
|
||||
public onFetchNextPageClick(): Q.Promise<any> {
|
||||
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;
|
||||
|
||||
await this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1);
|
||||
return this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1);
|
||||
}
|
||||
|
||||
public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => {
|
||||
@@ -265,18 +265,19 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
return true;
|
||||
};
|
||||
|
||||
private async _executeQueryDocumentsPage(firstItemIndex: number): Promise<any> {
|
||||
private _executeQueryDocumentsPage(firstItemIndex: number): Q.Promise<any> {
|
||||
this.error("");
|
||||
this.roundTrips(undefined);
|
||||
if (this._iterator === undefined) {
|
||||
this._initIterator();
|
||||
if (this._iterator == null) {
|
||||
const queryIteratorPromise = this._initIterator();
|
||||
return queryIteratorPromise.finally(() => this._queryDocumentsPage(firstItemIndex));
|
||||
}
|
||||
|
||||
await this._queryDocumentsPage(firstItemIndex);
|
||||
return this._queryDocumentsPage(firstItemIndex);
|
||||
}
|
||||
|
||||
// TODO: Position and enable spinner when request is in progress
|
||||
private async _queryDocumentsPage(firstItemIndex: number): Promise<void> {
|
||||
private _queryDocumentsPage(firstItemIndex: number): Q.Promise<any> {
|
||||
this.isExecutionError(false);
|
||||
this._resetAggregateQueryMetrics();
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.ExecuteQuery, {
|
||||
@@ -288,90 +289,90 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
let options: any = {};
|
||||
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
|
||||
|
||||
const queryDocuments = async (firstItemIndex: number) =>
|
||||
await queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex);
|
||||
const queryDocuments = (firstItemIndex: number) =>
|
||||
queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex, options);
|
||||
this.isExecuting(true);
|
||||
return QueryUtils.queryPagesUntilContentPresent(firstItemIndex, queryDocuments)
|
||||
.then(
|
||||
(queryResults: ViewModels.QueryResults) => {
|
||||
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
|
||||
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
|
||||
const resultsMetadata: ViewModels.QueryResultsMetadata = {
|
||||
hasMoreResults: queryResults.hasMoreResults,
|
||||
itemCount: queryResults.itemCount,
|
||||
firstItemIndex: queryResults.firstItemIndex,
|
||||
lastItemIndex: queryResults.lastItemIndex
|
||||
};
|
||||
this.allResultsMetadata.push(resultsMetadata);
|
||||
this.activityId(queryResults.activityId);
|
||||
this.roundTrips(queryResults.roundTrips);
|
||||
|
||||
try {
|
||||
const queryResults: ViewModels.QueryResults = await QueryUtils.queryPagesUntilContentPresent(
|
||||
firstItemIndex,
|
||||
queryDocuments
|
||||
);
|
||||
const allResultsMetadata = (this.allResultsMetadata && this.allResultsMetadata()) || [];
|
||||
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
|
||||
const resultsMetadata: ViewModels.QueryResultsMetadata = {
|
||||
hasMoreResults: queryResults.hasMoreResults,
|
||||
itemCount: queryResults.itemCount,
|
||||
firstItemIndex: queryResults.firstItemIndex,
|
||||
lastItemIndex: queryResults.lastItemIndex
|
||||
};
|
||||
this.allResultsMetadata.push(resultsMetadata);
|
||||
this.activityId(queryResults.activityId);
|
||||
this.roundTrips(queryResults.roundTrips);
|
||||
this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]);
|
||||
|
||||
this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]);
|
||||
if (queryResults.itemCount == 0 && metadata != null && metadata.itemCount >= 0) {
|
||||
// we let users query for the next page because the SDK sometimes specifies there are more elements
|
||||
// even though there aren't any so we should not update the prior query results.
|
||||
return;
|
||||
}
|
||||
|
||||
if (queryResults.itemCount == 0 && metadata != null && metadata.itemCount >= 0) {
|
||||
// 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 documents: any[] = queryResults.documents;
|
||||
const results = this.renderObjectForEditor(documents, null, 4);
|
||||
|
||||
const documents: any[] = queryResults.documents;
|
||||
const results = this.renderObjectForEditor(documents, null, 4);
|
||||
const resultsDisplay: string =
|
||||
queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`;
|
||||
this.showingDocumentsDisplayText(resultsDisplay);
|
||||
this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`);
|
||||
|
||||
const resultsDisplay: string =
|
||||
queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`;
|
||||
this.showingDocumentsDisplayText(resultsDisplay);
|
||||
this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`);
|
||||
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");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
this.queryResults(results);
|
||||
|
||||
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()
|
||||
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
|
||||
);
|
||||
},
|
||||
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();
|
||||
}
|
||||
(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();
|
||||
});
|
||||
}
|
||||
|
||||
private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void {
|
||||
@@ -476,17 +477,16 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
}
|
||||
}
|
||||
|
||||
protected _initIterator(): void {
|
||||
protected _initIterator(): Q.Promise<MinimalQueryIterator> {
|
||||
const options: any = QueryTab.getIteratorOptions(this.collection);
|
||||
if (this._resourceTokenPartitionKey) {
|
||||
options.partitionKey = this._resourceTokenPartitionKey;
|
||||
}
|
||||
|
||||
this._iterator = queryDocuments(
|
||||
this.collection.databaseId,
|
||||
this.collection.id(),
|
||||
this.sqlStatementToExecute(),
|
||||
options
|
||||
return Q(
|
||||
queryDocuments(this.collection.databaseId, this.collection.id(), this.sqlStatementToExecute(), options).then(
|
||||
iterator => (this._iterator = iterator)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -161,16 +161,17 @@ export default class QueryTablesTab extends TabsBase {
|
||||
return null;
|
||||
};
|
||||
|
||||
public onActivate(): void {
|
||||
super.onActivate();
|
||||
const columns =
|
||||
!!this.tableEntityListViewModel() &&
|
||||
!!this.tableEntityListViewModel().table &&
|
||||
this.tableEntityListViewModel().table.columns;
|
||||
if (!!columns) {
|
||||
columns.adjust();
|
||||
$(window).resize();
|
||||
}
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
|
||||
@@ -186,11 +186,12 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
|
||||
this._setBaselines();
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
super.onTabClick();
|
||||
if (this.isNew()) {
|
||||
this.collection.selectedSubnodeKind(this.tabKind);
|
||||
}
|
||||
public onTabClick(): Q.Promise<any> {
|
||||
return super.onTabClick().then(() => {
|
||||
if (this.isNew()) {
|
||||
this.collection.selectedSubnodeKind(this.tabKind);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public abstract onSaveClick: () => Promise<any>;
|
||||
|
||||
@@ -42,49 +42,54 @@ export default class SettingsTabV2 extends TabsBase {
|
||||
});
|
||||
}
|
||||
|
||||
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");
|
||||
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);
|
||||
}
|
||||
);
|
||||
|
||||
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);
|
||||
return super.onActivate().then(() => {
|
||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.SettingsV2);
|
||||
});
|
||||
}
|
||||
|
||||
public getSettingsTabContainer(): Explorer {
|
||||
|
||||
@@ -94,8 +94,9 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
});
|
||||
}
|
||||
|
||||
public onTabClick(): void {
|
||||
public onTabClick(): Q.Promise<any> {
|
||||
this.getContainer().tabsManager.activateTab(this);
|
||||
return Q();
|
||||
}
|
||||
|
||||
protected updateSelectedNode(): void {
|
||||
@@ -127,7 +128,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
||||
return this.onSpaceOrEnterKeyPress(event, () => this.onCloseTabButtonClick());
|
||||
};
|
||||
|
||||
public onActivate(): void {
|
||||
public onActivate(): Q.Promise<any> {
|
||||
this.updateSelectedNode();
|
||||
if (!!this.collection) {
|
||||
this.collection.selectedSubnodeKind(this.tabKind);
|
||||
@@ -150,6 +151,7 @@ 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 === 20 -->
|
||||
<!-- ko if: $data.tabKind === 19 -->
|
||||
<settings-tab-v2 params="{data: $data}"></settings-tab-v2>
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ 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";
|
||||
@@ -38,7 +39,6 @@ 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.SettingsV2, tab => {
|
||||
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.Settings, tab => {
|
||||
return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id();
|
||||
});
|
||||
|
||||
@@ -1091,7 +1091,8 @@ export default class Collection implements ViewModels.Collection {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> {
|
||||
private _createDocumentsFromFile(fileName: string, documentContent: string): Q.Promise<UploadDetailsRecord> {
|
||||
const deferred: Q.Deferred<UploadDetailsRecord> = Q.defer();
|
||||
const record: UploadDetailsRecord = {
|
||||
fileName: fileName,
|
||||
numSucceeded: 0,
|
||||
@@ -1101,25 +1102,39 @@ 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)) {
|
||||
await Promise.all(
|
||||
content.map(async documentContent => {
|
||||
await createDocument(this, documentContent);
|
||||
record.numSucceeded++;
|
||||
})
|
||||
);
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
promises.push(triggerCreateDocument(content[i]));
|
||||
}
|
||||
} else {
|
||||
await createDocument(this, documentContent);
|
||||
record.numSucceeded++;
|
||||
promises.push(triggerCreateDocument(content));
|
||||
}
|
||||
|
||||
return record;
|
||||
} catch (error) {
|
||||
Q.all(promises).then(() => {
|
||||
deferred.resolve(record);
|
||||
});
|
||||
} catch (e) {
|
||||
record.numFailed++;
|
||||
record.errors = [...record.errors, error.message];
|
||||
return record;
|
||||
record.errors = [...record.errors, e.message];
|
||||
deferred.resolve(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/dataAccess/readDocument";
|
||||
import { readDocument } from "../../Common/DocumentClientUtilityBase";
|
||||
|
||||
export default class ConflictId {
|
||||
public container: ConflictsTab;
|
||||
@@ -59,42 +59,41 @@ export default class ConflictId {
|
||||
return;
|
||||
}
|
||||
|
||||
public async loadConflict(): Promise<void> {
|
||||
public loadConflict(): Q.Promise<any> {
|
||||
const conflictsTab = this.container;
|
||||
this.container.selectedConflictId(this);
|
||||
|
||||
if (this.operationType === Constants.ConflictOperationType.Create) {
|
||||
this.container.initDocumentEditorForCreate(this, this.content);
|
||||
return;
|
||||
return Q();
|
||||
}
|
||||
|
||||
this.container.loadingConflictData(true);
|
||||
return readDocument(this.container.collection, this.buildDocumentIdFromConflict(this.partitionKeyValue)).then(
|
||||
(currentDocumentContent: any) => {
|
||||
this.container.loadingConflictData(false);
|
||||
if (this.operationType === Constants.ConflictOperationType.Replace) {
|
||||
this.container.initDocumentEditorForReplace(this, this.content, currentDocumentContent);
|
||||
} else {
|
||||
this.container.initDocumentEditorForDelete(this, currentDocumentContent);
|
||||
}
|
||||
},
|
||||
(reason: any) => {
|
||||
this.container.loadingConflictData(false);
|
||||
|
||||
try {
|
||||
const currentDocumentContent = await readDocument(
|
||||
this.container.collection,
|
||||
this.buildDocumentIdFromConflict(this.partitionKeyValue)
|
||||
);
|
||||
// Document could be deleted
|
||||
if (
|
||||
reason &&
|
||||
reason.code === Constants.HttpStatusCodes.NotFound &&
|
||||
this.operationType === Constants.ConflictOperationType.Delete
|
||||
) {
|
||||
this.container.initDocumentEditorForNoOp(this);
|
||||
return Q();
|
||||
}
|
||||
|
||||
if (this.operationType === Constants.ConflictOperationType.Replace) {
|
||||
this.container.initDocumentEditorForReplace(this, this.content, currentDocumentContent);
|
||||
} else {
|
||||
this.container.initDocumentEditorForDelete(this, currentDocumentContent);
|
||||
return Q.reject(reason);
|
||||
}
|
||||
} 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 async loadDocument(): Promise<void> {
|
||||
await this.container.selectDocument(this);
|
||||
public loadDocument(): Q.Promise<any> {
|
||||
return 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/dataAccess/executeStoredProcedure";
|
||||
import { executeStoredProcedure } from "../../Common/DocumentClientUtilityBase";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
import { Configuration, PublicClientApplication } from "@azure/msal-browser";
|
||||
import { AuthenticatedTemplate, MsalProvider, UnauthenticatedTemplate } from "@azure/msal-react";
|
||||
import { useBoolean } from "@uifabric/react-hooks";
|
||||
import {
|
||||
DefaultButton,
|
||||
DirectionalHint,
|
||||
FocusZone,
|
||||
initializeIcons,
|
||||
Panel,
|
||||
Persona,
|
||||
PersonaInitialsColor,
|
||||
PersonaSize
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { render } from "react-dom";
|
||||
import FeedbackIcon from "../images/Feedback.svg";
|
||||
import ConnectIcon from "../images/HostedConnectwhite.svg";
|
||||
import "../less/hostedexplorer.less";
|
||||
import { CommandButtonComponent } from "./Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import { DefaultDirectoryDropdownComponent } from "./Explorer/Controls/Directory/DefaultDirectoryDropdownComponent";
|
||||
import { DirectoryListComponent } from "./Explorer/Controls/Directory/DirectoryListComponent";
|
||||
import "./Explorer/Menus/NavBar/MeControlComponent.less";
|
||||
import { useGraphProfile } from "./hooks/useGraphProfile";
|
||||
import "./Shared/appInsights";
|
||||
|
||||
initializeIcons();
|
||||
|
||||
// MSAL configuration
|
||||
const configuration: Configuration = {
|
||||
auth: {
|
||||
clientId: "e8ae3d28-de2a-4dc8-8fa3-2d2998b1c38f",
|
||||
redirectUri: "https://localhost:1234/hostedExplorer.html",
|
||||
authority: "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47"
|
||||
}
|
||||
};
|
||||
|
||||
const application = new PublicClientApplication(configuration);
|
||||
|
||||
const App: React.FunctionComponent = () => {
|
||||
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
|
||||
const { graphData, photo } = useGraphProfile();
|
||||
|
||||
const menuProps = {
|
||||
className: "mecontrolContextualMenu",
|
||||
isBeakVisible: false,
|
||||
directionalHintFixed: true,
|
||||
directionalHint: DirectionalHint.bottomRightEdge,
|
||||
calloutProps: {
|
||||
minPagePadding: 0
|
||||
},
|
||||
items: [
|
||||
{
|
||||
key: "Persona",
|
||||
onRender: () => <Persona />
|
||||
},
|
||||
{
|
||||
key: "SwitchDirectory",
|
||||
onRender: () => (
|
||||
<div className="switchDirectoryLink" onClick={() => openPanel}>
|
||||
Switch Directory
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "SignOut",
|
||||
onRender: () => (
|
||||
<div
|
||||
className="signOutLink"
|
||||
onClick={() => {
|
||||
instance.logout();
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]
|
||||
};
|
||||
const personaProps = {};
|
||||
|
||||
// {
|
||||
// id: "commandbutton-settings",
|
||||
// iconSrc: SettingsIcon,
|
||||
// iconAlt: "setting button",
|
||||
// onCommandClick: () => {},
|
||||
// commandButtonLabel: undefined,
|
||||
// ariaLabel: "setting button",
|
||||
// tooltipText: "Global settings",
|
||||
// hasPopup: true,
|
||||
// disabled: false
|
||||
// },
|
||||
// {
|
||||
// id: "commandbutton-feedback",
|
||||
// iconSrc: FeedbackIcon,
|
||||
// iconAlt: "feeback button",
|
||||
// onCommandClick: () =>
|
||||
// window.open(
|
||||
// "https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback"
|
||||
// ),
|
||||
// commandButtonLabel: undefined,
|
||||
// ariaLabel: "feeback button",
|
||||
// tooltipText: "Send feedback",
|
||||
// hasPopup: true,
|
||||
// disabled: false
|
||||
// }
|
||||
|
||||
const buttonProps = {
|
||||
id: "mecontrolHeader",
|
||||
className: "mecontrolHeaderButton",
|
||||
menuProps: menuProps,
|
||||
onRenderMenuIcon: () => <span />,
|
||||
styles: {
|
||||
rootHovered: { backgroundColor: "#393939" },
|
||||
rootFocused: { backgroundColor: "#393939" },
|
||||
rootPressed: { backgroundColor: "#393939" },
|
||||
rootExpanded: { backgroundColor: "#393939" }
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<header>
|
||||
<div className="items" role="menubar">
|
||||
<div className="cosmosDBTitle">
|
||||
<span
|
||||
className="title"
|
||||
data-bind="click: openAzurePortal, event: { keypress: onOpenAzurePortalKeyPress }"
|
||||
tabIndex={0}
|
||||
title="Go to Azure Portal"
|
||||
>
|
||||
Microsoft Azure
|
||||
</span>
|
||||
<span className="accontSplitter" /> <span className="serviceTitle">Cosmos DB</span>
|
||||
<img
|
||||
className="chevronRight"
|
||||
src="/chevron-right.svg"
|
||||
alt="account separator"
|
||||
data-bind="visible: isAccountActive"
|
||||
/>
|
||||
<span
|
||||
className="accountSwitchComponentContainer"
|
||||
data-bind="react: accountSwitchComponentAdapter, visible: isAccountActive"
|
||||
/>
|
||||
</div>
|
||||
<div className="feedbackConnectSettingIcons">
|
||||
<AuthenticatedTemplate>
|
||||
<CommandButtonComponent
|
||||
id="commandbutton-connect"
|
||||
iconSrc={ConnectIcon}
|
||||
iconAlt="connect button"
|
||||
onCommandClick={() => {}}
|
||||
ariaLabel="connect button"
|
||||
tooltipText="Connect to a Cosmos DB account"
|
||||
hasPopup={true}
|
||||
disabled={false}
|
||||
/>
|
||||
</AuthenticatedTemplate>
|
||||
<UnauthenticatedTemplate>
|
||||
<CommandButtonComponent
|
||||
id="commandbutton-feedback"
|
||||
iconSrc={FeedbackIcon}
|
||||
iconAlt="feeback button"
|
||||
onCommandClick={() =>
|
||||
window.open(
|
||||
"https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20Hosted%20Data%20Explorer%20Feedback"
|
||||
)
|
||||
}
|
||||
ariaLabel="feeback button"
|
||||
tooltipText="Send feedback"
|
||||
hasPopup={true}
|
||||
disabled={false}
|
||||
/>
|
||||
</UnauthenticatedTemplate>
|
||||
</div>
|
||||
<div className="meControl">
|
||||
<AuthenticatedTemplate>
|
||||
<FocusZone>
|
||||
<DefaultButton {...buttonProps}>
|
||||
<Persona
|
||||
imageUrl={photo}
|
||||
text={graphData?.displayName}
|
||||
secondaryText={graphData?.displayName}
|
||||
showSecondaryText={true}
|
||||
showInitialsUntilImageLoads={true}
|
||||
initialsColor={PersonaInitialsColor.teal}
|
||||
size={PersonaSize.size28}
|
||||
className="mecontrolHeaderPersona"
|
||||
/>
|
||||
</DefaultButton>
|
||||
</FocusZone>
|
||||
</AuthenticatedTemplate>
|
||||
<UnauthenticatedTemplate>
|
||||
<DefaultButton
|
||||
className="mecontrolSigninButton"
|
||||
text="Sign In"
|
||||
onClick={() => {
|
||||
instance.loginPopup();
|
||||
}}
|
||||
styles={{
|
||||
rootHovered: { backgroundColor: "#393939", color: "#fff" },
|
||||
rootFocused: { backgroundColor: "#393939", color: "#fff" },
|
||||
rootPressed: { backgroundColor: "#393939", color: "#fff" }
|
||||
}}
|
||||
/>
|
||||
</UnauthenticatedTemplate>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{/* <iframe
|
||||
id="explorerMenu"
|
||||
name="explorer"
|
||||
className="iframe"
|
||||
title="explorer"
|
||||
src="explorer.html?v=1.0.1&platform=Hosted"
|
||||
data-bind="visible: navigationSelection() === 'explorer'"
|
||||
></iframe> */}
|
||||
<div data-bind="react: firewallWarningComponentAdapter" />
|
||||
<div data-bind="react: dialogComponentAdapter" />
|
||||
<Panel
|
||||
headerText="Select Directory"
|
||||
isOpen={!isOpen}
|
||||
onDismiss={dismissPanel}
|
||||
// You MUST provide this prop! Otherwise screen readers will just say "button" with no label.
|
||||
closeButtonAriaLabel="Close"
|
||||
>
|
||||
<div className="directoryDropdownContainer">
|
||||
<DefaultDirectoryDropdownComponent />
|
||||
</div>
|
||||
<div className="directoryDivider" />
|
||||
<div className="directoryListContainer">
|
||||
<DirectoryListComponent />
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render(
|
||||
<MsalProvider instance={application}>
|
||||
<App />
|
||||
</MsalProvider>,
|
||||
document.body
|
||||
);
|
||||
@@ -44,6 +44,7 @@ import "./Libs/jquery";
|
||||
import "bootstrap/dist/js/npm";
|
||||
import "../externals/jquery.typeahead.min.js";
|
||||
import "../externals/jquery-ui.min.js";
|
||||
import "../externals/adal.js";
|
||||
import "promise-polyfill/src/polyfill";
|
||||
import "abort-controller/polyfill";
|
||||
import "whatwg-fetch";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import "expose-loader?AuthenticationContext!../../../externals/adal";
|
||||
|
||||
import Q from "q";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
|
||||
@@ -17,7 +17,9 @@ 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 }) => {};
|
||||
const defaultRoutedCallback = (request: string, data: { route: any; params: string[]; isFirst: boolean }) => {
|
||||
console.log(request);
|
||||
};
|
||||
this._tabRouter.routed.add(onMatch || defaultRoutedCallback);
|
||||
hasher.initialized.add(parseHash);
|
||||
hasher.changed.add(parseHash);
|
||||
|
||||
@@ -25,151 +25,39 @@ describe("PricingUtils Tests", () => {
|
||||
|
||||
describe("computeRUUsagePriceHourly()", () => {
|
||||
it("should return 0 for NaN regions default cloud", () => {
|
||||
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
|
||||
});
|
||||
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, null, false);
|
||||
expect(value).toBe(0);
|
||||
});
|
||||
|
||||
it("should return 0 for -1 regions", () => {
|
||||
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
|
||||
});
|
||||
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, -1, false);
|
||||
expect(value).toBe(0);
|
||||
});
|
||||
|
||||
it("should return 0.00008 for default cloud, 1RU, 1 region, multimaster disabled", () => {
|
||||
const value = PricingUtils.computeRUUsagePriceHourly({
|
||||
serverId: "default",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 1,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: false
|
||||
});
|
||||
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 1, 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({
|
||||
serverId: "mooncake",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 1,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: false
|
||||
});
|
||||
const value = PricingUtils.computeRUUsagePriceHourly("mooncake", 1, 1, 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({
|
||||
serverId: "default",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 2,
|
||||
multimasterEnabled: false,
|
||||
isAutoscale: false
|
||||
});
|
||||
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 2, 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({
|
||||
serverId: "default",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 1,
|
||||
multimasterEnabled: true,
|
||||
isAutoscale: false
|
||||
});
|
||||
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 1, true);
|
||||
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({
|
||||
serverId: "default",
|
||||
requestUnits: 1,
|
||||
numberOfRegions: 2,
|
||||
multimasterEnabled: true,
|
||||
isAutoscale: false
|
||||
});
|
||||
const value = PricingUtils.computeRUUsagePriceHourly("default", 1, 2, true);
|
||||
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()", () => {
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
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
|
||||
@@ -58,16 +47,15 @@ export function getMultimasterMultiplier(numberOfRegions: number, multimasterEna
|
||||
return multimasterMultiplier;
|
||||
}
|
||||
|
||||
export function computeRUUsagePriceHourly({
|
||||
serverId,
|
||||
requestUnits,
|
||||
numberOfRegions,
|
||||
multimasterEnabled,
|
||||
isAutoscale
|
||||
}: ComputeRUUsagePriceHourlyArgs): number {
|
||||
export function computeRUUsagePriceHourly(
|
||||
serverId: string,
|
||||
requestUnits: number,
|
||||
numberOfRegions: number,
|
||||
multimasterEnabled: boolean
|
||||
): number {
|
||||
const regionMultiplier: number = getRegionMultiplier(numberOfRegions, multimasterEnabled);
|
||||
const multimasterMultiplier: number = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
|
||||
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multimasterMultiplier) : getPricePerRu(serverId);
|
||||
const pricePerRu = getPricePerRu(serverId);
|
||||
const ruCharge = requestUnits * pricePerRu * multimasterMultiplier * regionMultiplier;
|
||||
|
||||
return Number(ruCharge.toFixed(5));
|
||||
@@ -171,19 +159,28 @@ 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 = computeRUUsagePriceHourly({
|
||||
serverId: serverId,
|
||||
requestUnits: throughput,
|
||||
numberOfRegions: regions,
|
||||
multimasterEnabled: multimaster,
|
||||
isAutoscale: true
|
||||
});
|
||||
const hourlyPrice: number = computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster);
|
||||
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
|
||||
const currency: string = getPriceCurrency(serverId);
|
||||
const currencySign: string = getCurrencySign(serverId);
|
||||
@@ -206,13 +203,7 @@ export function getEstimatedSpendHtml(
|
||||
regions: number,
|
||||
multimaster: boolean
|
||||
): string {
|
||||
const hourlyPrice: number = computeRUUsagePriceHourly({
|
||||
serverId: serverId,
|
||||
requestUnits: throughput,
|
||||
numberOfRegions: regions,
|
||||
multimasterEnabled: multimaster,
|
||||
isAutoscale: false
|
||||
});
|
||||
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster);
|
||||
const dailyPrice: number = hourlyPrice * 24;
|
||||
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
|
||||
const currency: string = getPriceCurrency(serverId);
|
||||
@@ -226,7 +217,7 @@ export function getEstimatedSpendHtml(
|
||||
`${currencySign}${calculateEstimateNumber(monthlyPrice)} monthly </b> ` +
|
||||
`(${regions} ${regions === 1 ? "region" : "regions"}, ${throughput}RU/s, ${currencySign}${pricePerRu}/RU)` +
|
||||
`<p style='padding: 10px 0px 0px 0px;'>` +
|
||||
`<em>${estimatedCostDisclaimer}</em></p>`
|
||||
`<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>`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,13 +228,9 @@ export function getEstimatedSpendAcknowledgeString(
|
||||
multimaster: boolean,
|
||||
isAutoscale: boolean
|
||||
): string {
|
||||
const hourlyPrice: number = computeRUUsagePriceHourly({
|
||||
serverId: serverId,
|
||||
requestUnits: throughput,
|
||||
numberOfRegions: regions,
|
||||
multimasterEnabled: multimaster,
|
||||
isAutoscale: isAutoscale
|
||||
});
|
||||
const hourlyPrice: number = isAutoscale
|
||||
? computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster)
|
||||
: computeRUUsagePriceHourly(serverId, throughput, regions, multimaster);
|
||||
const dailyPrice: number = hourlyPrice * 24;
|
||||
const monthlyPrice: number = hourlyPrice * Constants.hoursInAMonth;
|
||||
const currencySign: string = getCurrencySign(serverId);
|
||||
|
||||
@@ -58,36 +58,41 @@ export class QueryUtils {
|
||||
return projections.join(",");
|
||||
}
|
||||
|
||||
public static async queryPagesUntilContentPresent(
|
||||
public static queryPagesUntilContentPresent(
|
||||
firstItemIndex: number,
|
||||
queryItems: (itemIndex: number) => Promise<ViewModels.QueryResults>
|
||||
): Promise<ViewModels.QueryResults> {
|
||||
queryItems: (itemIndex: number) => Q.Promise<ViewModels.QueryResults>
|
||||
): Q.Promise<ViewModels.QueryResults> {
|
||||
let roundTrips: number = 0;
|
||||
let netRequestCharge: number = 0;
|
||||
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;
|
||||
};
|
||||
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);
|
||||
}
|
||||
);
|
||||
|
||||
return await doRequest(firstItemIndex);
|
||||
return doRequest(firstItemIndex);
|
||||
}
|
||||
|
||||
public static async queryAllPages(
|
||||
queryItems: (itemIndex: number) => Promise<ViewModels.QueryResults>
|
||||
): Promise<ViewModels.QueryResults> {
|
||||
public static queryAllPages(
|
||||
queryItems: (itemIndex: number) => Q.Promise<ViewModels.QueryResults>
|
||||
): Q.Promise<ViewModels.QueryResults> {
|
||||
const queryResults: ViewModels.QueryResults = {
|
||||
documents: [],
|
||||
activityId: undefined,
|
||||
@@ -98,20 +103,25 @@ export class QueryUtils {
|
||||
requestCharge: 0,
|
||||
roundTrips: 0
|
||||
};
|
||||
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;
|
||||
};
|
||||
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);
|
||||
}
|
||||
);
|
||||
|
||||
return doRequest(0);
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { useAccount, useMsal } from "@azure/msal-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export async function fetchMe(accessToken: string): Promise<GraphMeResponse> {
|
||||
const headers = new Headers();
|
||||
const bearer = `Bearer ${accessToken}`;
|
||||
|
||||
headers.append("Authorization", bearer);
|
||||
|
||||
const options = {
|
||||
method: "GET",
|
||||
headers: headers
|
||||
};
|
||||
|
||||
console.log("EXECUTING REQUEST");
|
||||
return fetch("https://graph.microsoft.com/v1.0/me", options)
|
||||
.then(response => response.json())
|
||||
.catch(error => console.log(error));
|
||||
}
|
||||
|
||||
export async function fetchPhoto(accessToken: string): Promise<Blob | void> {
|
||||
const headers = new Headers();
|
||||
const bearer = `Bearer ${accessToken}`;
|
||||
|
||||
headers.append("Authorization", bearer);
|
||||
headers.append("Content-Type", "image/jpg");
|
||||
|
||||
const options = {
|
||||
method: "GET",
|
||||
headers: headers
|
||||
};
|
||||
|
||||
console.log("EXECUTING REQUEST");
|
||||
return fetch("https://graph.microsoft.com/v1.0/me/photo/$value", options)
|
||||
.then(response => response.blob())
|
||||
.catch(error => console.log(error));
|
||||
}
|
||||
|
||||
interface GraphMeResponse {
|
||||
businessPhones: any[];
|
||||
displayName: string;
|
||||
givenName: string;
|
||||
jobTitle: string;
|
||||
mail: string;
|
||||
mobilePhone: null;
|
||||
officeLocation: string;
|
||||
preferredLanguage: null;
|
||||
surname: string;
|
||||
userPrincipalName: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export function useGraphProfile(): { graphData: GraphMeResponse; photo: string } {
|
||||
const { instance, accounts } = useMsal();
|
||||
const account = useAccount(accounts[0] || {});
|
||||
const [graphData, setGraphData] = useState<GraphMeResponse>();
|
||||
const [photo, setPhoto] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
console.log("account", account);
|
||||
if (account) {
|
||||
instance
|
||||
.acquireTokenSilent({
|
||||
scopes: ["User.Read"],
|
||||
account
|
||||
})
|
||||
.then(response => {
|
||||
fetchMe(response.accessToken).then(response => setGraphData(response));
|
||||
fetchPhoto(response.accessToken).then(response => setPhoto(URL.createObjectURL(response)));
|
||||
});
|
||||
}
|
||||
}, [account]);
|
||||
return { graphData, photo };
|
||||
}
|
||||
@@ -7,5 +7,59 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<div class="items" role="menubar">
|
||||
<div class="cosmosDBTitle">
|
||||
<span
|
||||
class="title"
|
||||
data-bind="click: openAzurePortal, event: { keypress: onOpenAzurePortalKeyPress }"
|
||||
tabindex="0"
|
||||
title="Go to Azure Portal"
|
||||
>Microsoft Azure</span
|
||||
>
|
||||
<span class="accontSplitter"></span> <span class="serviceTitle">Cosmos DB</span>
|
||||
<img
|
||||
class="chevronRight"
|
||||
src="/chevron-right.svg"
|
||||
alt="account separator"
|
||||
data-bind="visible: isAccountActive"
|
||||
/>
|
||||
<span
|
||||
class="accountSwitchComponentContainer"
|
||||
data-bind="react: accountSwitchComponentAdapter, visible: isAccountActive"
|
||||
></span>
|
||||
</div>
|
||||
<div class="feedbackConnectSettingIcons" data-bind="react: controlBarComponentAdapter"></div>
|
||||
<div class="meControl" data-bind="react: meControlComponentAdapter"></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- TODO display after introducing multiple account access -->
|
||||
<nav class="fixedleftpane" style="display: none;">
|
||||
<div class="fixedLeftPaneIcons"><img src="/Hamburger.svg" alt="Expand" /></div>
|
||||
<div class="fixedLeftPaneIcons"><img src="/Connect.svg" alt="Connect to an account" /></div>
|
||||
<div
|
||||
class="fixedLeftPaneIcons"
|
||||
data-bind="click: explorer_click, css:{ topSelected: navigationSelection() === 'explorer' }"
|
||||
>
|
||||
<img src="/HostedExplorer.svg" alt="Open Data Explorer" />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<switch-directory-pane params="{data: switchDirectoryPane}"></switch-directory-pane>
|
||||
|
||||
<!-- TODO generate version number dynamically -->
|
||||
<iframe
|
||||
id="explorerMenu"
|
||||
name="explorer"
|
||||
class="iframe"
|
||||
title="explorer"
|
||||
src="explorer.html?v=1.0.1&platform=Hosted"
|
||||
data-bind="visible: navigationSelection() === 'explorer'"
|
||||
>
|
||||
</iframe>
|
||||
|
||||
<div data-bind="react: firewallWarningComponentAdapter"></div>
|
||||
<div data-bind="react: dialogComponentAdapter"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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(CREATE_DELAY);
|
||||
await frame.waitFor(LOADING_STATE_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 => {
|
||||
|
||||
@@ -182,7 +182,7 @@ module.exports = function(env = {}, argv = {}) {
|
||||
main: "./src/Main.tsx",
|
||||
index: "./src/Index.ts",
|
||||
quickstart: "./src/quickstart.ts",
|
||||
hostedExplorer: "./src/HostedExplorer.tsx",
|
||||
hostedExplorer: "./src/HostedExplorer.ts",
|
||||
testExplorer: "./test/notebooks/testExplorer/TestExplorer.ts",
|
||||
heatmap: "./src/Controls/Heatmap/Heatmap.ts",
|
||||
terminal: "./src/Terminal/index.ts",
|
||||
|
||||
Reference in New Issue
Block a user