mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-24 19:31:36 +00:00
Compare commits
6 Commits
e2e-test-d
...
users/lang
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
572d573fdd | ||
|
|
37c64c4a4d | ||
|
|
fc5ffeb7ca | ||
|
|
f39b6accb1 | ||
|
|
64601693b7 | ||
|
|
0c80c45e22 |
@@ -3,11 +3,7 @@ PORTAL_RUNNER_PASSWORD=
|
||||
PORTAL_RUNNER_SUBSCRIPTION=
|
||||
PORTAL_RUNNER_RESOURCE_GROUP=
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY=
|
||||
PORTAL_RUNNER_CONNECTION_STRING=
|
||||
NOTEBOOKS_TEST_RUNNER_TENANT_ID=
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_ID=
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET=
|
||||
CASSANDRA_CONNECTION_STRING=
|
||||
MONGO_CONNECTION_STRING=
|
||||
TABLES_CONNECTION_STRING=
|
||||
|
||||
@@ -14,6 +14,7 @@ src/Common/DataAccessUtilityBase.ts
|
||||
src/Common/DeleteFeedback.ts
|
||||
src/Common/DocumentClientUtilityBase.ts
|
||||
src/Common/EditableUtility.ts
|
||||
src/Common/EnvironmentUtility.ts
|
||||
src/Common/HashMap.test.ts
|
||||
src/Common/HashMap.ts
|
||||
src/Common/HeadersUtility.test.ts
|
||||
@@ -201,6 +202,8 @@ src/Explorer/Tabs/QueryTab.test.ts
|
||||
src/Explorer/Tabs/QueryTab.ts
|
||||
src/Explorer/Tabs/QueryTablesTab.ts
|
||||
src/Explorer/Tabs/ScriptTabBase.ts
|
||||
src/Explorer/Tabs/SettingsTab.test.ts
|
||||
src/Explorer/Tabs/SettingsTab.ts
|
||||
src/Explorer/Tabs/SparkMasterTab.ts
|
||||
src/Explorer/Tabs/StoredProcedureTab.ts
|
||||
src/Explorer/Tabs/TabComponents.ts
|
||||
@@ -287,6 +290,8 @@ src/Utils/DatabaseAccountUtils.ts
|
||||
src/Utils/JunoUtils.ts
|
||||
src/Utils/MessageValidation.ts
|
||||
src/Utils/NotebookConfigurationUtils.ts
|
||||
src/Utils/OfferUtils.test.ts
|
||||
src/Utils/OfferUtils.ts
|
||||
src/Utils/PricingUtils.test.ts
|
||||
src/Utils/QueryUtils.test.ts
|
||||
src/Utils/QueryUtils.ts
|
||||
|
||||
9
.github/workflows/ci.yml
vendored
9
.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-*
|
||||
@@ -147,20 +146,12 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
PORTAL_RUNNER_SUBSCRIPTION: ${{ secrets.PORTAL_RUNNER_SUBSCRIPTION }}
|
||||
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }}
|
||||
NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }}
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
|
||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
||||
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
||||
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
||||
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
|
||||
TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }}
|
||||
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html"
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: screenshots
|
||||
path: failed-*
|
||||
|
||||
25
.github/workflows/runners.yml
vendored
Normal file
25
.github/workflows/runners.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Runners
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * 1 * *"
|
||||
jobs:
|
||||
sqlcreatecollection:
|
||||
runs-on: ubuntu-latest
|
||||
name: "SQL | Create Collection"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- run: npm ci
|
||||
- run: npm run test:e2e
|
||||
env:
|
||||
PORTAL_RUNNER_APP_INSIGHTS_KEY: ${{ secrets.PORTAL_RUNNER_APP_INSIGHTS_KEY }}
|
||||
PORTAL_RUNNER_USERNAME: ${{ secrets.PORTAL_RUNNER_USERNAME }}
|
||||
PORTAL_RUNNER_PASSWORD: ${{ secrets.PORTAL_RUNNER_PASSWORD }}
|
||||
PORTAL_RUNNER_SUBSCRIPTION: 69e02f2d-f059-4409-9eac-97e8a276ae2c
|
||||
PORTAL_RUNNER_RESOURCE_GROUP: runners
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT: portal-sql-runner
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: screenshots
|
||||
path: failure.png
|
||||
37
README.md
37
README.md
@@ -13,18 +13,29 @@ UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), ht
|
||||
|
||||
### Watch mode
|
||||
|
||||
Run `npm start` to start the development server and automatically rebuild on changes
|
||||
Run `npm run watch` to start the development server and automatically rebuild on changes
|
||||
|
||||
### Hosted Development (https://cosmos.azure.com)
|
||||
### Specifying Development Platform
|
||||
|
||||
- Visit: `https://localhost:1234/hostedExplorer.html`
|
||||
- Local sign in via AAD will NOT work. Connection string only in dev mode. Use the Portal if you need AAD auth.
|
||||
- The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
|
||||
Setting the environment variable `PLATFORM` during the build process will force the explorer to load the specified platform. By default in development it will run in `Hosted` mode. Valid options:
|
||||
|
||||
- Hosted
|
||||
- Emulator
|
||||
- Portal
|
||||
|
||||
`PLATFORM=Emulator npm run watch`
|
||||
|
||||
### Hosted Development
|
||||
|
||||
The default webpack dev server configuration will proxy requests to the production portal backend: `https://main.documentdb.ext.azure.com`. This will allow you to use production connection strings on your local machine.
|
||||
|
||||
To run pure hosted mode, in `webpack.config.js` change index HtmlWebpackPlugin to use hostedExplorer.html and change entry for index to use HostedExplorer.ts.
|
||||
|
||||
### Emulator Development
|
||||
|
||||
- Start the Cosmos Emulator
|
||||
- Visit: https://localhost:1234/index.html
|
||||
In a window environment, running `npm run build` will automatically copy the built files from `/dist` over to the default emulator install paths. In a non-windows environment you can specify an alternate endpoint using `EMULATOR_ENDPOINT` and webpack dev server will proxy requests for you.
|
||||
|
||||
`PLATFORM=Emulator EMULATOR_ENDPOINT=https://my-vm.azure.com:8081 npm run watch`
|
||||
|
||||
#### Setting up a Remote Emulator
|
||||
|
||||
@@ -44,8 +55,16 @@ The Cosmos emulator currently only runs in Windows environments. You can still d
|
||||
|
||||
### Portal Development
|
||||
|
||||
- Visit: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html
|
||||
- You may have to manually visit https://localhost:1234/explorer.html first and click through any SSL certificate warnings
|
||||
The Cosmos Portal that consumes this repo is not currently open source. If you have access to this project, `npm run build` will copy the built files over to the portal where they will be loaded by the portal development environment
|
||||
|
||||
You can however load a local running instance of data explorer in the production portal.
|
||||
|
||||
1. Turn off browser SSL validation for localhost: chrome://flags/#allow-insecure-localhost OR Install valid SSL certs for localhost (on IE, follow these [instructions](https://www.technipages.com/ie-bypass-problem-with-this-websites-security-certificate) to install the localhost certificate in the right place)
|
||||
2. Allowlist `https://localhost:1234` domain for CORS in the Azure Cosmos DB portal
|
||||
3. Start the project in portal mode: `PLATFORM=Portal npm run watch`
|
||||
4. Load the portal using the following link: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Flocalhost%3A1234%2Fexplorer.html
|
||||
|
||||
Live reload will occur, but data explorer will not properly integrate again with the parent iframe. You will have to manually reload the page.
|
||||
|
||||
### Testing
|
||||
|
||||
|
||||
3656
package-lock.json
generated
3656
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,10 +4,8 @@
|
||||
"description": "Cosmos Explorer",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "3.9.0",
|
||||
"@azure/identity": "1.1.0",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/cosmos-language-service": "0.0.4",
|
||||
"@jupyterlab/services": "6.0.0-rc.2",
|
||||
"@jupyterlab/terminal": "3.0.0-rc.2",
|
||||
"@microsoft/applicationinsights-web": "2.5.9",
|
||||
@@ -68,7 +66,7 @@
|
||||
"jquery-ui-dist": "1.12.1",
|
||||
"knockout": "3.5.1",
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.18.1",
|
||||
"monaco-editor": "0.15.6",
|
||||
"object.entries": "1.1.0",
|
||||
"office-ui-fabric-react": "7.134.1",
|
||||
"p-retry": "4.2.0",
|
||||
@@ -117,7 +115,7 @@
|
||||
"@types/prop-types": "15.5.8",
|
||||
"@types/puppeteer": "3.0.1",
|
||||
"@types/q": "1.5.1",
|
||||
"@types/react": "16.9.56",
|
||||
"@types/react": "16.9.49",
|
||||
"@types/react-dom": "16.0.7",
|
||||
"@types/react-notification-system": "0.2.39",
|
||||
"@types/react-redux": "7.1.7",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"offerThroughput": 400,
|
||||
"databaseLevelThroughput": false,
|
||||
"collectionId": "Persons",
|
||||
"rupmEnabled": false,
|
||||
"partitionKey": { "kind": "Hash", "paths": ["/name"] },
|
||||
"data": [
|
||||
"g.addV('person').property(id, '1').property('name', 'Eva').property('age', 44)",
|
||||
@@ -12,4 +13,4 @@
|
||||
"g.V('1').addE('knows').to(g.V('2')).outV().addE('knows').to(g.V('3'))",
|
||||
"g.V('3').addE('knows').to(g.V('4'))"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -108,11 +108,13 @@ export class CapabilityNames {
|
||||
export class Features {
|
||||
public static readonly cosmosdb = "cosmosdb";
|
||||
public static readonly enableChangeFeedPolicy = "enablechangefeedpolicy";
|
||||
public static readonly enableRupm = "enablerupm";
|
||||
public static readonly executeSproc = "dataexplorerexecutesproc";
|
||||
public static readonly hostedDataExplorer = "hosteddataexplorerenabled";
|
||||
public static readonly enableTtl = "enablettl";
|
||||
public static readonly enableNotebooks = "enablenotebooks";
|
||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||
public static readonly enableCodeOfConduct = "enablecodeofconduct";
|
||||
public static readonly enableLinkInjection = "enablelinkinjection";
|
||||
public static readonly enableSpark = "enablespark";
|
||||
public static readonly livyEndpoint = "livyendpoint";
|
||||
@@ -179,6 +181,11 @@ export class CassandraBackend {
|
||||
public static readonly guestSchemaApi: string = "api/guest/cassandra/schema";
|
||||
}
|
||||
|
||||
export class RUPMStates {
|
||||
public static on: string = "on";
|
||||
public static off: string = "off";
|
||||
}
|
||||
|
||||
export class Queries {
|
||||
public static CustomPageOption: string = "custom";
|
||||
public static UnlimitedPageOption: string = "unlimited";
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
182
src/Common/DataAccessUtilityBase.ts
Normal file
182
src/Common/DataAccessUtilityBase.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import {
|
||||
ConflictDefinition,
|
||||
FeedOptions,
|
||||
ItemDefinition,
|
||||
OfferDefinition,
|
||||
QueryIterator,
|
||||
Resource
|
||||
} from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import Q from "q";
|
||||
import { configContext, Platform } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
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 { OfferUtils } from "../Utils/OfferUtils";
|
||||
import * as Constants from "./Constants";
|
||||
import { client } from "./CosmosClient";
|
||||
import * as HeadersUtility from "./HeadersUtility";
|
||||
import { sendCachedDataMessage } from "./MessageHandler";
|
||||
|
||||
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";
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
export function normalizeArmEndpoint(uri: string): string {
|
||||
if (uri && uri.slice(-1) !== "/") {
|
||||
return `${uri}/`;
|
||||
export default class EnvironmentUtility {
|
||||
public static normalizeArmEndpointUri(uri: string): string {
|
||||
if (uri && uri.slice(-1) !== "/") {
|
||||
return `${uri}/`;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import * as OfferUtility from "./OfferUtility";
|
||||
import { SDKOfferDefinition, Offer } from "../Contracts/DataModels";
|
||||
import { OfferResponse } from "@azure/cosmos";
|
||||
|
||||
describe("parseSDKOfferResponse", () => {
|
||||
it("manual throughput", () => {
|
||||
const mockOfferDefinition = {
|
||||
content: {
|
||||
offerThroughput: 500,
|
||||
collectionThroughputInfo: {
|
||||
minimumRUForCollection: 400,
|
||||
numPhysicalPartitions: 1
|
||||
}
|
||||
},
|
||||
id: "test"
|
||||
} as SDKOfferDefinition;
|
||||
|
||||
const mockResponse = {
|
||||
resource: mockOfferDefinition
|
||||
} as OfferResponse;
|
||||
|
||||
const expectedResult: Offer = {
|
||||
manualThroughput: 500,
|
||||
autoscaleMaxThroughput: undefined,
|
||||
minimumThroughput: 400,
|
||||
id: "test",
|
||||
offerDefinition: mockOfferDefinition,
|
||||
offerReplacePending: false
|
||||
};
|
||||
|
||||
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it("autoscale throughput", () => {
|
||||
const mockOfferDefinition = {
|
||||
content: {
|
||||
offerThroughput: 400,
|
||||
collectionThroughputInfo: {
|
||||
minimumRUForCollection: 400,
|
||||
numPhysicalPartitions: 1
|
||||
},
|
||||
offerAutopilotSettings: {
|
||||
maxThroughput: 5000
|
||||
}
|
||||
},
|
||||
id: "test"
|
||||
} as SDKOfferDefinition;
|
||||
|
||||
const mockResponse = {
|
||||
resource: mockOfferDefinition
|
||||
} as OfferResponse;
|
||||
|
||||
const expectedResult: Offer = {
|
||||
manualThroughput: undefined,
|
||||
autoscaleMaxThroughput: 5000,
|
||||
minimumThroughput: 400,
|
||||
id: "test",
|
||||
offerDefinition: mockOfferDefinition,
|
||||
offerReplacePending: false
|
||||
};
|
||||
|
||||
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
|
||||
import { OfferResponse } from "@azure/cosmos";
|
||||
import { HttpHeaders } from "./Constants";
|
||||
|
||||
export const parseSDKOfferResponse = (offerResponse: OfferResponse): Offer => {
|
||||
const offerDefinition: SDKOfferDefinition = offerResponse?.resource;
|
||||
const offerContent = offerDefinition.content;
|
||||
if (!offerContent) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const minimumThroughput = offerContent.collectionThroughputInfo?.minimumRUForCollection;
|
||||
const autopilotSettings = offerContent.offerAutopilotSettings;
|
||||
|
||||
if (autopilotSettings) {
|
||||
return {
|
||||
id: offerDefinition.id,
|
||||
autoscaleMaxThroughput: autopilotSettings.maxThroughput,
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput,
|
||||
offerDefinition,
|
||||
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: offerDefinition.id,
|
||||
autoscaleMaxThroughput: undefined,
|
||||
manualThroughput: offerContent.offerThroughput,
|
||||
minimumThroughput,
|
||||
offerDefinition,
|
||||
offerReplacePending: offerResponse.headers?.[HttpHeaders.offerReplacePending] === "true"
|
||||
};
|
||||
};
|
||||
@@ -16,7 +16,7 @@ const notificationsPath = () => {
|
||||
};
|
||||
|
||||
export const fetchPortalNotifications = async (): Promise<DataModels.Notification[]> => {
|
||||
if (configContext.platform === Platform.Emulator || configContext.platform === Platform.Hosted) {
|
||||
if (configContext.platform === Platform.Emulator) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -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,92 +0,0 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { armRequest } from "../../Utils/arm/request";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
interface TimeSeriesData {
|
||||
data: {
|
||||
timeStamp: string;
|
||||
total: number;
|
||||
}[];
|
||||
metadatavalues: {
|
||||
name: {
|
||||
localizedValue: string;
|
||||
value: string;
|
||||
};
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface MetricsData {
|
||||
displayDescription: string;
|
||||
errorCode: string;
|
||||
id: string;
|
||||
name: {
|
||||
value: string;
|
||||
localizedValue: string;
|
||||
};
|
||||
timeseries: TimeSeriesData[];
|
||||
type: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
interface MetricsResponse {
|
||||
cost: number;
|
||||
interval: string;
|
||||
namespace: string;
|
||||
resourceregion: string;
|
||||
timespan: string;
|
||||
value: MetricsData[];
|
||||
}
|
||||
|
||||
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
|
||||
if (window.authType !== AuthType.AAD) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
const filter = `DatabaseName eq '${databaseName}' and CollectionName eq '${containerName}'`;
|
||||
const metricNames = "DataUsage,IndexUsage";
|
||||
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/providers/microsoft.insights/metrics`;
|
||||
|
||||
try {
|
||||
const metricsResponse: MetricsResponse = await armRequest({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path,
|
||||
method: "GET",
|
||||
apiVersion: "2018-01-01",
|
||||
queryParams: {
|
||||
filter,
|
||||
metricNames
|
||||
}
|
||||
});
|
||||
|
||||
if (metricsResponse?.value?.length !== 2) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const dataUsageData: MetricsData = metricsResponse.value[0];
|
||||
const indexUsagedata: MetricsData = metricsResponse.value[1];
|
||||
const dataUsageSizeInKb: number = getUsageSizeInKb(dataUsageData);
|
||||
const indexUsageSizeInKb: number = getUsageSizeInKb(indexUsagedata);
|
||||
|
||||
return dataUsageSizeInKb + indexUsageSizeInKb;
|
||||
} catch (error) {
|
||||
handleError(error, "getCollectionUsageSize");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getUsageSizeInKb = (metricsData: MetricsData): number => {
|
||||
if (metricsData?.errorCode !== "Success") {
|
||||
throw Error(`Get collection usage size failed: ${metricsData.errorCode}`);
|
||||
}
|
||||
|
||||
const timeSeriesData: TimeSeriesData = metricsData?.timeseries?.[0];
|
||||
const usageSizeInBytes: number = timeSeriesData?.data?.[0]?.total;
|
||||
|
||||
return usageSizeInBytes ? usageSizeInBytes / 1024 : 0;
|
||||
};
|
||||
@@ -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,6 +1,9 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { Offer, ReadCollectionOfferParams } from "../../Contracts/DataModels";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||
@@ -8,22 +11,50 @@ import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/20
|
||||
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||
import { readOffers } from "./readOffers";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
|
||||
export const readCollectionOffer = async (
|
||||
params: DataModels.ReadCollectionOfferParams
|
||||
): Promise<DataModels.OfferWithHeaders> => {
|
||||
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
|
||||
let offerId = params.offerId;
|
||||
if (!offerId) {
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
try {
|
||||
offerId = await getCollectionOfferIdWithARM(params.databaseId, params.collectionId);
|
||||
} catch (error) {
|
||||
clearMessage();
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
offerId = await getCollectionOfferIdWithSDK(params.collectionResourceId);
|
||||
if (!offerId) {
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const options: RequestOptions = {
|
||||
initialHeaders: {
|
||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (
|
||||
window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
) {
|
||||
return await readCollectionOfferWithARM(params.databaseId, params.collectionId);
|
||||
}
|
||||
|
||||
return await readOfferWithSDK(params.offerId, params.collectionResourceId);
|
||||
const response = await client()
|
||||
.offer(offerId)
|
||||
.read(options);
|
||||
return (
|
||||
response && {
|
||||
...response.resource,
|
||||
headers: response.headers
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
handleError(error, "ReadCollectionOffer", `Error while querying offer for collection ${params.collectionId}`);
|
||||
throw error;
|
||||
@@ -32,92 +63,61 @@ export const readCollectionOffer = async (params: ReadCollectionOfferParams): Pr
|
||||
}
|
||||
};
|
||||
|
||||
const readCollectionOfferWithARM = async (databaseId: string, collectionId: string): Promise<Offer> => {
|
||||
const getCollectionOfferIdWithARM = async (databaseId: string, collectionId: string): Promise<string> => {
|
||||
let rpResponse;
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
const defaultExperience = userContext.defaultExperience;
|
||||
|
||||
let rpResponse;
|
||||
try {
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
rpResponse = await getSqlContainerThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.MongoDB:
|
||||
rpResponse = await getMongoDBCollectionThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Cassandra:
|
||||
rpResponse = await getCassandraTableThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Graph:
|
||||
rpResponse = await getGremlinGraphThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Table:
|
||||
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
rpResponse = await getSqlContainerThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.MongoDB:
|
||||
rpResponse = await getMongoDBCollectionThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Cassandra:
|
||||
rpResponse = await getCassandraTableThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Graph:
|
||||
rpResponse = await getGremlinGraphThroughput(
|
||||
subscriptionId,
|
||||
resourceGroup,
|
||||
accountName,
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Table:
|
||||
rpResponse = await getTableThroughput(subscriptionId, resourceGroup, accountName, collectionId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||
}
|
||||
|
||||
const resource = rpResponse?.properties?.resource;
|
||||
if (resource) {
|
||||
const offerId: string = rpResponse.name;
|
||||
const minimumThroughput: number =
|
||||
typeof resource.minimumThroughput === "string"
|
||||
? parseInt(resource.minimumThroughput)
|
||||
: resource.minimumThroughput;
|
||||
const autoscaleSettings = resource.autoscaleSettings;
|
||||
|
||||
if (autoscaleSettings) {
|
||||
return {
|
||||
id: offerId,
|
||||
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: offerId,
|
||||
autoscaleMaxThroughput: undefined,
|
||||
manualThroughput: resource.throughput,
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true"
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return rpResponse?.name;
|
||||
};
|
||||
|
||||
const getCollectionOfferIdWithSDK = async (collectionResourceId: string): Promise<string> => {
|
||||
const offers = await readOffers();
|
||||
const offer = offers.find(offer => offer.resource === collectionResourceId);
|
||||
return offer?.id;
|
||||
};
|
||||
|
||||
45
src/Common/dataAccess/readCollectionQuotaInfo.ts
Normal file
45
src/Common/dataAccess/readCollectionQuotaInfo.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as HeadersUtility from "../HeadersUtility";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { ContainerDefinition, Resource } from "@azure/cosmos";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
|
||||
interface ResourceWithStatistics {
|
||||
statistics: DataModels.Statistic[];
|
||||
}
|
||||
|
||||
export const readCollectionQuotaInfo = async (
|
||||
collection: ViewModels.Collection
|
||||
): Promise<DataModels.CollectionQuotaInfo> => {
|
||||
const clearMessage = logConsoleProgress(`Querying containers for database ${collection.id}`);
|
||||
const options: RequestOptions = {};
|
||||
options.populateQuotaInfo = true;
|
||||
options.initialHeaders = options.initialHeaders || {};
|
||||
options.initialHeaders[HttpHeaders.populatePartitionStatistics] = true;
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.read(options);
|
||||
const quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers);
|
||||
const resource = response.resource as ContainerDefinition & Resource & ResourceWithStatistics;
|
||||
quota["usageSizeInKB"] = resource.statistics.reduce(
|
||||
(previousValue: number, currentValue: DataModels.Statistic) => previousValue + currentValue.sizeInKB,
|
||||
0
|
||||
);
|
||||
quota["numPartitions"] = resource.statistics.length;
|
||||
quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617)
|
||||
|
||||
return quota;
|
||||
} catch (error) {
|
||||
handleError(error, "ReadCollectionQuotaInfo", `Error while querying quota info for container ${collection.id}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,28 +1,51 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { Offer, ReadDatabaseOfferParams } from "../../Contracts/DataModels";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getSqlDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { getMongoDBDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||
import { getCassandraKeyspaceThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||
import { getGremlinDatabaseThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { readOfferWithSDK } from "./readOfferWithSDK";
|
||||
import { readOffers } from "./readOffers";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promise<Offer> => {
|
||||
export const readDatabaseOffer = async (
|
||||
params: DataModels.ReadDatabaseOfferParams
|
||||
): Promise<DataModels.OfferWithHeaders> => {
|
||||
const clearMessage = logConsoleProgress(`Querying offer for database ${params.databaseId}`);
|
||||
let offerId = params.offerId;
|
||||
if (!offerId) {
|
||||
offerId = await (window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
? getDatabaseOfferIdWithARM(params.databaseId)
|
||||
: getDatabaseOfferIdWithSDK(params.databaseResourceId));
|
||||
if (!offerId) {
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const options: RequestOptions = {
|
||||
initialHeaders: {
|
||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (
|
||||
window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
) {
|
||||
return await readDatabaseOfferWithARM(params.databaseId);
|
||||
}
|
||||
|
||||
return await readOfferWithSDK(params.offerId, params.databaseResourceId);
|
||||
const response = await client()
|
||||
.offer(offerId)
|
||||
.read(options);
|
||||
return (
|
||||
response && {
|
||||
...response.resource,
|
||||
headers: response.headers
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
handleError(error, "ReadDatabaseOffer", `Error while querying offer for database ${params.databaseId}`);
|
||||
throw error;
|
||||
@@ -31,13 +54,13 @@ export const readDatabaseOffer = async (params: ReadDatabaseOfferParams): Promis
|
||||
}
|
||||
};
|
||||
|
||||
const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||
const getDatabaseOfferIdWithARM = async (databaseId: string): Promise<string> => {
|
||||
let rpResponse;
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const accountName = userContext.databaseAccount.name;
|
||||
const defaultExperience = userContext.defaultExperience;
|
||||
|
||||
let rpResponse;
|
||||
try {
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
@@ -55,41 +78,18 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||
}
|
||||
|
||||
return rpResponse?.name;
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const resource = rpResponse?.properties?.resource;
|
||||
if (resource) {
|
||||
const offerId: string = rpResponse.name;
|
||||
const minimumThroughput: number =
|
||||
typeof resource.minimumThroughput === "string"
|
||||
? parseInt(resource.minimumThroughput)
|
||||
: resource.minimumThroughput;
|
||||
const autoscaleSettings = resource.autoscaleSettings;
|
||||
|
||||
if (autoscaleSettings) {
|
||||
return {
|
||||
id: offerId,
|
||||
autoscaleMaxThroughput: autoscaleSettings.maxThroughput,
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: offerId,
|
||||
autoscaleMaxThroughput: undefined,
|
||||
manualThroughput: resource.throughput,
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true"
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {
|
||||
const offers = await readOffers();
|
||||
const offer = offers.find(offer => offer.resource === databaseResourceId);
|
||||
return offer?.id;
|
||||
};
|
||||
|
||||
@@ -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,29 +0,0 @@
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { Offer } from "../../Contracts/DataModels";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { client } from "../CosmosClient";
|
||||
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||
import { readOffers } from "./readOffers";
|
||||
|
||||
export const readOfferWithSDK = async (offerId: string, resourceId: string): Promise<Offer> => {
|
||||
if (!offerId) {
|
||||
const offers = await readOffers();
|
||||
const offer = offers.find(offer => offer.resource === resourceId);
|
||||
|
||||
if (!offer) {
|
||||
return undefined;
|
||||
}
|
||||
offerId = offer.id;
|
||||
}
|
||||
|
||||
const options: RequestOptions = {
|
||||
initialHeaders: {
|
||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||
}
|
||||
};
|
||||
const response = await client()
|
||||
.offer(offerId)
|
||||
.read(options);
|
||||
|
||||
return parseSDKOfferResponse(response);
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { SDKOfferDefinition } from "../../Contracts/DataModels";
|
||||
import { Offer } from "../../Contracts/DataModels";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError, getErrorMessage } from "../ErrorHandlingUtils";
|
||||
|
||||
export const readOffers = async (): Promise<SDKOfferDefinition[]> => {
|
||||
export const readOffers = async (): Promise<Offer[]> => {
|
||||
const clearMessage = logConsoleProgress(`Querying offers`);
|
||||
|
||||
try {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -1,14 +1,13 @@
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { Offer, SDKOfferDefinition, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { Offer, UpdateOfferParams } from "../../Contracts/DataModels";
|
||||
import { OfferDefinition } from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { ThroughputSettingsUpdateParameters } from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { parseSDKOfferResponse } from "../OfferUtility";
|
||||
import { readCollectionOffer } from "./readCollectionOffer";
|
||||
import { readDatabaseOffer } from "./readDatabaseOffer";
|
||||
import {
|
||||
@@ -374,21 +373,21 @@ const createUpdateOfferBody = (params: UpdateOfferParams): ThroughputSettingsUpd
|
||||
};
|
||||
|
||||
const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> => {
|
||||
const sdkOfferDefinition = params.currentOffer.offerDefinition;
|
||||
const newOffer: SDKOfferDefinition = {
|
||||
const currentOffer = params.currentOffer;
|
||||
const newOffer: Offer = {
|
||||
content: {
|
||||
offerThroughput: undefined,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
},
|
||||
_etag: undefined,
|
||||
_ts: undefined,
|
||||
_rid: sdkOfferDefinition._rid,
|
||||
_self: sdkOfferDefinition._self,
|
||||
id: sdkOfferDefinition.id,
|
||||
offerResourceId: sdkOfferDefinition.offerResourceId,
|
||||
offerVersion: sdkOfferDefinition.offerVersion,
|
||||
offerType: sdkOfferDefinition.offerType,
|
||||
resource: sdkOfferDefinition.resource
|
||||
_rid: currentOffer._rid,
|
||||
_self: currentOffer._self,
|
||||
id: currentOffer.id,
|
||||
offerResourceId: currentOffer.offerResourceId,
|
||||
offerVersion: currentOffer.offerVersion,
|
||||
offerType: currentOffer.offerType,
|
||||
resource: currentOffer.resource
|
||||
};
|
||||
|
||||
if (params.autopilotThroughput) {
|
||||
@@ -416,6 +415,5 @@ const updateOfferWithSDK = async (params: UpdateOfferParams): Promise<Offer> =>
|
||||
.offer(params.currentOffer.id)
|
||||
// TODO Remove casting when SDK types are fixed (https://github.com/Azure/azure-sdk-for-js/issues/10660)
|
||||
.replace((newOffer as unknown) as OfferDefinition, options);
|
||||
|
||||
return parseSDKOfferResponse(sdkResponse);
|
||||
return sdkResponse?.resource;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { updateOfferThroughputBeyondLimit } from "./updateOfferThroughputBeyondLimit";
|
||||
|
||||
describe("updateOfferThroughputBeyondLimit", () => {
|
||||
it("should call fetch", async () => {
|
||||
window.fetch = jest.fn(() => {
|
||||
return {
|
||||
ok: true
|
||||
};
|
||||
});
|
||||
window.dataExplorer = {
|
||||
logConsoleData: jest.fn(),
|
||||
deleteInProgressConsoleDataWithId: jest.fn()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
await updateOfferThroughputBeyondLimit({
|
||||
subscriptionId: "foo",
|
||||
resourceGroup: "foo",
|
||||
databaseAccountName: "foo",
|
||||
databaseName: "foo",
|
||||
throughput: 1000000000,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
});
|
||||
expect(window.fetch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
57
src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts
Normal file
57
src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Platform, configContext } from "../../ConfigContext";
|
||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||
import { AutoPilotOfferSettings } from "../../Contracts/DataModels";
|
||||
import { logConsoleProgress, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
interface UpdateOfferThroughputRequest {
|
||||
subscriptionId: string;
|
||||
resourceGroup: string;
|
||||
databaseAccountName: string;
|
||||
databaseName: string;
|
||||
collectionName?: string;
|
||||
throughput: number;
|
||||
offerIsRUPerMinuteThroughputEnabled: boolean;
|
||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
||||
}
|
||||
|
||||
export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThroughputRequest): Promise<void> {
|
||||
if (configContext.platform !== Platform.Portal) {
|
||||
throw new Error("Updating throughput beyond specified limit is not supported on this platform");
|
||||
}
|
||||
|
||||
const resourceDescriptionInfo = request.collectionName
|
||||
? `database ${request.databaseName} and container ${request.collectionName}`
|
||||
: `database ${request.databaseName}`;
|
||||
|
||||
const clearMessage = logConsoleProgress(
|
||||
`Requesting increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
||||
);
|
||||
|
||||
const url = `${configContext.BACKEND_ENDPOINT}/api/offerthroughputrequest/updatebeyondspecifiedlimit`;
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(request),
|
||||
headers: { [authorizationHeader.header]: authorizationHeader.token, [HttpHeaders.contentType]: "application/json" }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
logConsoleInfo(
|
||||
`Successfully requested an increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
||||
);
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const error = await response.json();
|
||||
handleError(
|
||||
error,
|
||||
"updateOfferThroughputBeyondLimit",
|
||||
`Failed to request an increase in throughput for ${request.throughput}`
|
||||
);
|
||||
clearMessage();
|
||||
throw error;
|
||||
}
|
||||
@@ -208,21 +208,12 @@ export interface QueryMetrics {
|
||||
vmExecutionTime: any;
|
||||
}
|
||||
|
||||
export interface Offer {
|
||||
id: string;
|
||||
autoscaleMaxThroughput: number;
|
||||
manualThroughput: number;
|
||||
minimumThroughput: number;
|
||||
offerDefinition?: SDKOfferDefinition;
|
||||
offerReplacePending: boolean;
|
||||
}
|
||||
|
||||
export interface SDKOfferDefinition extends Resource {
|
||||
export interface Offer extends Resource {
|
||||
offerVersion?: string;
|
||||
offerType?: string;
|
||||
content?: {
|
||||
offerThroughput: number;
|
||||
offerIsRUPerMinuteThroughputEnabled?: boolean;
|
||||
offerIsRUPerMinuteThroughputEnabled: boolean;
|
||||
collectionThroughputInfo?: OfferThroughputInfo;
|
||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
||||
};
|
||||
@@ -230,6 +221,22 @@ export interface SDKOfferDefinition extends Resource {
|
||||
offerResourceId?: string;
|
||||
}
|
||||
|
||||
export interface OfferWithHeaders extends Offer {
|
||||
headers: any;
|
||||
}
|
||||
|
||||
export interface CollectionQuotaInfo {
|
||||
storedProcedures: number;
|
||||
triggers: number;
|
||||
functions: number;
|
||||
documentsSize: number;
|
||||
collectionSize: number;
|
||||
documentsCount: number;
|
||||
usageSizeInKB: number;
|
||||
numPartitions: number;
|
||||
uniqueKeyPolicy?: UniqueKeyPolicy; // TODO: This should ideally not be a part of the collection quota. Remove after refactoring. (#119617)
|
||||
}
|
||||
|
||||
export interface OfferThroughputInfo {
|
||||
minimumRUForCollection: number;
|
||||
numPhysicalPartitions: number;
|
||||
@@ -248,6 +255,7 @@ export interface CreateDatabaseAndCollectionRequest {
|
||||
collectionId: string;
|
||||
offerThroughput: number;
|
||||
databaseLevelThroughput: boolean;
|
||||
rupmEnabled?: boolean;
|
||||
partitionKey?: PartitionKey;
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||
|
||||
@@ -32,8 +32,7 @@ export enum MessageTypes {
|
||||
GetArcadiaToken,
|
||||
CreateWorkspace,
|
||||
CreateSparkPool,
|
||||
RefreshDatabaseAccount,
|
||||
InitTestExplorer
|
||||
RefreshDatabaseAccount
|
||||
}
|
||||
|
||||
export { Versions, ActionContracts, Diagnostics };
|
||||
|
||||
@@ -120,7 +120,7 @@ export interface Collection extends CollectionBase {
|
||||
requestSchema?: () => void;
|
||||
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
|
||||
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
|
||||
usageSizeInKB: ko.Observable<number>;
|
||||
quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
|
||||
offer: ko.Observable<DataModels.Offer>;
|
||||
conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
|
||||
changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
|
||||
|
||||
@@ -44,6 +44,10 @@ describe("Component Registerer", () => {
|
||||
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register settings-tab component", () => {
|
||||
expect(ko.components.isRegistered("settings-tab")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register settings-tab-v2 component", () => {
|
||||
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -26,11 +26,12 @@ ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponent
|
||||
ko.components.register("tabs-manager", TabsManagerKOComponent());
|
||||
|
||||
// Collection Tabs
|
||||
ko.components.register("documents-tab", new TabComponents.DocumentsTab());
|
||||
ko.components.register("documents-tab", new TabComponents.MongoDocumentsTabV2());
|
||||
ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab());
|
||||
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
|
||||
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
|
||||
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
|
||||
ko.components.register("settings-tab", new TabComponents.SettingsTab());
|
||||
ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2());
|
||||
ko.components.register("query-tab", new TabComponents.QueryTab());
|
||||
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
|
||||
|
||||
@@ -44,10 +44,12 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
onChange?: (_?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => void;
|
||||
}[] = [
|
||||
{ key: "feature.enablechangefeedpolicy", label: "Enable change feed policy", value: "true" },
|
||||
{ key: "feature.enablerupm", label: "Enable RUPM", value: "true" },
|
||||
{ key: "feature.dataexplorerexecutesproc", label: "Execute stored procedure", value: "true" },
|
||||
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
||||
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
||||
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
|
||||
{ key: "feature.enablecodeofconduct", label: "Enable Code Of Conduct Acknowledgement", value: "true" },
|
||||
{
|
||||
key: "feature.enableLinkInjection",
|
||||
label: "Enable Injecting Notebook Viewer Link into the first cell",
|
||||
|
||||
@@ -131,6 +131,12 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
label="Enable change feed policy"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablerupm"
|
||||
label="Enable RUPM"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.dataexplorerexecutesproc"
|
||||
@@ -157,14 +163,14 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enableLinkInjection"
|
||||
label="Enable Injecting Notebook Viewer Link into the first cell"
|
||||
key="feature.enablecodeofconduct"
|
||||
label="Enable Code Of Conduct Acknowledgement"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.canexceedmaximumvalue"
|
||||
label="Can exceed max value"
|
||||
key="feature.enableLinkInjection"
|
||||
label="Enable Injecting Notebook Viewer Link into the first cell"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -172,6 +178,12 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
className="checkboxRow"
|
||||
horizontalAlign="space-between"
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.canexceedmaximumvalue"
|
||||
label="Can exceed max value"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablefixedcollectionwithsharedthroughput"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
Dropdown,
|
||||
FocusZone,
|
||||
FontIcon,
|
||||
FontWeights,
|
||||
IDropdownOption,
|
||||
IPageSpecification,
|
||||
@@ -17,7 +16,7 @@ import {
|
||||
Text
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient";
|
||||
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
|
||||
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
|
||||
@@ -137,7 +136,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||
|
||||
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||
tabs.push(
|
||||
@@ -147,7 +146,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
this.state.isCodeOfConductAccepted
|
||||
)
|
||||
);
|
||||
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||
|
||||
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
|
||||
// Displaying code of conduct component on gallery load should not be the default behavior.
|
||||
@@ -184,27 +183,6 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
);
|
||||
}
|
||||
|
||||
private isEmptyData = (data: IGalleryItem[]): boolean => {
|
||||
return !data || data.length === 0;
|
||||
};
|
||||
|
||||
private createEmptyTabContent = (iconName: string, line1: string, line2: string): JSX.Element => {
|
||||
return (
|
||||
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}>
|
||||
<FontIcon iconName={iconName} style={{ fontSize: 100, color: "lightgray", marginTop: 20 }} />
|
||||
<Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{line1}</Text>
|
||||
<Text>{line2}</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
private createSamplesTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||
return {
|
||||
tab,
|
||||
content: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||
};
|
||||
};
|
||||
|
||||
private createPublicGalleryTab(
|
||||
tab: GalleryTab,
|
||||
data: IGalleryItem[],
|
||||
@@ -216,29 +194,17 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
};
|
||||
}
|
||||
|
||||
private createFavoritesTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||
return {
|
||||
tab,
|
||||
content: this.isEmptyData(data)
|
||||
? this.createEmptyTabContent(
|
||||
"ContactHeart",
|
||||
"You have not liked anything",
|
||||
"Like any notebook from Official Samples or Public gallery"
|
||||
)
|
||||
: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||
content: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||
};
|
||||
}
|
||||
|
||||
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||
return {
|
||||
tab,
|
||||
content: this.isEmptyData(data)
|
||||
? this.createEmptyTabContent(
|
||||
"Contact",
|
||||
"You have not published anything",
|
||||
"Publish your sample notebooks to share your published work with others"
|
||||
)
|
||||
: this.createPublishedNotebooksTabContent(data)
|
||||
content: this.createPublishedNotebooksTabContent(data)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -398,9 +364,9 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||
if (!offline) {
|
||||
try {
|
||||
let response: IJunoResponse<IGalleryItem[]> | IJunoResponse<IPublicGalleryData>;
|
||||
if (this.props.container) {
|
||||
response = await this.props.junoClient.getPublicGalleryData();
|
||||
let response: IJunoResponse<IPublicGalleryData> | IJunoResponse<IGalleryItem[]>;
|
||||
if (this.props.container.isCodeOfConductEnabled()) {
|
||||
response = await this.props.junoClient.fetchPublicNotebooks();
|
||||
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
|
||||
this.publicNotebooks = response.data?.notebooksData;
|
||||
} else {
|
||||
@@ -602,7 +568,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
|
||||
private deleteItem = async (data: IGalleryItem): Promise<void> => {
|
||||
GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, item => {
|
||||
this.publishedNotebooks = this.publishedNotebooks?.filter(notebook => item.id !== notebook.id);
|
||||
this.publishedNotebooks = this.publishedNotebooks.filter(notebook => item.id !== notebook.id);
|
||||
this.refreshSelectedTab(item);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -89,12 +89,12 @@ describe("SettingsComponent", () => {
|
||||
it("auto pilot helper functions pass on correct value", () => {
|
||||
const newCollection = { ...collection };
|
||||
newCollection.offer = ko.observable<DataModels.Offer>({
|
||||
autoscaleMaxThroughput: 10000,
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput: 400,
|
||||
id: "test",
|
||||
offerReplacePending: false
|
||||
});
|
||||
content: {
|
||||
offerAutopilotSettings: {
|
||||
maxThroughput: 10000
|
||||
}
|
||||
}
|
||||
} as DataModels.Offer);
|
||||
|
||||
const props = { ...baseProps };
|
||||
props.settingsTab.collection = newCollection;
|
||||
@@ -187,6 +187,21 @@ describe("SettingsComponent", () => {
|
||||
expect(settingsComponentInstance.hasConflictResolution()).toEqual(true);
|
||||
});
|
||||
|
||||
it("isOfferReplacePending", () => {
|
||||
let settingsComponentInstance = new SettingsComponent(baseProps);
|
||||
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(undefined);
|
||||
|
||||
const newCollection = { ...collection };
|
||||
newCollection.offer = ko.observable({
|
||||
headers: { "x-ms-offer-replace-pending": true }
|
||||
} as DataModels.OfferWithHeaders);
|
||||
const props = { ...baseProps };
|
||||
props.settingsTab.collection = newCollection;
|
||||
|
||||
settingsComponentInstance = new SettingsComponent(props);
|
||||
expect(settingsComponentInstance.isOfferReplacePending()).toEqual(true);
|
||||
});
|
||||
|
||||
it("save calls updateCollection, updateMongoDBCollectionThroughRP and updateOffer", async () => {
|
||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||
wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true, isMongoIndexingPolicySaveable: true });
|
||||
|
||||
@@ -2,23 +2,28 @@ import * as React from "react";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as SharedConstants from "../../../Shared/Constants";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
import SaveIcon from "../../../../images/save-cosmos.svg";
|
||||
import { traceStart, traceFailure, traceSuccess, trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import Explorer from "../../Explorer";
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { updateOfferThroughputBeyondLimit } from "../../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
||||
import SettingsTab from "../../Tabs/SettingsTabV2";
|
||||
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||
import { throughputUnit } from "./SettingsRenderUtils";
|
||||
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
||||
import {
|
||||
MongoIndexingPolicyComponent,
|
||||
MongoIndexingPolicyComponentProps
|
||||
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
||||
import {
|
||||
getMaxRUs,
|
||||
hasDatabaseSharedThroughput,
|
||||
GeospatialConfigType,
|
||||
TtlType,
|
||||
@@ -44,7 +49,6 @@ import { MongoDBCollectionResource, MongoIndex } from "../../../Utils/arm/genera
|
||||
import { readMongoDBCollectionThroughRP } from "../../../Common/dataAccess/readMongoDBCollection";
|
||||
import { getIndexTransformationProgress } from "../../../Common/dataAccess/getIndexTransformationProgress";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import { isEmpty } from "underscore";
|
||||
|
||||
interface SettingsV2TabInfo {
|
||||
tab: SettingsV2TabTypes;
|
||||
@@ -223,6 +227,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
public loadMongoIndexes = async (): Promise<void> => {
|
||||
if (
|
||||
this.container.isMongoIndexEditorEnabled() &&
|
||||
this.container.isPreferredApiMongoDB() &&
|
||||
this.container.isEnableMongoCapabilityPresent() &&
|
||||
this.container.databaseAccount()
|
||||
@@ -270,14 +275,19 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
};
|
||||
|
||||
private setAutoPilotStates = (): void => {
|
||||
const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput;
|
||||
const offer = this.collection?.offer && this.collection.offer();
|
||||
const offerAutopilotSettings = offer?.content?.offerAutopilotSettings;
|
||||
|
||||
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||
if (
|
||||
offerAutopilotSettings &&
|
||||
offerAutopilotSettings.maxThroughput &&
|
||||
AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)
|
||||
) {
|
||||
this.setState({
|
||||
isAutoPilotSelected: true,
|
||||
wasAutopilotOriginallySet: true,
|
||||
autoPilotThroughput: autoscaleMaxThroughput,
|
||||
autoPilotThroughputBaseline: autoscaleMaxThroughput
|
||||
autoPilotThroughput: offerAutopilotSettings.maxThroughput,
|
||||
autoPilotThroughputBaseline: offerAutopilotSettings.maxThroughput
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -295,7 +305,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
!!this.collection.conflictResolutionPolicy();
|
||||
|
||||
public isOfferReplacePending = (): boolean => {
|
||||
return this.collection?.offer()?.offerReplacePending;
|
||||
const offer = this.collection?.offer && this.collection.offer();
|
||||
return (
|
||||
offer &&
|
||||
Object.keys(offer).find(value => value === "headers") &&
|
||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||
);
|
||||
};
|
||||
|
||||
public onSaveClick = async (): Promise<void> => {
|
||||
@@ -433,33 +448,102 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
|
||||
if (this.state.isScaleSaveable) {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.collection.databaseId,
|
||||
collectionId: this.collection.id(),
|
||||
currentOffer: this.collection.offer(),
|
||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput
|
||||
};
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
updateOfferParams.migrateToAutoPilot = true;
|
||||
} else {
|
||||
updateOfferParams.migrateToManual = true;
|
||||
}
|
||||
const newThroughput = this.state.throughput;
|
||||
const newOffer: DataModels.Offer = { ...this.collection.offer() };
|
||||
const originalThroughputValue: number = this.state.throughput;
|
||||
|
||||
if (newOffer.content) {
|
||||
newOffer.content.offerThroughput = newThroughput;
|
||||
} else {
|
||||
newOffer.content = {
|
||||
offerThroughput: newThroughput,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
};
|
||||
}
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||
this.collection.offer(updatedOffer);
|
||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||
|
||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
||||
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
this.setState({
|
||||
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
|
||||
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput
|
||||
});
|
||||
newOffer.content.offerAutopilotSettings = {
|
||||
maxThroughput: this.state.autoPilotThroughput
|
||||
};
|
||||
|
||||
// user has changed from provisioned --> autoscale
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
|
||||
delete newOffer.content.offerAutopilotSettings;
|
||||
} else {
|
||||
delete newOffer.content.offerThroughput;
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
throughput: updatedOffer.manualThroughput,
|
||||
throughputBaseline: updatedOffer.manualThroughput
|
||||
isAutoPilotSelected: false
|
||||
});
|
||||
|
||||
// user has changed from autoscale --> provisioned
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
|
||||
} else {
|
||||
delete newOffer.content.offerAutopilotSettings;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
getMaxRUs(this.collection, this.container) <=
|
||||
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.container
|
||||
) {
|
||||
const requestPayload = {
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
databaseAccountName: userContext.databaseAccount.name,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseName: this.collection.databaseId,
|
||||
collectionName: this.collection.id(),
|
||||
throughput: newThroughput,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
};
|
||||
|
||||
await updateOfferThroughputBeyondLimit(requestPayload);
|
||||
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
||||
this.setState({
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
throughput: originalThroughputValue,
|
||||
throughputBaseline: originalThroughputValue,
|
||||
initialNotification: {
|
||||
description: `Throughput update for ${newThroughput} ${throughputUnit}`
|
||||
} as DataModels.Notification
|
||||
});
|
||||
} else {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.collection.databaseId,
|
||||
collectionId: this.collection.id(),
|
||||
currentOffer: this.collection.offer(),
|
||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : newThroughput
|
||||
};
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
updateOfferParams.migrateToAutoPilot = true;
|
||||
} else {
|
||||
updateOfferParams.migrateToManual = true;
|
||||
}
|
||||
}
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||
this.collection.offer(updatedOffer);
|
||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
this.setState({
|
||||
autoPilotThroughput: updatedOffer.content.offerAutopilotSettings.maxThroughput,
|
||||
autoPilotThroughputBaseline: updatedOffer.content.offerAutopilotSettings.maxThroughput
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
throughput: updatedOffer.content.offerThroughput,
|
||||
throughputBaseline: updatedOffer.content.offerThroughput
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
this.container.isRefreshingExplorer(false);
|
||||
@@ -725,7 +809,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
}
|
||||
|
||||
const offerThroughput = this.collection.offer()?.manualThroughput;
|
||||
const offerThroughput = this.collection?.offer && this.collection.offer()?.content?.offerThroughput;
|
||||
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
||||
? ChangeFeedPolicyState.On
|
||||
: ChangeFeedPolicyState.Off;
|
||||
@@ -916,18 +1000,15 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
|
||||
});
|
||||
} else if (this.container.isPreferredApiMongoDB()) {
|
||||
if (isEmpty(this.container.features())) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: mongoIndexingPolicyAADError
|
||||
});
|
||||
} else if (this.container.isEnableMongoCapabilityPresent()) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
this.container.isMongoIndexEditorEnabled() &&
|
||||
this.container.isPreferredApiMongoDB() &&
|
||||
this.container.isEnableMongoCapabilityPresent()
|
||||
) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hasConflictResolution()) {
|
||||
|
||||
@@ -31,7 +31,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
{getAutoPilotV3SpendElement(1000, true)}
|
||||
{getAutoPilotV3SpendElement(undefined, true)}
|
||||
|
||||
{getEstimatedSpendElement(1000, "mooncake", 2, false)}
|
||||
{getEstimatedSpendElement(1000, "mooncake", 2, false, true)}
|
||||
|
||||
{getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)}
|
||||
|
||||
@@ -42,7 +42,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
{updateThroughputDelayedApplyWarningMessage}
|
||||
|
||||
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection")}
|
||||
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
{getThroughputApplyLongDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
|
||||
{getToolTipContainer(<span>Sample Text</span>)}
|
||||
|
||||
@@ -199,9 +199,10 @@ export const getEstimatedSpendElement = (
|
||||
throughput: number,
|
||||
serverId: string,
|
||||
regions: number,
|
||||
multimaster: boolean
|
||||
multimaster: boolean,
|
||||
rupmEnabled: boolean
|
||||
): JSX.Element => {
|
||||
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, throughput, regions, multimaster);
|
||||
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, rupmEnabled, throughput, regions, multimaster);
|
||||
const dailyPrice: number = hourlyPrice * 24;
|
||||
const monthlyPrice: number = hourlyPrice * hoursInAMonth;
|
||||
const currency: string = getPriceCurrency(serverId);
|
||||
@@ -318,13 +319,14 @@ export const getThroughputApplyShortDelayMessage = (
|
||||
throughput: number,
|
||||
throughputUnit: string,
|
||||
databaseName: string,
|
||||
collectionName: string
|
||||
collectionName: string,
|
||||
targetThroughput: number
|
||||
): JSX.Element => (
|
||||
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
|
||||
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
||||
<br />
|
||||
Database: {databaseName}, Container: {collectionName}{" "}
|
||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
|
||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, targetThroughput)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
transparentDetailsRowStyles,
|
||||
createAndAddMongoIndexStackProps,
|
||||
separatorStyles,
|
||||
mongoIndexingPolicyAADError,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
infoAndToolTipTextStyle
|
||||
} from "../../SettingsRenderUtils";
|
||||
@@ -39,6 +40,7 @@ import {
|
||||
} from "../../SettingsUtils";
|
||||
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
||||
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { AuthType } from "../../../../../AuthType";
|
||||
import { IndexingPolicyRefreshComponent } from "../IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||
|
||||
export interface MongoIndexingPolicyComponentProps {
|
||||
@@ -319,7 +321,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
</Stack>
|
||||
);
|
||||
} else {
|
||||
return <Spinner size={SpinnerSize.large} />;
|
||||
return window.authType !== AuthType.AAD ? mongoIndexingPolicyAADError : <Spinner size={SpinnerSize.large} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("ScaleComponent", () => {
|
||||
} as DataModels.Notification
|
||||
};
|
||||
|
||||
it("renders with correct initial notification", () => {
|
||||
it("renders with correct intiial notification", () => {
|
||||
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
||||
@@ -54,13 +54,16 @@ describe("ScaleComponent", () => {
|
||||
|
||||
const newCollection = { ...collection };
|
||||
const maxThroughput = 5000;
|
||||
const targetMaxThroughput = 50000;
|
||||
newCollection.offer = ko.observable({
|
||||
manualThroughput: undefined,
|
||||
autoscaleMaxThroughput: maxThroughput,
|
||||
minimumThroughput: 400,
|
||||
id: "offer",
|
||||
offerReplacePending: true
|
||||
});
|
||||
content: {
|
||||
offerAutopilotSettings: {
|
||||
maxThroughput: maxThroughput,
|
||||
targetMaxThroughput: targetMaxThroughput
|
||||
}
|
||||
},
|
||||
headers: { "x-ms-offer-replace-pending": true }
|
||||
} as DataModels.OfferWithHeaders);
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
initialNotification: undefined as DataModels.Notification,
|
||||
@@ -70,6 +73,7 @@ describe("ScaleComponent", () => {
|
||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
||||
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(false);
|
||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(maxThroughput);
|
||||
expect(wrapper.find("#throughputApplyShortDelayMessage").html()).toContain(targetMaxThroughput);
|
||||
});
|
||||
|
||||
it("autoScale disabled", () => {
|
||||
@@ -105,6 +109,11 @@ describe("ScaleComponent", () => {
|
||||
expect(scaleComponent.isAutoScaleEnabled()).toEqual(true);
|
||||
});
|
||||
|
||||
it("getMaxRUThroughputInputLimit", () => {
|
||||
const scaleComponent = new ScaleComponent(baseProps);
|
||||
expect(scaleComponent.getMaxRUThroughputInputLimit()).toEqual(40000);
|
||||
});
|
||||
|
||||
it("getThroughputTitle", () => {
|
||||
let scaleComponent = new ScaleComponent(baseProps);
|
||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
||||
@@ -129,8 +138,14 @@ describe("ScaleComponent", () => {
|
||||
|
||||
it("getThroughputWarningMessage", () => {
|
||||
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
|
||||
const throughputBeyondMaxRus = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million - 1000;
|
||||
|
||||
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
|
||||
const scaleComponent = new ScaleComponent(newProps);
|
||||
let scaleComponent = new ScaleComponent(newProps);
|
||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
|
||||
|
||||
newProps.throughput = throughputBeyondMaxRus;
|
||||
scaleComponent = new ScaleComponent(newProps);
|
||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputDelayedApplyWarningMessage");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,9 +12,10 @@ import {
|
||||
throughputUnit,
|
||||
getThroughputApplyLongDelayMessage,
|
||||
getThroughputApplyShortDelayMessage,
|
||||
updateThroughputBeyondLimitWarningMessage
|
||||
updateThroughputBeyondLimitWarningMessage,
|
||||
updateThroughputDelayedApplyWarningMessage
|
||||
} from "../SettingsRenderUtils";
|
||||
import { hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||
import { getMaxRUs, getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
||||
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||
import { configContext, Platform } from "../../../../ConfigContext";
|
||||
@@ -61,7 +62,11 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
};
|
||||
|
||||
private getStorageCapacityTitle = (): JSX.Element => {
|
||||
const capacity: string = this.props.isFixedContainer ? "Fixed" : "Unlimited";
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
const isFixed =
|
||||
!this.props.collection.partitionKey ||
|
||||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey);
|
||||
const capacity: string = isFixed ? "Fixed" : "Unlimited";
|
||||
return (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<Label>Storage capacity</Label>
|
||||
@@ -70,26 +75,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
);
|
||||
};
|
||||
|
||||
public getMaxRUs = (): number => {
|
||||
if (this.props.container?.isTryCosmosDBSubscription()) {
|
||||
return Constants.TryCosmosExperience.maxRU;
|
||||
public getMaxRUThroughputInputLimit = (): number => {
|
||||
if (configContext.platform === Platform.Hosted && this.props.collection.partitionKey) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||
}
|
||||
|
||||
if (this.props.isFixedContainer) {
|
||||
return SharedConstants.CollectionCreation.MaxRUPerPartition;
|
||||
}
|
||||
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||
};
|
||||
|
||||
public getMinRUs = (): number => {
|
||||
if (this.props.container?.isTryCosmosDBSubscription()) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||
}
|
||||
|
||||
return (
|
||||
this.props.collection.offer()?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400
|
||||
);
|
||||
return getMaxRUs(this.props.collection, this.props.container);
|
||||
};
|
||||
|
||||
public getThroughputTitle = (): string => {
|
||||
@@ -97,8 +88,11 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
return AutoPilotUtils.getAutoPilotHeaderText();
|
||||
}
|
||||
|
||||
const minThroughput: string = this.getMinRUs().toLocaleString();
|
||||
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : this.getMaxRUs().toLocaleString();
|
||||
const minThroughput: string = getMinRUs(this.props.collection, this.props.container).toLocaleString();
|
||||
const maxThroughput: string =
|
||||
this.canThroughputExceedMaximumValue() && !this.props.isFixedContainer
|
||||
? "unlimited"
|
||||
: getMaxRUs(this.props.collection, this.props.container).toLocaleString();
|
||||
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
||||
};
|
||||
|
||||
@@ -115,15 +109,26 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
return this.getLongDelayMessage();
|
||||
}
|
||||
|
||||
const offer = this.props.collection?.offer();
|
||||
if (offer?.offerReplacePending) {
|
||||
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
||||
const offer = this.props.collection?.offer && this.props.collection.offer();
|
||||
if (
|
||||
offer &&
|
||||
Object.keys(offer).find(value => {
|
||||
return value === "headers";
|
||||
}) &&
|
||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||
) {
|
||||
const throughput = offer?.content?.offerAutopilotSettings?.maxThroughput;
|
||||
|
||||
const targetThroughput =
|
||||
offer.content?.offerAutopilotSettings?.targetMaxThroughput || offer?.content?.offerThroughput;
|
||||
|
||||
return getThroughputApplyShortDelayMessage(
|
||||
this.props.isAutoPilotSelected,
|
||||
throughput,
|
||||
throughputUnit,
|
||||
this.props.collection.databaseId,
|
||||
this.props.collection.id()
|
||||
this.props.collection.id(),
|
||||
targetThroughput
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,12 +138,21 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
public getThroughputWarningMessage = (): JSX.Element => {
|
||||
const throughputExceedsBackendLimits: boolean =
|
||||
this.canThroughputExceedMaximumValue() &&
|
||||
getMaxRUs(this.props.collection, this.props.container) <=
|
||||
SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||
|
||||
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
||||
return updateThroughputBeyondLimitWarningMessage;
|
||||
}
|
||||
|
||||
const throughputExceedsMaxValue: boolean =
|
||||
!this.isEmulator && this.props.throughput > getMaxRUs(this.props.collection, this.props.container);
|
||||
|
||||
if (throughputExceedsMaxValue && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
||||
return updateThroughputDelayedApplyWarningMessage;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -169,8 +183,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
throughput={this.props.throughput}
|
||||
throughputBaseline={this.props.throughputBaseline}
|
||||
onThroughputChange={this.props.onThroughputChange}
|
||||
minimum={this.getMinRUs()}
|
||||
maximum={this.getMaxRUs()}
|
||||
minimum={getMinRUs(this.props.collection, this.props.container)}
|
||||
maximum={this.getMaxRUThroughputInputLimit()}
|
||||
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
||||
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
||||
label={this.getThroughputTitle()}
|
||||
@@ -186,7 +200,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
||||
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
||||
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
||||
usageSizeInKB={this.props.collection.usageSizeInKB()}
|
||||
usageSizeInKB={this.props.collection.quotaInfo().usageSizeInKB}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -179,7 +179,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : offerThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster
|
||||
multimaster,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
estimatedSpend = getEstimatedAutoscaleSpendElement(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
@@ -48,7 +48,7 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
label="Throughput (6,000 - unlimited RU/s)"
|
||||
maxAutoPilotThroughput={4000}
|
||||
maxAutoPilotThroughputBaseline={4000}
|
||||
maximum={1000000}
|
||||
maximum={40000}
|
||||
minimum={6000}
|
||||
onAutoPilotSelected={[Function]}
|
||||
onMaxAutoPilotThroughputChange={[Function]}
|
||||
@@ -58,7 +58,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
spendAckChecked={false}
|
||||
throughput={1000}
|
||||
throughputBaseline={1000}
|
||||
usageSizeInKB={100}
|
||||
wasAutopilotOriginallySet={true}
|
||||
/>
|
||||
<Stack
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { collection } from "./TestUtils";
|
||||
import { collection, container } from "./TestUtils";
|
||||
import {
|
||||
getMaxRUs,
|
||||
getMinRUs,
|
||||
getMongoIndexType,
|
||||
getMongoNotification,
|
||||
getSanitizedInputValue,
|
||||
@@ -21,6 +23,16 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import ko from "knockout";
|
||||
|
||||
describe("SettingsUtils", () => {
|
||||
it("getMaxRUs", () => {
|
||||
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
|
||||
expect(getMaxRUs(collection, container)).toEqual(40000);
|
||||
});
|
||||
|
||||
it("getMinRUs", () => {
|
||||
expect(collection.offer().content.collectionThroughputInfo.numPhysicalPartitions).toEqual(4);
|
||||
expect(getMinRUs(collection, container)).toEqual(6000);
|
||||
});
|
||||
|
||||
it("hasDatabaseSharedThroughput", () => {
|
||||
expect(hasDatabaseSharedThroughput(collection)).toEqual(undefined);
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import * as SharedConstants from "../../../Shared/Constants";
|
||||
import * as PricingUtils from "../../../Utils/PricingUtils";
|
||||
|
||||
import Explorer from "../../Explorer";
|
||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
|
||||
const zeroValue = 0;
|
||||
@@ -67,6 +71,57 @@ export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection):
|
||||
return database?.isDatabaseShared() && !collection.offer();
|
||||
};
|
||||
|
||||
export const getMaxRUs = (collection: ViewModels.Collection, container: Explorer): number => {
|
||||
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription() || false;
|
||||
if (isTryCosmosDBSubscription) {
|
||||
return Constants.TryCosmosExperience.maxRU;
|
||||
}
|
||||
|
||||
const numPartitionsFromOffer: number =
|
||||
collection?.offer && collection.offer()?.content?.collectionThroughputInfo?.numPhysicalPartitions;
|
||||
|
||||
const numPartitionsFromQuotaInfo: number = collection?.quotaInfo()?.numPartitions;
|
||||
|
||||
const numPartitions = numPartitionsFromOffer ?? numPartitionsFromQuotaInfo ?? 1;
|
||||
|
||||
return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions;
|
||||
};
|
||||
|
||||
export const getMinRUs = (collection: ViewModels.Collection, container: Explorer): number => {
|
||||
const isTryCosmosDBSubscription = container?.isTryCosmosDBSubscription();
|
||||
if (isTryCosmosDBSubscription) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||
}
|
||||
|
||||
const offerContent = collection?.offer && collection.offer()?.content;
|
||||
|
||||
if (offerContent?.offerAutopilotSettings) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||
}
|
||||
|
||||
const collectionThroughputInfo: DataModels.OfferThroughputInfo = offerContent?.collectionThroughputInfo;
|
||||
|
||||
if (collectionThroughputInfo?.minimumRUForCollection > 0) {
|
||||
return collectionThroughputInfo.minimumRUForCollection;
|
||||
}
|
||||
|
||||
const numPartitions = collectionThroughputInfo?.numPhysicalPartitions ?? collection.quotaInfo()?.numPartitions;
|
||||
|
||||
if (!numPartitions || numPartitions === 1) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||
}
|
||||
|
||||
const baseRU = SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||
|
||||
const quotaInKb = collection.quotaInfo().collectionSize;
|
||||
const quotaInGb = PricingUtils.usageInGB(quotaInKb);
|
||||
|
||||
const perPartitionGBQuota: number = Math.max(10, quotaInGb / numPartitions);
|
||||
const baseRUbyPartitions: number = ((numPartitions * perPartitionGBQuota) / 10) * 100;
|
||||
|
||||
return Math.max(baseRU, baseRUbyPartitions);
|
||||
};
|
||||
|
||||
export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => {
|
||||
// Backend can contain different casing as it does case-insensitive comparisson
|
||||
if (!modeFromBackend) {
|
||||
|
||||
@@ -18,14 +18,17 @@ export const collection = ({
|
||||
excludedPaths: []
|
||||
}),
|
||||
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
|
||||
usageSizeInKB: ko.observable(100),
|
||||
quotaInfo: ko.observable<DataModels.CollectionQuotaInfo>({} as DataModels.CollectionQuotaInfo),
|
||||
offer: ko.observable<DataModels.Offer>({
|
||||
autoscaleMaxThroughput: undefined,
|
||||
manualThroughput: 10000,
|
||||
minimumThroughput: 6000,
|
||||
id: "offer",
|
||||
offerReplacePending: false
|
||||
}),
|
||||
content: {
|
||||
offerThroughput: 10000,
|
||||
offerIsRUPerMinuteThroughputEnabled: false,
|
||||
collectionThroughputInfo: {
|
||||
minimumRUForCollection: 6000,
|
||||
numPhysicalPartitions: 4
|
||||
} as DataModels.OfferThroughputInfo
|
||||
}
|
||||
} as DataModels.Offer),
|
||||
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
|
||||
{} as DataModels.ConflictResolutionPolicy
|
||||
),
|
||||
|
||||
@@ -133,6 +133,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyVisible": [Function],
|
||||
"requestUnitsUsageCost": [Function],
|
||||
"ruToolTipText": [Function],
|
||||
"rupm": [Function],
|
||||
"rupmVisible": [Function],
|
||||
"sharedAutoPilotThroughput": [Function],
|
||||
"sharedThroughputRangeText": [Function],
|
||||
"shouldCreateMongoWildcardIndex": [Function],
|
||||
@@ -620,6 +622,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyVisible": [Function],
|
||||
"requestUnitsUsageCost": [Function],
|
||||
"ruToolTipText": [Function],
|
||||
"rupm": [Function],
|
||||
"rupmVisible": [Function],
|
||||
"sharedAutoPilotThroughput": [Function],
|
||||
"sharedThroughputRangeText": [Function],
|
||||
"shouldCreateMongoWildcardIndex": [Function],
|
||||
@@ -731,6 +735,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
"arcadiaToken": [Function],
|
||||
"armEndpoint": [Function],
|
||||
"browseQueriesPane": BrowseQueriesPane {
|
||||
"canSaveQueries": [Function],
|
||||
"container": [Circular],
|
||||
@@ -942,6 +947,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
@@ -950,6 +956,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isLeftPaneExpanded": [Function],
|
||||
"isLinkInjectionEnabled": [Function],
|
||||
"isMongoIndexEditorEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isNotificationConsoleExpanded": [Function],
|
||||
@@ -966,6 +973,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isRightPanelV2Enabled": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isServerlessEnabled": [Function],
|
||||
"isSettingsV2Enabled": [Function],
|
||||
"isSparkEnabled": [Function],
|
||||
"isSparkEnabledForAccount": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
@@ -1022,6 +1030,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
"onToggleKeyDown": [Function],
|
||||
"parentFrameDataExplorerVersion": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -1047,6 +1056,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"titleLabel": "Select Columns",
|
||||
"visible": [Function],
|
||||
},
|
||||
"quotaId": [Function],
|
||||
"refreshDatabaseAccount": [Function],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
@@ -1292,9 +1302,9 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"version": 2,
|
||||
},
|
||||
"partitionKeyProperty": "partitionKey",
|
||||
"quotaInfo": [Function],
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": Object {},
|
||||
"usageSizeInKB": [Function],
|
||||
}
|
||||
}
|
||||
container={
|
||||
@@ -1404,6 +1414,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyVisible": [Function],
|
||||
"requestUnitsUsageCost": [Function],
|
||||
"ruToolTipText": [Function],
|
||||
"rupm": [Function],
|
||||
"rupmVisible": [Function],
|
||||
"sharedAutoPilotThroughput": [Function],
|
||||
"sharedThroughputRangeText": [Function],
|
||||
"shouldCreateMongoWildcardIndex": [Function],
|
||||
@@ -1891,6 +1903,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyVisible": [Function],
|
||||
"requestUnitsUsageCost": [Function],
|
||||
"ruToolTipText": [Function],
|
||||
"rupm": [Function],
|
||||
"rupmVisible": [Function],
|
||||
"sharedAutoPilotThroughput": [Function],
|
||||
"sharedThroughputRangeText": [Function],
|
||||
"shouldCreateMongoWildcardIndex": [Function],
|
||||
@@ -2002,6 +2016,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
"arcadiaToken": [Function],
|
||||
"armEndpoint": [Function],
|
||||
"browseQueriesPane": BrowseQueriesPane {
|
||||
"canSaveQueries": [Function],
|
||||
"container": [Circular],
|
||||
@@ -2213,6 +2228,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
@@ -2221,6 +2237,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isLeftPaneExpanded": [Function],
|
||||
"isLinkInjectionEnabled": [Function],
|
||||
"isMongoIndexEditorEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isNotificationConsoleExpanded": [Function],
|
||||
@@ -2237,6 +2254,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isRightPanelV2Enabled": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isServerlessEnabled": [Function],
|
||||
"isSettingsV2Enabled": [Function],
|
||||
"isSparkEnabled": [Function],
|
||||
"isSparkEnabledForAccount": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
@@ -2293,6 +2311,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
"onToggleKeyDown": [Function],
|
||||
"parentFrameDataExplorerVersion": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -2318,6 +2337,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"titleLabel": "Select Columns",
|
||||
"visible": [Function],
|
||||
},
|
||||
"quotaId": [Function],
|
||||
"refreshDatabaseAccount": [Function],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
@@ -2688,6 +2708,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyVisible": [Function],
|
||||
"requestUnitsUsageCost": [Function],
|
||||
"ruToolTipText": [Function],
|
||||
"rupm": [Function],
|
||||
"rupmVisible": [Function],
|
||||
"sharedAutoPilotThroughput": [Function],
|
||||
"sharedThroughputRangeText": [Function],
|
||||
"shouldCreateMongoWildcardIndex": [Function],
|
||||
@@ -3175,6 +3197,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyVisible": [Function],
|
||||
"requestUnitsUsageCost": [Function],
|
||||
"ruToolTipText": [Function],
|
||||
"rupm": [Function],
|
||||
"rupmVisible": [Function],
|
||||
"sharedAutoPilotThroughput": [Function],
|
||||
"sharedThroughputRangeText": [Function],
|
||||
"shouldCreateMongoWildcardIndex": [Function],
|
||||
@@ -3286,6 +3310,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
"arcadiaToken": [Function],
|
||||
"armEndpoint": [Function],
|
||||
"browseQueriesPane": BrowseQueriesPane {
|
||||
"canSaveQueries": [Function],
|
||||
"container": [Circular],
|
||||
@@ -3497,6 +3522,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
@@ -3505,6 +3531,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isLeftPaneExpanded": [Function],
|
||||
"isLinkInjectionEnabled": [Function],
|
||||
"isMongoIndexEditorEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isNotificationConsoleExpanded": [Function],
|
||||
@@ -3521,6 +3548,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isRightPanelV2Enabled": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isServerlessEnabled": [Function],
|
||||
"isSettingsV2Enabled": [Function],
|
||||
"isSparkEnabled": [Function],
|
||||
"isSparkEnabledForAccount": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
@@ -3577,6 +3605,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
"onToggleKeyDown": [Function],
|
||||
"parentFrameDataExplorerVersion": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -3602,6 +3631,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"titleLabel": "Select Columns",
|
||||
"visible": [Function],
|
||||
},
|
||||
"quotaId": [Function],
|
||||
"refreshDatabaseAccount": [Function],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
@@ -3847,9 +3877,9 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"version": 2,
|
||||
},
|
||||
"partitionKeyProperty": "partitionKey",
|
||||
"quotaInfo": [Function],
|
||||
"readSettings": [Function],
|
||||
"uniqueKeyPolicy": Object {},
|
||||
"usageSizeInKB": [Function],
|
||||
}
|
||||
}
|
||||
container={
|
||||
@@ -3959,6 +3989,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyVisible": [Function],
|
||||
"requestUnitsUsageCost": [Function],
|
||||
"ruToolTipText": [Function],
|
||||
"rupm": [Function],
|
||||
"rupmVisible": [Function],
|
||||
"sharedAutoPilotThroughput": [Function],
|
||||
"sharedThroughputRangeText": [Function],
|
||||
"shouldCreateMongoWildcardIndex": [Function],
|
||||
@@ -4446,6 +4478,8 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"partitionKeyVisible": [Function],
|
||||
"requestUnitsUsageCost": [Function],
|
||||
"ruToolTipText": [Function],
|
||||
"rupm": [Function],
|
||||
"rupmVisible": [Function],
|
||||
"sharedAutoPilotThroughput": [Function],
|
||||
"sharedThroughputRangeText": [Function],
|
||||
"shouldCreateMongoWildcardIndex": [Function],
|
||||
@@ -4557,6 +4591,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"visible": [Function],
|
||||
},
|
||||
"arcadiaToken": [Function],
|
||||
"armEndpoint": [Function],
|
||||
"browseQueriesPane": BrowseQueriesPane {
|
||||
"canSaveQueries": [Function],
|
||||
"container": [Circular],
|
||||
@@ -4768,6 +4803,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"hasWriteAccess": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAuthWithResourceToken": [Function],
|
||||
"isCodeOfConductEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
@@ -4776,6 +4812,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isHostedDataExplorerEnabled": [Function],
|
||||
"isLeftPaneExpanded": [Function],
|
||||
"isLinkInjectionEnabled": [Function],
|
||||
"isMongoIndexEditorEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isNotificationConsoleExpanded": [Function],
|
||||
@@ -4792,6 +4829,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isRightPanelV2Enabled": [Function],
|
||||
"isSchemaEnabled": [Function],
|
||||
"isServerlessEnabled": [Function],
|
||||
"isSettingsV2Enabled": [Function],
|
||||
"isSparkEnabled": [Function],
|
||||
"isSparkEnabledForAccount": [Function],
|
||||
"isSynapseLinkUpdating": [Function],
|
||||
@@ -4848,6 +4886,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"onSwitchToConnectionString": [Function],
|
||||
"onToggleKeyDown": [Function],
|
||||
"parentFrameDataExplorerVersion": [Function],
|
||||
"provideFeedbackEmail": [Function],
|
||||
"queriesClient": QueriesClient {
|
||||
"container": [Circular],
|
||||
@@ -4873,6 +4912,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"titleLabel": "Select Columns",
|
||||
"visible": [Function],
|
||||
},
|
||||
"quotaId": [Function],
|
||||
"refreshDatabaseAccount": [Function],
|
||||
"refreshNotebookList": [Function],
|
||||
"refreshTreeTitle": [Function],
|
||||
|
||||
@@ -69,15 +69,15 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
|
||||
<b>
|
||||
¥
|
||||
1.02
|
||||
1.29
|
||||
hourly
|
||||
/
|
||||
¥
|
||||
24.48
|
||||
31.06
|
||||
daily
|
||||
/
|
||||
¥
|
||||
744.60
|
||||
944.60
|
||||
monthly
|
||||
|
||||
</b>
|
||||
@@ -227,7 +227,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
, Container:
|
||||
sampleCollection
|
||||
|
||||
, Current manual throughput: 1000 RU/s
|
||||
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
|
||||
</Text>
|
||||
<Text
|
||||
id="throughputApplyLongDelayMessage"
|
||||
|
||||
@@ -126,12 +126,6 @@
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: !isAutoPilotSelected()">
|
||||
<p>
|
||||
<span
|
||||
>Estimate your required throughput with
|
||||
<a target="_blank" href="https://cosmos.azure.com/capacitycalculator/">capacity calculator</a></span
|
||||
>
|
||||
</p>
|
||||
<div data-bind="setTemplateReady: true">
|
||||
<input
|
||||
data-bind="
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
jest.mock("../../Common/DocumentClientUtilityBase");
|
||||
jest.mock("../Graph/GraphExplorerComponent/GremlinClient");
|
||||
jest.mock("../../Common/dataAccess/createCollection");
|
||||
jest.mock("../../Common/dataAccess/createDocument");
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import Q from "q";
|
||||
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
|
||||
import { createDocument } from "../../Common/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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPa
|
||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
||||
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
|
||||
import EnvironmentUtility from "../Common/EnvironmentUtility";
|
||||
import GraphStylingPane from "./Panes/GraphStylingPane";
|
||||
import hasher from "hasher";
|
||||
import NewVertexPane from "./Panes/NewVertexPane";
|
||||
@@ -121,6 +121,7 @@ export default class Explorer {
|
||||
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
|
||||
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
|
||||
public subscriptionType: ko.Observable<SubscriptionType>;
|
||||
public quotaId: ko.Observable<string>;
|
||||
public defaultExperience: ko.Observable<string>;
|
||||
public isPreferredApiDocumentDB: ko.Computed<boolean>;
|
||||
public isPreferredApiCassandra: ko.Computed<boolean>;
|
||||
@@ -134,10 +135,12 @@ export default class Explorer {
|
||||
public canSaveQueries: ko.Computed<boolean>;
|
||||
public features: ko.Observable<any>;
|
||||
public serverId: ko.Observable<string>;
|
||||
public armEndpoint: ko.Observable<string>;
|
||||
public isTryCosmosDBSubscription: ko.Observable<boolean>;
|
||||
public queriesClient: QueriesClient;
|
||||
public tableDataClient: TableDataClient;
|
||||
public splitter: Splitter;
|
||||
public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>("");
|
||||
public mostRecentActivity: MostRecentActivity.MostRecentActivity;
|
||||
|
||||
// Notification Console
|
||||
@@ -201,7 +204,10 @@ export default class Explorer {
|
||||
|
||||
// features
|
||||
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||
public isCodeOfConductEnabled: ko.Computed<boolean>;
|
||||
public isLinkInjectionEnabled: ko.Computed<boolean>;
|
||||
public isSettingsV2Enabled: ko.Observable<boolean>;
|
||||
public isMongoIndexEditorEnabled: ko.Observable<boolean>;
|
||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
@@ -275,6 +281,7 @@ export default class Explorer {
|
||||
|
||||
this.databaseAccount = ko.observable<DataModels.DatabaseAccount>();
|
||||
this.subscriptionType = ko.observable<SubscriptionType>(SharedConstants.CollectionCreation.DefaultSubscriptionType);
|
||||
this.quotaId = ko.observable<string>("");
|
||||
let firstInitialization = true;
|
||||
this.isRefreshingExplorer = ko.observable<boolean>(true);
|
||||
this.isRefreshingExplorer.subscribe((isRefreshing: boolean) => {
|
||||
@@ -314,9 +321,9 @@ export default class Explorer {
|
||||
if (isAccountReady) {
|
||||
this.isAuthWithResourceToken() ? this.refreshDatabaseForResourceToken() : this.refreshAllDatabases(true);
|
||||
RouteHandler.getInstance().initHandler();
|
||||
this.notebookWorkspaceManager = new NotebookWorkspaceManager();
|
||||
this.notebookWorkspaceManager = new NotebookWorkspaceManager(this.armEndpoint());
|
||||
this.arcadiaWorkspaces = ko.observableArray();
|
||||
this._arcadiaManager = new ArcadiaResourceManager();
|
||||
this._arcadiaManager = new ArcadiaResourceManager(this.armEndpoint());
|
||||
this._isAfecFeatureRegistered(Constants.AfecFeatures.StorageAnalytics).then(isRegistered =>
|
||||
this.hasStorageAnalyticsAfecFeature(isRegistered)
|
||||
);
|
||||
@@ -366,6 +373,7 @@ export default class Explorer {
|
||||
|
||||
this.features = ko.observable();
|
||||
this.serverId = ko.observable<string>();
|
||||
this.armEndpoint = ko.observable<string>(undefined);
|
||||
this.queriesClient = new QueriesClient(this);
|
||||
this.isTryCosmosDBSubscription = ko.observable<boolean>(false);
|
||||
|
||||
@@ -398,9 +406,14 @@ export default class Explorer {
|
||||
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
||||
);
|
||||
this.isCodeOfConductEnabled = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.enableCodeOfConduct)
|
||||
);
|
||||
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
||||
);
|
||||
this.isSettingsV2Enabled = ko.observable(false);
|
||||
this.isMongoIndexEditorEnabled = ko.observable(false);
|
||||
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
||||
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||
@@ -1007,7 +1020,9 @@ export default class Explorer {
|
||||
this.isSynapseLinkUpdating(true);
|
||||
this._closeSynapseLinkModalDialog();
|
||||
|
||||
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(this.databaseAccount().id);
|
||||
const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(
|
||||
this.databaseAccount().id
|
||||
);
|
||||
|
||||
try {
|
||||
const databaseAccount: DataModels.DatabaseAccount = await resourceProviderClient.patchAsync(
|
||||
@@ -1719,7 +1734,6 @@ export default class Explorer {
|
||||
case MessageTypes.SendNotification:
|
||||
case MessageTypes.ClearNotification:
|
||||
case MessageTypes.LoadingStatus:
|
||||
case MessageTypes.InitTestExplorer:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1747,59 +1761,61 @@ export default class Explorer {
|
||||
inputs.extensionEndpoint = configContext.PROXY_PATH;
|
||||
}
|
||||
|
||||
this.initDataExplorerWithFrameInputs(inputs);
|
||||
const initPromise: Q.Promise<void> = inputs ? this.initDataExplorerWithFrameInputs(inputs) : Q();
|
||||
|
||||
const openAction: ActionContracts.DataExplorerAction = message.openAction;
|
||||
if (!!openAction) {
|
||||
if (this.isRefreshingExplorer()) {
|
||||
const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => {
|
||||
initPromise.then(() => {
|
||||
const openAction: ActionContracts.DataExplorerAction = message.openAction;
|
||||
if (!!openAction) {
|
||||
if (this.isRefreshingExplorer()) {
|
||||
const subscription = this.databases.subscribe((databases: ViewModels.Database[]) => {
|
||||
handleOpenAction(openAction, this.nonSystemDatabases(), this);
|
||||
subscription.dispose();
|
||||
});
|
||||
} else {
|
||||
handleOpenAction(openAction, this.nonSystemDatabases(), this);
|
||||
subscription.dispose();
|
||||
});
|
||||
} else {
|
||||
handleOpenAction(openAction, this.nonSystemDatabases(), this);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
|
||||
handleCachedDataMessage(message);
|
||||
return;
|
||||
}
|
||||
if (message.type) {
|
||||
switch (message.type) {
|
||||
case MessageTypes.UpdateLocationHash:
|
||||
if (!message.locationHash) {
|
||||
break;
|
||||
}
|
||||
hasher.replaceHash(message.locationHash);
|
||||
RouteHandler.getInstance().parseHash(message.locationHash);
|
||||
break;
|
||||
case MessageTypes.SendNotification:
|
||||
if (!message.message) {
|
||||
break;
|
||||
}
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
message.consoleDataType || ConsoleDataType.Info,
|
||||
message.message,
|
||||
message.id
|
||||
);
|
||||
break;
|
||||
case MessageTypes.ClearNotification:
|
||||
if (!message.id) {
|
||||
break;
|
||||
}
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(message.id);
|
||||
break;
|
||||
case MessageTypes.LoadingStatus:
|
||||
if (!message.text) {
|
||||
break;
|
||||
}
|
||||
this._setLoadingStatusText(message.text, message.title);
|
||||
break;
|
||||
if (message.actionType === ActionContracts.ActionType.TransmitCachedData) {
|
||||
handleCachedDataMessage(message);
|
||||
return;
|
||||
}
|
||||
if (message.type) {
|
||||
switch (message.type) {
|
||||
case MessageTypes.UpdateLocationHash:
|
||||
if (!message.locationHash) {
|
||||
break;
|
||||
}
|
||||
hasher.replaceHash(message.locationHash);
|
||||
RouteHandler.getInstance().parseHash(message.locationHash);
|
||||
break;
|
||||
case MessageTypes.SendNotification:
|
||||
if (!message.message) {
|
||||
break;
|
||||
}
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
message.consoleDataType || ConsoleDataType.Info,
|
||||
message.message,
|
||||
message.id
|
||||
);
|
||||
break;
|
||||
case MessageTypes.ClearNotification:
|
||||
if (!message.id) {
|
||||
break;
|
||||
}
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(message.id);
|
||||
break;
|
||||
case MessageTypes.LoadingStatus:
|
||||
if (!message.text) {
|
||||
break;
|
||||
}
|
||||
this._setLoadingStatusText(message.text, message.title);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.splashScreenAdapter.forceRender();
|
||||
this.splashScreenAdapter.forceRender();
|
||||
});
|
||||
}
|
||||
|
||||
public findSelectedDatabase(): ViewModels.Database {
|
||||
@@ -1839,14 +1855,8 @@ export default class Explorer {
|
||||
return false;
|
||||
}
|
||||
|
||||
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): void {
|
||||
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): Q.Promise<void> {
|
||||
if (inputs != null) {
|
||||
// In development mode, save the iframe message from the portal in session storage.
|
||||
// This allows webpack hot reload to funciton properly
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs));
|
||||
}
|
||||
|
||||
const authorizationToken = inputs.authorizationToken || "";
|
||||
const masterKey = inputs.masterKey || "";
|
||||
const databaseAccount = inputs.databaseAccount || null;
|
||||
@@ -1855,18 +1865,25 @@ export default class Explorer {
|
||||
}
|
||||
this.features(inputs.features);
|
||||
this.serverId(inputs.serverId);
|
||||
this.armEndpoint(EnvironmentUtility.normalizeArmEndpointUri(inputs.csmEndpoint || configContext.ARM_ENDPOINT));
|
||||
this.databaseAccount(databaseAccount);
|
||||
this.subscriptionType(inputs.subscriptionType);
|
||||
this.quotaId(inputs.quotaId);
|
||||
this.hasWriteAccess(inputs.hasWriteAccess);
|
||||
this.flight(inputs.addCollectionDefaultFlight);
|
||||
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription);
|
||||
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken);
|
||||
this.setFeatureFlagsFromFlights(inputs.flights);
|
||||
|
||||
if (!!inputs.dataExplorerVersion) {
|
||||
this.parentFrameDataExplorerVersion(inputs.dataExplorerVersion);
|
||||
}
|
||||
|
||||
this._importExplorerConfigComplete = true;
|
||||
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: inputs.extensionEndpoint || "",
|
||||
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT)
|
||||
ARM_ENDPOINT: this.armEndpoint()
|
||||
});
|
||||
|
||||
updateUserContext({
|
||||
@@ -1875,8 +1892,7 @@ export default class Explorer {
|
||||
databaseAccount,
|
||||
resourceGroup: inputs.resourceGroup,
|
||||
subscriptionId: inputs.subscriptionId,
|
||||
subscriptionType: inputs.subscriptionType,
|
||||
quotaId: inputs.quotaId
|
||||
subscriptionType: inputs.subscriptionType
|
||||
});
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.LoadDatabaseAccount,
|
||||
@@ -1890,12 +1906,21 @@ export default class Explorer {
|
||||
|
||||
this.isAccountReady(true);
|
||||
}
|
||||
return Q();
|
||||
}
|
||||
|
||||
public setFeatureFlagsFromFlights(flights: readonly string[]): void {
|
||||
if (!flights) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (flights.indexOf(Constants.Flights.SettingsV2) !== -1) {
|
||||
this.isSettingsV2Enabled(true);
|
||||
}
|
||||
|
||||
if (flights.indexOf(Constants.Flights.MongoIndexEditor) !== -1) {
|
||||
this.isMongoIndexEditorEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
public findSelectedCollection(): ViewModels.Collection {
|
||||
@@ -2262,6 +2287,7 @@ export default class Explorer {
|
||||
name,
|
||||
content,
|
||||
parentDomElement,
|
||||
this.isCodeOfConductEnabled(),
|
||||
this.isLinkInjectionEnabled()
|
||||
);
|
||||
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
||||
@@ -2353,13 +2379,11 @@ export default class Explorer {
|
||||
this.tabsManager.activateTab(notebookTab);
|
||||
} else {
|
||||
const options: NotebookTabOptions = {
|
||||
account: userContext.databaseAccount,
|
||||
tabKind: ViewModels.CollectionTabKind.NotebookV2,
|
||||
node: null,
|
||||
title: notebookContentItem.name,
|
||||
tabPath: notebookContentItem.path,
|
||||
collection: null,
|
||||
masterKey: userContext.masterKey || "",
|
||||
hashLocation: "notebooks",
|
||||
isActive: ko.observable(false),
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
@@ -2552,7 +2576,7 @@ export default class Explorer {
|
||||
|
||||
public _refreshSparkEnabledStateForAccount = async (): Promise<void> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const armEndpoint = configContext.ARM_ENDPOINT;
|
||||
const armEndpoint = this.armEndpoint();
|
||||
const authType = window.authType as AuthType;
|
||||
if (!subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
|
||||
// explorer is not aware of the database account yet
|
||||
@@ -2561,7 +2585,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${Constants.AfecFeatures.Spark}`;
|
||||
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri);
|
||||
const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(featureUri);
|
||||
try {
|
||||
const sparkNotebooksFeature: DataModels.AfecFeature = await resourceProviderClient.getAsync(
|
||||
featureUri,
|
||||
@@ -2581,7 +2605,7 @@ export default class Explorer {
|
||||
|
||||
public _isAfecFeatureRegistered = async (featureName: string): Promise<boolean> => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const armEndpoint = configContext.ARM_ENDPOINT;
|
||||
const armEndpoint = this.armEndpoint();
|
||||
const authType = window.authType as AuthType;
|
||||
if (!featureName || !subscriptionId || !armEndpoint || authType === AuthType.EncryptedToken) {
|
||||
// explorer is not aware of the database account yet
|
||||
@@ -2589,7 +2613,7 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
const featureUri = `subscriptions/${subscriptionId}/providers/Microsoft.Features/providers/Microsoft.DocumentDb/features/${featureName}`;
|
||||
const resourceProviderClient = new ResourceProviderClientFactory().getOrCreate(featureUri);
|
||||
const resourceProviderClient = new ResourceProviderClientFactory(this.armEndpoint()).getOrCreate(featureUri);
|
||||
try {
|
||||
const featureStatus: DataModels.AfecFeature = await resourceProviderClient.getAsync(
|
||||
featureUri,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -99,22 +99,7 @@
|
||||
.notificationConsoleControls {
|
||||
padding: @MediumSpace;
|
||||
margin-left:@DefaultSpace;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.ms-Dropdown-container {
|
||||
display: flex;
|
||||
.ms-Dropdown-title {
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
}
|
||||
.ms-Dropdown {
|
||||
min-width: 110px;
|
||||
margin-left: 10px;
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
}
|
||||
}
|
||||
#consoleFilterLabel {
|
||||
padding: 4px;
|
||||
}
|
||||
@@ -122,7 +107,6 @@
|
||||
.consoleSplitter {
|
||||
border-left: 1px solid @BaseMedium;
|
||||
margin: @MediumSpace;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.clearNotificationsButton {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import * as React from "react";
|
||||
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
|
||||
import AnimateHeight from "react-animate-height";
|
||||
import { Dropdown, IDropdownOption } from "office-ui-fabric-react";
|
||||
|
||||
import LoadingIcon from "../../../../images/loading.svg";
|
||||
import ErrorBlackIcon from "../../../../images/error_black.svg";
|
||||
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
|
||||
@@ -53,12 +53,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
NotificationConsoleComponentState
|
||||
> {
|
||||
private static readonly transitionDurationMs = 200;
|
||||
private static readonly FilterOptions = [
|
||||
{ key: "All", text: "All" },
|
||||
{ key: "In Progress", text: "In progress" },
|
||||
{ key: "Info", text: "Info" },
|
||||
{ key: "Error", text: "Error" }
|
||||
];
|
||||
private static readonly FilterOptions = ["All", "In Progress", "Info", "Error"];
|
||||
private headerTimeoutId: number;
|
||||
private prevHeaderStatus: string;
|
||||
private consoleHeaderElement: HTMLElement;
|
||||
@@ -67,7 +62,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
super(props);
|
||||
this.state = {
|
||||
headerStatus: "",
|
||||
selectedFilter: NotificationConsoleComponent.FilterOptions[0].key || "",
|
||||
selectedFilter: NotificationConsoleComponent.FilterOptions[0],
|
||||
isExpanded: props.isConsoleExpanded
|
||||
};
|
||||
this.prevHeaderStatus = null;
|
||||
@@ -155,15 +150,20 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
>
|
||||
<div className="notificationConsoleContents">
|
||||
<div className="notificationConsoleControls">
|
||||
<Dropdown
|
||||
label="Filter:"
|
||||
role="combobox"
|
||||
selectedKey={this.state.selectedFilter}
|
||||
options={NotificationConsoleComponent.FilterOptions}
|
||||
onChange={this.onFilterSelected.bind(this)}
|
||||
<label id="consoleFilterLabel">Filter</label>
|
||||
<select
|
||||
aria-labelledby="consoleFilterLabel"
|
||||
role="combobox"
|
||||
aria-label={this.state.selectedFilter}
|
||||
/>
|
||||
value={this.state.selectedFilter}
|
||||
onChange={this.onFilterSelected.bind(this)}
|
||||
>
|
||||
{NotificationConsoleComponent.FilterOptions.map((value: string) => (
|
||||
<option value={value} key={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="consoleSplitter" />
|
||||
<span
|
||||
className="clearNotificationsButton"
|
||||
@@ -220,8 +220,8 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
));
|
||||
}
|
||||
|
||||
private onFilterSelected(event: React.ChangeEvent<HTMLSelectElement>, option: IDropdownOption): void {
|
||||
this.setState({ selectedFilter: String(option.key) });
|
||||
private onFilterSelected(event: React.ChangeEvent<HTMLSelectElement>): void {
|
||||
this.setState({ selectedFilter: event.target.value });
|
||||
}
|
||||
|
||||
private getFilteredConsoleData(): ConsoleData[] {
|
||||
|
||||
@@ -110,34 +110,43 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
|
||||
<div
|
||||
className="notificationConsoleControls"
|
||||
>
|
||||
<StyledWithResponsiveMode
|
||||
<label
|
||||
id="consoleFilterLabel"
|
||||
>
|
||||
Filter
|
||||
</label>
|
||||
<select
|
||||
aria-label="All"
|
||||
aria-labelledby="consoleFilterLabel"
|
||||
label="Filter:"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "All",
|
||||
"text": "All",
|
||||
},
|
||||
Object {
|
||||
"key": "In Progress",
|
||||
"text": "In progress",
|
||||
},
|
||||
Object {
|
||||
"key": "Info",
|
||||
"text": "Info",
|
||||
},
|
||||
Object {
|
||||
"key": "Error",
|
||||
"text": "Error",
|
||||
},
|
||||
]
|
||||
}
|
||||
role="combobox"
|
||||
selectedKey="All"
|
||||
/>
|
||||
value="All"
|
||||
>
|
||||
<option
|
||||
key="All"
|
||||
value="All"
|
||||
>
|
||||
All
|
||||
</option>
|
||||
<option
|
||||
key="In Progress"
|
||||
value="In Progress"
|
||||
>
|
||||
In Progress
|
||||
</option>
|
||||
<option
|
||||
key="Info"
|
||||
value="Info"
|
||||
>
|
||||
Info
|
||||
</option>
|
||||
<option
|
||||
key="Error"
|
||||
value="Error"
|
||||
>
|
||||
Error
|
||||
</option>
|
||||
</select>
|
||||
<span
|
||||
className="consoleSplitter"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
.mongoQueryComponent {
|
||||
margin-left: 10px;
|
||||
|
||||
input {
|
||||
margin-top: 0;
|
||||
}
|
||||
label {
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
label:before {
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.queryInput {
|
||||
border: 1px solid black;
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import * as React from "react";
|
||||
import { Dispatch } from "redux";
|
||||
import MonacoEditor from "@nteract/monaco-editor";
|
||||
import { PrimaryButton } from "office-ui-fabric-react";
|
||||
import { ChoiceGroup, IChoiceGroupOption } from "office-ui-fabric-react/lib/ChoiceGroup";
|
||||
import Outputs from "@nteract/stateful-components/lib/outputs";
|
||||
import { KernelOutputError, StreamText } from "@nteract/outputs";
|
||||
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
|
||||
import { actions, selectors, AppState, ContentRef, KernelRef } from "@nteract/core";
|
||||
import loadTransform from "../NotebookComponent/loadTransform";
|
||||
import { connect } from "react-redux";
|
||||
import Immutable from "immutable";
|
||||
|
||||
import "./MongoQueryComponent.less";
|
||||
interface MongoQueryComponentPureProps {
|
||||
contentRef: ContentRef;
|
||||
kernelRef: KernelRef;
|
||||
databaseId: string;
|
||||
collectionId: string;
|
||||
}
|
||||
|
||||
interface MongoQueryComponentDispatchProps {
|
||||
runCell: (contentRef: ContentRef, cellId: string) => void;
|
||||
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void;
|
||||
onChange: (text: string, id: string, contentRef: ContentRef) => void;
|
||||
save: (contentRef: ContentRef) => void;
|
||||
}
|
||||
|
||||
type OutputType = "rich" | "json";
|
||||
|
||||
interface MongoQueryComponentState {
|
||||
outputType: OutputType;
|
||||
selectedId: string;
|
||||
}
|
||||
|
||||
const options: IChoiceGroupOption[] = [
|
||||
{ key: "rich", text: "Rich Output" },
|
||||
{ key: "json", text: "Json Output" }
|
||||
];
|
||||
|
||||
interface MongoKernelJsonOutput {
|
||||
results: any;
|
||||
}
|
||||
|
||||
interface MongoDocument {
|
||||
id: string;
|
||||
}
|
||||
|
||||
type MongoQueryComponentProps = MongoQueryComponentPureProps & StateProps & MongoQueryComponentDispatchProps;
|
||||
export class MongoQueryComponent extends React.Component<MongoQueryComponentProps, MongoQueryComponentState> {
|
||||
constructor(props: MongoQueryComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
outputType: "json",
|
||||
selectedId: undefined
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
loadTransform(this.props);
|
||||
}
|
||||
|
||||
private onExecute = () => {
|
||||
this.props.runCell(this.props.contentRef, this.props.firstCellId);
|
||||
this.props.save(this.props.contentRef);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param databaseId
|
||||
* @param collectionId
|
||||
* @param query e.g. { "lastName": { $in: ["Andersen"] } }
|
||||
*/
|
||||
private createFilterQuery(databaseId: string, collectionId: string, query: string): string {
|
||||
const newCommand = `{ "command": "filter", "database": "${databaseId}", "collection": "${collectionId}", "filter": ${JSON.stringify(query)}, "outputType": "${this.state.outputType}" }`;
|
||||
return newCommand;
|
||||
}
|
||||
|
||||
private onOutputTypeChange = (e: React.FormEvent<HTMLElement | HTMLInputElement>, option: IChoiceGroupOption): void => {
|
||||
const outputType = option.key as OutputType;
|
||||
this.setState({ outputType }, () => this.onInputChange(this.props.inputValue));
|
||||
};
|
||||
|
||||
private onInputChange = (text: string) => {
|
||||
this.props.onChange(this.createFilterQuery(this.props.databaseId, this.props.collectionId, text),
|
||||
this.props.firstCellId, this.props.contentRef);
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const { firstCellId: id, contentRef, outputDocuments } = this.props;
|
||||
|
||||
if (!id) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mongoQueryComponent">
|
||||
<div className="queryInput">
|
||||
<MonacoEditor id={this.props.firstCellId} contentRef={this.props.contentRef} theme={""}
|
||||
language="json" onChange={this.onInputChange}
|
||||
value={this.props.inputValue} />
|
||||
</div>
|
||||
<PrimaryButton text="Apply" onClick={this.onExecute} disabled={!this.props.firstCellId} />
|
||||
<ChoiceGroup
|
||||
selectedKey={this.state.outputType}
|
||||
options={options}
|
||||
onChange={this.onOutputTypeChange}
|
||||
label="Output Type"
|
||||
styles={{ input: { marginTop: 0 }, root: { marginTop: 0 } }}
|
||||
/>
|
||||
<hr />
|
||||
<div style={ { display: "flex" } }>
|
||||
<ul>
|
||||
{outputDocuments && outputDocuments.map(d => (
|
||||
<li key={d.id}>
|
||||
<a onClick={() => this.setState({ selectedId: id })}>{d.id}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div style={{ width: "100%" }} >
|
||||
<MonacoEditor id={""} contentRef={""} theme={""} language="json" onChange={() => {}}
|
||||
value={JSON.stringify(outputDocuments.find(doc => doc.id ===this.state.selectedId)) ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<Outputs id={id} contentRef={contentRef}>
|
||||
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
|
||||
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
|
||||
<KernelOutputError />
|
||||
<StreamText />
|
||||
</Outputs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
firstCellId: string;
|
||||
inputValue: string;
|
||||
outputDocuments: MongoDocument[];
|
||||
}
|
||||
interface InitialProps {
|
||||
contentRef: string;
|
||||
}
|
||||
|
||||
// Redux
|
||||
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
|
||||
const { contentRef } = initialProps;
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
let firstCellId;
|
||||
let inputValue = "";
|
||||
let outputDocuments = [];
|
||||
const content = selectors.content(state, { contentRef });
|
||||
if (content?.type === "notebook") {
|
||||
const cellOrder = selectors.notebook.cellOrder(content.model);
|
||||
if (cellOrder.size > 0) {
|
||||
firstCellId = cellOrder.first() as string;
|
||||
const cell = selectors.notebook.cellById(content.model, { id: firstCellId });
|
||||
|
||||
// Parse to extract filter and output type
|
||||
const cellValue = cell.get("source", "");
|
||||
if (cellValue) {
|
||||
try {
|
||||
const filterValue = JSON.parse(cellValue).filter;
|
||||
if (filterValue) {
|
||||
inputValue = filterValue;
|
||||
}
|
||||
} catch(e) {
|
||||
console.error("Could not parse", e);
|
||||
}
|
||||
}
|
||||
const outputs = cell.get("outputs", Immutable.List());
|
||||
// Extract "application/json" mime-type
|
||||
let jsonOutput: MongoKernelJsonOutput;
|
||||
for (const output of outputs) {
|
||||
if (Object.prototype.hasOwnProperty.call(output.data, "application/json")) {
|
||||
jsonOutput = output.data["application/json"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
outputDocuments = jsonOutput?.results ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
firstCellId,
|
||||
inputValue,
|
||||
outputDocuments
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: MongoQueryComponentProps) => {
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => {
|
||||
return {
|
||||
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
|
||||
return dispatch(
|
||||
actions.addTransform({
|
||||
mediaType: transform.MIMETYPE,
|
||||
component: transform
|
||||
})
|
||||
);
|
||||
},
|
||||
runCell: (contentRef: ContentRef, cellId: string) => {
|
||||
return dispatch(
|
||||
actions.executeCell({
|
||||
contentRef,
|
||||
id: cellId
|
||||
})
|
||||
);
|
||||
},
|
||||
onChange: (text: string, id: string, contentRef: ContentRef) => {
|
||||
dispatch(actions.updateCellSource({ id, contentRef, value: text }));
|
||||
},
|
||||
save: (contentRef: ContentRef) => {
|
||||
dispatch(actions.save({ contentRef }));
|
||||
}
|
||||
};
|
||||
};
|
||||
return mapDispatchToProps;
|
||||
};
|
||||
|
||||
export default connect(makeMapStateToProps, makeMapDispatchToProps)(MongoQueryComponent);
|
||||
@@ -0,0 +1,49 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import {
|
||||
NotebookComponentBootstrapper,
|
||||
NotebookComponentBootstrapperOptions
|
||||
} from "../NotebookComponent/NotebookComponentBootstrapper";
|
||||
import MongoQueryComponent from "../MongoQueryComponent/MongoQueryComponent";
|
||||
import { actions, createContentRef, createKernelRef, KernelRef } from "@nteract/core";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
export class MongoQueryComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
|
||||
public parameters: unknown;
|
||||
private kernelRef: KernelRef;
|
||||
|
||||
constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) {
|
||||
super(options);
|
||||
|
||||
if (!this.contentRef) {
|
||||
this.contentRef = createContentRef();
|
||||
this.kernelRef = createKernelRef();
|
||||
|
||||
// Request fetching notebook content
|
||||
this.getStore().dispatch(
|
||||
actions.fetchContent({
|
||||
filepath: "mongo.ipynb",
|
||||
params: {},
|
||||
kernelRef: this.kernelRef,
|
||||
contentRef: this.contentRef
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
const props = {
|
||||
contentRef: this.contentRef,
|
||||
kernelRef: this.kernelRef,
|
||||
databaseId: this.databaseId,
|
||||
collectionId: this.collectionId
|
||||
};
|
||||
|
||||
return (
|
||||
<Provider store={this.getStore()}>
|
||||
<MongoQueryComponent {...props} />;
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -128,9 +128,17 @@ export default class NotebookManager {
|
||||
name: string,
|
||||
content: string | ImmutableNotebook,
|
||||
parentDomElement: HTMLElement,
|
||||
isCodeOfConductEnabled: boolean,
|
||||
isLinkInjectionEnabled: boolean
|
||||
): Promise<void> {
|
||||
await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement, isLinkInjectionEnabled);
|
||||
await this.publishNotebookPaneAdapter.open(
|
||||
name,
|
||||
getFullName(),
|
||||
content,
|
||||
parentDomElement,
|
||||
isCodeOfConductEnabled,
|
||||
isLinkInjectionEnabled
|
||||
);
|
||||
}
|
||||
|
||||
public openCopyNotebookPane(name: string, content: string): void {
|
||||
|
||||
@@ -243,6 +243,38 @@
|
||||
</div>
|
||||
<!-- Unlimited Button Content - Start -->
|
||||
<div class="tabcontent" data-bind="visible: isUnlimitedStorageSelected() || databaseHasSharedOffer()">
|
||||
<div data-bind="visible: rupmVisible">
|
||||
<div class="tabs">
|
||||
<p>
|
||||
<span class="mandatoryStar">*</span>
|
||||
<span class="addCollectionLabel">RU/m</span>
|
||||
<span class="infoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information">
|
||||
<span class="tooltiptext throughputRuInfo">
|
||||
For each 100 Request Units per second (RU/s) provisioned, 1,000 Request Units
|
||||
per
|
||||
minute
|
||||
(RU/m) can be provisioned. E.g.: for a container with 5,000 RU/s provisioned
|
||||
with
|
||||
RU/m
|
||||
enabled, the RU/m budget will be 50,000 RU/m.
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
<div tabindex="0" data-bind="event: { keydown: onRupmOptionsKeyDown }" aria-label="RU/m">
|
||||
<div class="tab">
|
||||
<input type="radio" id="rupmOn2" name="rupmcoll2" value="on" class="radio"
|
||||
data-bind="checked: rupm">
|
||||
<label for="rupmOn2">ON</label>
|
||||
</div>
|
||||
<div class="tab">
|
||||
<input type="radio" id="rupmOff2" name="rupmcoll2" value="off" class="radio"
|
||||
data-bind="checked: rupm">
|
||||
<label for="rupmOff2">OFF</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: partitionKeyVisible">
|
||||
<p>
|
||||
<span class="mandatoryStar">*</span>
|
||||
|
||||
@@ -16,7 +16,6 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
|
||||
import { createCollection } from "../../Common/dataAccess/createCollection";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
|
||||
isPreferredApiTable: ko.Computed<boolean>;
|
||||
@@ -43,6 +42,8 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
public partitionKeyVisible: ko.Computed<boolean>;
|
||||
public partitionKeyPattern: ko.Computed<string>;
|
||||
public partitionKeyTitle: ko.Computed<string>;
|
||||
public rupm: ko.Observable<string>;
|
||||
public rupmVisible: ko.Observable<boolean>;
|
||||
public storage: ko.Observable<string>;
|
||||
public throughputSinglePartition: ViewModels.Editable<number>;
|
||||
public throughputMultiPartition: ViewModels.Editable<number>;
|
||||
@@ -142,6 +143,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
}
|
||||
return "";
|
||||
});
|
||||
this.rupm = ko.observable<string>(Constants.RUPMStates.off);
|
||||
this.rupmVisible = ko.observable<boolean>(false);
|
||||
const featureSubcription = this.container.features.subscribe(() => {
|
||||
this.rupmVisible(this.container.isFeatureEnabled(Constants.Features.enableRupm));
|
||||
featureSubcription.dispose();
|
||||
});
|
||||
|
||||
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
|
||||
|
||||
@@ -194,6 +201,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
account.properties.readLocations.length) ||
|
||||
1;
|
||||
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
|
||||
const rupmEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
|
||||
|
||||
let throughputSpendAckText: string;
|
||||
let estimatedSpend: string;
|
||||
@@ -203,15 +211,23 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
rupmEnabled,
|
||||
this.isSharedAutoPilotSelected()
|
||||
);
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster);
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
|
||||
offerThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
rupmEnabled
|
||||
);
|
||||
} else {
|
||||
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||
this.sharedAutoPilotThroughput(),
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
rupmEnabled,
|
||||
this.isSharedAutoPilotSelected()
|
||||
);
|
||||
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
|
||||
@@ -248,6 +264,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
account.properties.readLocations.length) ||
|
||||
1;
|
||||
const multimaster = (account && account.properties && account.properties.enableMultipleWriteLocations) || false;
|
||||
const rupmEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
|
||||
|
||||
let throughputSpendAckText: string;
|
||||
let estimatedSpend: string;
|
||||
@@ -257,13 +274,15 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
rupmEnabled,
|
||||
this.isAutoPilotSelected()
|
||||
);
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
|
||||
this.throughputMultiPartition(),
|
||||
serverId,
|
||||
regions,
|
||||
multimaster
|
||||
multimaster,
|
||||
rupmEnabled
|
||||
);
|
||||
} else {
|
||||
throughputSpendAckText = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||
@@ -271,6 +290,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
rupmEnabled,
|
||||
this.isAutoPilotSelected()
|
||||
);
|
||||
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
|
||||
@@ -666,10 +686,11 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
storage: this.storage(),
|
||||
offerThroughput: this._getThroughput(),
|
||||
partitionKey: this.partitionKey(),
|
||||
databaseId: this.databaseId()
|
||||
databaseId: this.databaseId(),
|
||||
rupm: this.rupm()
|
||||
}),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
defaultsCheck: {
|
||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||
throughput: this._getThroughput(),
|
||||
@@ -767,11 +788,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
id: this.collectionId(),
|
||||
storage: this.storage(),
|
||||
partitionKey,
|
||||
rupm: this.rupm(),
|
||||
uniqueKeyPolicy,
|
||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||
}),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
defaultsCheck: {
|
||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||
throughput: offerThroughput,
|
||||
@@ -841,11 +863,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
id: this.collectionId(),
|
||||
storage: this.storage(),
|
||||
partitionKey,
|
||||
rupm: this.rupm(),
|
||||
uniqueKeyPolicy,
|
||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||
}),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
defaultsCheck: {
|
||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||
throughput: offerThroughput,
|
||||
@@ -875,11 +898,12 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
id: this.collectionId(),
|
||||
storage: this.storage(),
|
||||
partitionKey,
|
||||
rupm: this.rupm(),
|
||||
uniqueKeyPolicy,
|
||||
collectionWithThroughputInShared: this.collectionWithThroughputInShared()
|
||||
},
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
defaultsCheck: {
|
||||
storage: this.storage() === Constants.BackendDefaults.singlePartitionStorageInGb ? "f" : "u",
|
||||
throughput: offerThroughput,
|
||||
@@ -957,6 +981,20 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
return true;
|
||||
}
|
||||
|
||||
public onRupmOptionsKeyDown(source: any, event: KeyboardEvent): boolean {
|
||||
if (event.key === "ArrowRight") {
|
||||
this.rupm("off");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowLeft") {
|
||||
this.rupm("on");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public onEnableSynapseLinkButtonClicked() {
|
||||
this.container.openEnableSynapseLinkDialog();
|
||||
}
|
||||
@@ -980,6 +1018,16 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
}
|
||||
|
||||
const throughput = this._getThroughput();
|
||||
const maxThroughputWithRUPM =
|
||||
SharedConstants.CollectionCreation.MaxRUPMPerPartition * this._calculateNumberOfPartitions();
|
||||
|
||||
if (this.rupm() === Constants.RUPMStates.on && throughput > maxThroughputWithRUPM) {
|
||||
this.formErrors(
|
||||
`The maximum supported provisioned throughput with RU/m enabled is ${maxThroughputWithRUPM} RU/s. Please turn off RU/m to incease thoughput above ${maxThroughputWithRUPM} RU/s.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !this.throughputSpendAck()) {
|
||||
this.formErrors(`Please acknowledge the estimated daily spend.`);
|
||||
return false;
|
||||
|
||||
@@ -13,7 +13,6 @@ import { createDatabase } from "../../Common/dataAccess/createDatabase";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export default class AddDatabasePane extends ContextualPaneBase {
|
||||
public defaultExperience: ko.Computed<string>;
|
||||
@@ -134,12 +133,19 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
let estimatedSpendAcknowledge: string;
|
||||
let estimatedSpend: string;
|
||||
if (!this.isAutoPilotSelected()) {
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster);
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
|
||||
offerThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/
|
||||
);
|
||||
estimatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||
offerThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/,
|
||||
this.isAutoPilotSelected()
|
||||
);
|
||||
} else {
|
||||
@@ -154,6 +160,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/,
|
||||
this.isAutoPilotSelected()
|
||||
);
|
||||
}
|
||||
@@ -251,7 +258,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
defaultsCheck: {
|
||||
throughput: this.throughput(),
|
||||
flight: this.container.flight()
|
||||
@@ -279,7 +286,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
}),
|
||||
offerThroughput,
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
defaultsCheck: {
|
||||
flight: this.container.flight()
|
||||
},
|
||||
@@ -343,7 +350,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
}),
|
||||
offerThroughput: offerThroughput,
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
defaultsCheck: {
|
||||
flight: this.container.flight()
|
||||
},
|
||||
@@ -367,7 +374,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
}),
|
||||
offerThroughput: offerThroughput,
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
defaultsCheck: {
|
||||
flight: this.container.flight()
|
||||
},
|
||||
|
||||
@@ -15,7 +15,6 @@ import { HashMap } from "../../Common/HashMap";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { SubscriptionType } from "../../Contracts/SubscriptionType";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
public createTableQuery: ko.Observable<string>;
|
||||
@@ -139,12 +138,19 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
let estimatedSpend: string;
|
||||
let estimatedDedicatedSpendAcknowledge: string;
|
||||
if (!this.isAutoPilotSelected()) {
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(offerThroughput, serverId, regions, multimaster);
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
|
||||
offerThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/
|
||||
);
|
||||
estimatedDedicatedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||
offerThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/,
|
||||
this.isAutoPilotSelected()
|
||||
);
|
||||
} else {
|
||||
@@ -159,6 +165,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/,
|
||||
this.isAutoPilotSelected()
|
||||
);
|
||||
}
|
||||
@@ -183,12 +190,19 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
let estimatedSpend: string;
|
||||
let estimatedSharedSpendAcknowledge: string;
|
||||
if (!this.isSharedAutoPilotSelected()) {
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(this.keyspaceThroughput(), serverId, regions, multimaster);
|
||||
estimatedSpend = PricingUtils.getEstimatedSpendHtml(
|
||||
this.keyspaceThroughput(),
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/
|
||||
);
|
||||
estimatedSharedSpendAcknowledge = PricingUtils.getEstimatedSpendAcknowledgeString(
|
||||
this.keyspaceThroughput(),
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/,
|
||||
this.isSharedAutoPilotSelected()
|
||||
);
|
||||
} else {
|
||||
@@ -203,6 +217,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false /*rupmEnabled*/,
|
||||
this.isSharedAutoPilotSelected()
|
||||
);
|
||||
}
|
||||
@@ -297,10 +312,11 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
storage: Constants.BackendDefaults.multiPartitionStorageInGb,
|
||||
offerThroughput: this.throughput(),
|
||||
partitionKey: "",
|
||||
databaseId: this.keyspaceId()
|
||||
databaseId: this.keyspaceId(),
|
||||
rupm: false
|
||||
}),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
defaultsCheck: {
|
||||
storage: "u",
|
||||
throughput: this.throughput(),
|
||||
@@ -350,11 +366,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
offerThroughput: this.throughput(),
|
||||
partitionKey: "",
|
||||
databaseId: this.keyspaceId(),
|
||||
rupm: false,
|
||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||
}),
|
||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
defaultsCheck: {
|
||||
storage: "u",
|
||||
throughput: this.throughput(),
|
||||
@@ -396,11 +413,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
offerThroughput: this.throughput(),
|
||||
partitionKey: "",
|
||||
databaseId: this.keyspaceId(),
|
||||
rupm: false,
|
||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||
}),
|
||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
defaultsCheck: {
|
||||
storage: "u",
|
||||
throughput: this.throughput(),
|
||||
@@ -426,11 +444,12 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
offerThroughput: this.throughput(),
|
||||
partitionKey: "",
|
||||
databaseId: this.keyspaceId(),
|
||||
rupm: false,
|
||||
hasDedicatedThroughput: this.dedicateTableThroughput()
|
||||
},
|
||||
keyspaceHasSharedOffer: this.keyspaceHasSharedOffer(),
|
||||
subscriptionType: SubscriptionType[this.container.subscriptionType()],
|
||||
subscriptionQuotaId: userContext.quotaId,
|
||||
subscriptionQuotaId: this.container.quotaId(),
|
||||
defaultsCheck: {
|
||||
storage: "u",
|
||||
throughput: this.throughput(),
|
||||
|
||||
@@ -98,21 +98,26 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||
author: string,
|
||||
notebookContent: string | ImmutableNotebook,
|
||||
parentDomElement: HTMLElement,
|
||||
isCodeOfConductEnabled: boolean,
|
||||
isLinkInjectionEnabled: boolean
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = await this.junoClient.isCodeOfConductAccepted();
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
|
||||
}
|
||||
if (isCodeOfConductEnabled) {
|
||||
try {
|
||||
const response = await this.junoClient.isCodeOfConductAccepted();
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
|
||||
}
|
||||
|
||||
this.isCodeOfConductAccepted = response.data;
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
"PublishNotebookPaneAdapter/isCodeOfConductAccepted",
|
||||
"Failed to check if code of conduct was accepted"
|
||||
);
|
||||
this.isCodeOfConductAccepted = response.data;
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
"PublishNotebookPaneAdapter/isCodeOfConductAccepted",
|
||||
"Failed to check if code of conduct was accepted"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.isCodeOfConductAccepted = true;
|
||||
}
|
||||
|
||||
this.name = name;
|
||||
|
||||
@@ -50,24 +50,13 @@
|
||||
id="fileImportLinkNotebook"
|
||||
data-bind="event: { click: onImportLinkClick, keypress: onImportLinkKeyPress }"
|
||||
>
|
||||
<img
|
||||
id="importFileButton"
|
||||
class="fileImportImg"
|
||||
src="/folder_16x16.svg"
|
||||
alt="upload files"
|
||||
title="Upload files"
|
||||
/>
|
||||
<img class="fileImportImg" src="/folder_16x16.svg" alt="upload files" title="Upload files" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paneFooter">
|
||||
<div class="leftpanel-okbut">
|
||||
<input
|
||||
id="uploadFileButton"
|
||||
type="submit"
|
||||
data-bind="attr: { value: submitButtonLabel }"
|
||||
class="btncreatecoll1"
|
||||
/>
|
||||
<input type="submit" data-bind="attr: { value: submitButtonLabel }" class="btncreatecoll1" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Upload File inputs - End -->
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
padding: 32px 16px;
|
||||
display: flex;
|
||||
background-color: @BaseLight;
|
||||
border: 1px solid #949494;
|
||||
border: 1px solid #E5E5E5;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -16,6 +16,8 @@ import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import Explorer from "../Explorer";
|
||||
import { updateOffer } from "../../Common/dataAccess/updateOffer";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
|
||||
@@ -33,6 +35,11 @@ const currentThroughput: (isAutoscale: boolean, throughput: number) => string =
|
||||
? `Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} RU/s`
|
||||
: `Current manual throughput: ${throughput} RU/s`;
|
||||
|
||||
const throughputApplyDelayedMessage = (isAutoscale: boolean, throughput: number, databaseName: string) =>
|
||||
`The request to increase the throughput has successfully been submitted.
|
||||
This operation will take 1-3 business days to complete. View the latest status in Notifications.<br />
|
||||
Database: ${databaseName}, ${currentThroughput(isAutoscale, throughput)}`;
|
||||
|
||||
const throughputApplyShortDelayMessage = (isAutoscale: boolean, throughput: number, databaseName: string) =>
|
||||
`A request to increase the throughput is currently in progress.
|
||||
This operation will take some time to complete.<br />
|
||||
@@ -59,8 +66,8 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
public displayedError: ko.Observable<string>;
|
||||
public isTemplateReady: ko.Observable<boolean>;
|
||||
public minRUAnotationVisible: ko.Computed<boolean>;
|
||||
public minRUs: ko.Observable<number>;
|
||||
public maxRUs: ko.Observable<number>;
|
||||
public minRUs: ko.Computed<number>;
|
||||
public maxRUs: ko.Computed<number>;
|
||||
public maxRUsText: ko.PureComputed<string>;
|
||||
public maxRUThroughputInputLimit: ko.Computed<number>;
|
||||
public notificationStatusInfo: ko.Observable<string>;
|
||||
@@ -85,7 +92,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
|
||||
private _hasProvisioningTypeChanged: ko.Computed<boolean>;
|
||||
private _wasAutopilotOriginallySet: ko.Observable<boolean>;
|
||||
private _offerReplacePending: ko.Observable<boolean>;
|
||||
private _offerReplacePending: ko.Computed<boolean>;
|
||||
private container: Explorer;
|
||||
|
||||
constructor(options: ViewModels.TabOptions) {
|
||||
@@ -104,14 +111,15 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
this._wasAutopilotOriginallySet = ko.observable(false);
|
||||
this.isAutoPilotSelected = editable.observable(false);
|
||||
this.autoPilotThroughput = editable.observable<number>();
|
||||
const offer = this.database && this.database.offer && this.database.offer();
|
||||
const offerAutopilotSettings = offer && offer.content && offer.content.offerAutopilotSettings;
|
||||
this.userCanChangeProvisioningTypes = ko.observable(true);
|
||||
|
||||
const autoscaleMaxThroughput = this.database?.offer()?.autoscaleMaxThroughput;
|
||||
if (autoscaleMaxThroughput) {
|
||||
if (AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||
if (offerAutopilotSettings && offerAutopilotSettings.maxThroughput) {
|
||||
if (AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)) {
|
||||
this._wasAutopilotOriginallySet(true);
|
||||
this.isAutoPilotSelected(true);
|
||||
this.autoPilotThroughput(autoscaleMaxThroughput);
|
||||
this.autoPilotThroughput(offerAutopilotSettings.maxThroughput);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +163,8 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
this.overrideWithAutoPilotSettings() ? this.autoPilotThroughput() : this.throughput(),
|
||||
serverId,
|
||||
regions,
|
||||
multimaster
|
||||
multimaster,
|
||||
false /*rupmEnabled*/
|
||||
);
|
||||
} else {
|
||||
estimatedSpend = PricingUtils.getEstimatedAutoscaleSpendHtml(
|
||||
@@ -196,15 +205,45 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
return this._hasProvisioningTypeChanged() && !this._wasAutopilotOriginallySet();
|
||||
});
|
||||
|
||||
this.minRUs = ko.observable<number>(
|
||||
this.database.offer()?.minimumThroughput || this.container.collectionCreationDefaults.throughput.unlimitedmin
|
||||
);
|
||||
this.minRUs = ko.computed<number>(() => {
|
||||
const offerContent =
|
||||
this.database && this.database.offer && this.database.offer() && this.database.offer().content;
|
||||
|
||||
// TODO: backend is returning 1,000,000 as min throughput which seems wrong
|
||||
// Setting to min throughput to not block and let the backend pass or fail
|
||||
if (offerContent && offerContent.offerAutopilotSettings) {
|
||||
return 400;
|
||||
}
|
||||
|
||||
const collectionThroughputInfo: DataModels.OfferThroughputInfo =
|
||||
offerContent && offerContent.collectionThroughputInfo;
|
||||
|
||||
if (collectionThroughputInfo && !!collectionThroughputInfo.minimumRUForCollection) {
|
||||
return collectionThroughputInfo.minimumRUForCollection;
|
||||
}
|
||||
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
|
||||
return throughputDefaults.unlimitedmin;
|
||||
});
|
||||
|
||||
this.minRUAnotationVisible = ko.computed<boolean>(() => {
|
||||
return PricingUtils.isLargerThanDefaultMinRU(this.minRUs());
|
||||
});
|
||||
|
||||
this.maxRUs = ko.observable<number>(this.container.collectionCreationDefaults.throughput.unlimitedmax);
|
||||
this.maxRUs = ko.computed<number>(() => {
|
||||
const collectionThroughputInfo: DataModels.OfferThroughputInfo =
|
||||
this.database &&
|
||||
this.database.offer &&
|
||||
this.database.offer() &&
|
||||
this.database.offer().content &&
|
||||
this.database.offer().content.collectionThroughputInfo;
|
||||
const numPartitions = collectionThroughputInfo && collectionThroughputInfo.numPhysicalPartitions;
|
||||
if (!!numPartitions) {
|
||||
return SharedConstants.CollectionCreation.MaxRUPerPartition * numPartitions;
|
||||
}
|
||||
|
||||
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
|
||||
return throughputDefaults.unlimitedmax;
|
||||
});
|
||||
|
||||
this.maxRUThroughputInputLimit = ko.pureComputed<number>(() => {
|
||||
if (configContext.platform === Platform.Hosted) {
|
||||
@@ -230,21 +269,37 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
return this.throughputTitle() + this.requestUnitsUsageCost();
|
||||
});
|
||||
this.pendingNotification = ko.observable<DataModels.Notification>();
|
||||
this._offerReplacePending = ko.observable<boolean>(!!this.database.offer()?.offerReplacePending);
|
||||
this._offerReplacePending = ko.pureComputed<boolean>(() => {
|
||||
const offer = this.database && this.database.offer && this.database.offer();
|
||||
return (
|
||||
offer &&
|
||||
offer.hasOwnProperty("headers") &&
|
||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||
);
|
||||
});
|
||||
this.notificationStatusInfo = ko.observable<string>("");
|
||||
this.shouldShowNotificationStatusPrompt = ko.computed<boolean>(() => this.notificationStatusInfo().length > 0);
|
||||
this.warningMessage = ko.computed<string>(() => {
|
||||
const offer = this.database && this.database.offer && this.database.offer();
|
||||
|
||||
if (this.overrideWithProvisionedThroughputSettings()) {
|
||||
return AutoPilotUtils.manualToAutoscaleDisclaimer;
|
||||
}
|
||||
|
||||
const offer = this.database.offer();
|
||||
if (offer?.offerReplacePending) {
|
||||
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
||||
if (
|
||||
offer &&
|
||||
offer.hasOwnProperty("headers") &&
|
||||
!!(offer as DataModels.OfferWithHeaders).headers[Constants.HttpHeaders.offerReplacePending]
|
||||
) {
|
||||
const throughput = offer.content.offerAutopilotSettings
|
||||
? offer.content.offerAutopilotSettings.maxThroughput
|
||||
: offer.content.offerThroughput;
|
||||
|
||||
return throughputApplyShortDelayMessage(this.isAutoPilotSelected(), throughput, this.database.id());
|
||||
}
|
||||
|
||||
if (
|
||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.canThroughputExceedMaximumValue()
|
||||
) {
|
||||
@@ -377,26 +432,60 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
||||
|
||||
try {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.database.id(),
|
||||
currentOffer: this.database.offer(),
|
||||
autopilotThroughput: this.isAutoPilotSelected() ? this.autoPilotThroughput() : undefined,
|
||||
manualThroughput: this.isAutoPilotSelected() ? undefined : this.throughput()
|
||||
};
|
||||
if (this.isAutoPilotSelected()) {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.database.id(),
|
||||
currentOffer: this.database.offer(),
|
||||
autopilotThroughput: this.autoPilotThroughput(),
|
||||
manualThroughput: undefined,
|
||||
migrateToAutoPilot: this._hasProvisioningTypeChanged()
|
||||
};
|
||||
|
||||
if (this._hasProvisioningTypeChanged()) {
|
||||
if (this.isAutoPilotSelected()) {
|
||||
updateOfferParams.migrateToAutoPilot = true;
|
||||
} else {
|
||||
updateOfferParams.migrateToManual = true;
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||
this.database.offer(updatedOffer);
|
||||
this.database.offer.valueHasMutated();
|
||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||
} else {
|
||||
if (this.throughput.editableIsDirty() || this.isAutoPilotSelected.editableIsDirty()) {
|
||||
const originalThroughputValue = this.throughput.getEditableOriginalValue();
|
||||
const newThroughput = this.throughput();
|
||||
|
||||
if (
|
||||
this.canThroughputExceedMaximumValue() &&
|
||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
|
||||
) {
|
||||
const requestPayload = {
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
databaseAccountName: userContext.databaseAccount.name,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseName: this.database.id(),
|
||||
throughput: newThroughput,
|
||||
offerIsRUPerMinuteThroughputEnabled: false
|
||||
};
|
||||
await updateOfferThroughputBeyondLimit(requestPayload);
|
||||
this.database.offer().content.offerThroughput = originalThroughputValue;
|
||||
this.throughput(originalThroughputValue);
|
||||
this.notificationStatusInfo(
|
||||
throughputApplyDelayedMessage(this.isAutoPilotSelected(), newThroughput, this.database.id())
|
||||
);
|
||||
this.throughput.valueHasMutated(); // force component re-render
|
||||
} else {
|
||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||
databaseId: this.database.id(),
|
||||
currentOffer: this.database.offer(),
|
||||
autopilotThroughput: undefined,
|
||||
manualThroughput: newThroughput,
|
||||
migrateToManual: this._hasProvisioningTypeChanged()
|
||||
};
|
||||
|
||||
const updatedOffer = await updateOffer(updateOfferParams);
|
||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||
this.database.offer(updatedOffer);
|
||||
this.database.offer.valueHasMutated();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||
this.database.offer(updatedOffer);
|
||||
this.database.offer.valueHasMutated();
|
||||
this._setBaseline();
|
||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||
} catch (error) {
|
||||
this.container.isRefreshingExplorer(false);
|
||||
this.isExecutionError(true);
|
||||
@@ -429,18 +518,24 @@ 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() {
|
||||
const offer = this.database && this.database.offer && this.database.offer();
|
||||
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(offer.autoscaleMaxThroughput));
|
||||
this.autoPilotThroughput.setBaseline(offer.autoscaleMaxThroughput);
|
||||
this.throughput.setBaseline(offer.manualThroughput);
|
||||
const offerThroughput = offer.content && offer.content.offerThroughput;
|
||||
const offerAutopilotSettings = offer && offer.content && offer.content.offerAutopilotSettings;
|
||||
|
||||
this.throughput.setBaseline(offerThroughput);
|
||||
this.userCanChangeProvisioningTypes(true);
|
||||
|
||||
const maxThroughputForAutoPilot = offerAutopilotSettings && offerAutopilotSettings.maxThroughput;
|
||||
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(maxThroughputForAutoPilot));
|
||||
this.autoPilotThroughput.setBaseline(maxThroughputForAutoPilot || AutoPilotUtils.minAutoPilotThroughput);
|
||||
}
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
1
src/Explorer/Tabs/MongoDocumentsTabV2.html
Normal file
1
src/Explorer/Tabs/MongoDocumentsTabV2.html
Normal file
@@ -0,0 +1 @@
|
||||
<div data-bind="react:mongoQueryComponentAdapter" style="height: 100%"></div>
|
||||
45
src/Explorer/Tabs/MongoDocumentsTabV2.ts
Normal file
45
src/Explorer/Tabs/MongoDocumentsTabV2.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as Q from "q";
|
||||
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
|
||||
import { MongoQueryComponentAdapter } from "../Notebook/MongoQueryComponent/MongoQueryComponentAdapter";
|
||||
|
||||
export default class MongoDocumentsTabV2 extends NotebookTabBase {
|
||||
private mongoQueryComponentAdapter: MongoQueryComponentAdapter;
|
||||
|
||||
constructor(options: NotebookTabBaseOptions) {
|
||||
super(options);
|
||||
this.mongoQueryComponentAdapter = new MongoQueryComponentAdapter({
|
||||
contentRef: undefined,
|
||||
notebookClient: NotebookTabBase.clientManager
|
||||
}, options.collection?.databaseId, options.collection?.id());
|
||||
}
|
||||
|
||||
public onCloseTabButtonClick(): Q.Promise<void> {
|
||||
super.onCloseTabButtonClick();
|
||||
|
||||
// const cleanup = () => {
|
||||
// this.notebookComponentAdapter.notebookShutdown();
|
||||
// this.isActive(false);
|
||||
// super.onCloseTabButtonClick();
|
||||
// };
|
||||
|
||||
// if (this.notebookComponentAdapter.isContentDirty()) {
|
||||
// this.container.showOkCancelModalDialog(
|
||||
// "Close without saving?",
|
||||
// `File has unsaved changes, close without saving?`,
|
||||
// "Close",
|
||||
// cleanup,
|
||||
// "Cancel",
|
||||
// undefined
|
||||
// );
|
||||
// return Q.resolve(null);
|
||||
// } else {
|
||||
// cleanup();
|
||||
// return Q.resolve(null);
|
||||
// }
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected buildCommandBarOptions(): void {
|
||||
this.updateNavbarWithTabsButtons();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
50
src/Explorer/Tabs/NotebookTabBase.ts
Normal file
50
src/Explorer/Tabs/NotebookTabBase.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
import { NotebookClientV2 } from "../Notebook/NotebookClientV2";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import Explorer from "../Explorer";
|
||||
import { ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { Areas } from "../../Common/Constants";
|
||||
|
||||
export interface NotebookTabBaseOptions extends ViewModels.TabOptions {
|
||||
container: Explorer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every notebook-based tab inherits from this class. It holds the static reference to a notebook client (singleton)
|
||||
*/
|
||||
export default class NotebookTabBase extends TabsBase {
|
||||
protected static clientManager: NotebookClientV2;
|
||||
protected container: Explorer;
|
||||
|
||||
constructor(options: NotebookTabBaseOptions) {
|
||||
super(options);
|
||||
|
||||
this.container = options.container;
|
||||
|
||||
if (!NotebookTabBase.clientManager) {
|
||||
NotebookTabBase.clientManager = new NotebookClientV2({
|
||||
connectionInfo: this.container.notebookServerInfo(),
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
contentProvider: this.container.notebookManager?.notebookContentProvider
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override base behavior
|
||||
*/
|
||||
protected getContainer(): Explorer {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
protected traceTelemetry(actionType: number): void {
|
||||
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
||||
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
||||
defaultExperience: this.container.defaultExperience && this.container.defaultExperience(),
|
||||
dataExplorerArea: Areas.Notebook
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,7 @@ import * as _ from "underscore";
|
||||
import * as Q from "q";
|
||||
import * as ko from "knockout";
|
||||
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg";
|
||||
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
|
||||
@@ -17,31 +15,25 @@ import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
|
||||
import InterruptKernelIcon from "../../../images/notebook/Notebook-stop.svg";
|
||||
import KillKernelIcon from "../../../images/notebook/Notebook-stop.svg";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { Areas, ArmApiVersions } from "../../Common/Constants";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { ArmApiVersions } from "../../Common/Constants";
|
||||
import { CommandBarComponentButtonFactory } from "../Menus/CommandBar/CommandBarComponentButtonFactory";
|
||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
|
||||
import { NotebookConfigurationUtils } from "../../Utils/NotebookConfigurationUtils";
|
||||
import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2";
|
||||
import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import Explorer from "../Explorer";
|
||||
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import { toJS, stringifyNotebook } from "@nteract/commutable";
|
||||
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
|
||||
|
||||
export interface NotebookTabOptions extends ViewModels.TabOptions {
|
||||
account: DataModels.DatabaseAccount;
|
||||
masterKey: string;
|
||||
container: Explorer;
|
||||
export interface NotebookTabOptions extends NotebookTabBaseOptions {
|
||||
notebookContentItem: NotebookContentItem;
|
||||
}
|
||||
|
||||
export default class NotebookTabV2 extends TabsBase {
|
||||
private static clientManager: NotebookClientV2;
|
||||
private container: Explorer;
|
||||
export default class NotebookTabV2 extends NotebookTabBase {
|
||||
public notebookPath: ko.Observable<string>;
|
||||
private selectedSparkPool: ko.Observable<string>;
|
||||
private notebookComponentAdapter: NotebookComponentAdapter;
|
||||
@@ -50,16 +42,6 @@ export default class NotebookTabV2 extends TabsBase {
|
||||
super(options);
|
||||
|
||||
this.container = options.container;
|
||||
|
||||
if (!NotebookTabV2.clientManager) {
|
||||
NotebookTabV2.clientManager = new NotebookClientV2({
|
||||
connectionInfo: this.container.notebookServerInfo(),
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
contentProvider: this.container.notebookManager?.notebookContentProvider
|
||||
});
|
||||
}
|
||||
|
||||
this.notebookPath = ko.observable(options.notebookContentItem.path);
|
||||
|
||||
this.container.notebookServerInfo.subscribe((newValue: DataModels.NotebookWorkspaceConnectionInfo) => {
|
||||
@@ -69,7 +51,7 @@ export default class NotebookTabV2 extends TabsBase {
|
||||
this.notebookComponentAdapter = new NotebookComponentAdapter({
|
||||
contentItem: options.notebookContentItem,
|
||||
notebooksBasePath: this.container.getNotebookBasePath(),
|
||||
notebookClient: NotebookTabV2.clientManager,
|
||||
notebookClient: NotebookTabBase.clientManager,
|
||||
onUpdateKernelInfo: this.onKernelUpdate
|
||||
});
|
||||
|
||||
@@ -115,10 +97,6 @@ export default class NotebookTabV2 extends TabsBase {
|
||||
return await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName());
|
||||
}
|
||||
|
||||
protected getContainer(): Explorer {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
||||
|
||||
@@ -493,12 +471,4 @@ export default class NotebookTabV2 extends TabsBase {
|
||||
|
||||
this.container.copyNotebook(notebookContent.name, content);
|
||||
};
|
||||
|
||||
private traceTelemetry(actionType: number) {
|
||||
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
||||
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
||||
defaultExperience: this.container.defaultExperience && this.container.defaultExperience(),
|
||||
dataExplorerArea: Areas.Notebook
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
723
src/Explorer/Tabs/SettingsTab.html
Normal file
723
src/Explorer/Tabs/SettingsTab.html
Normal file
@@ -0,0 +1,723 @@
|
||||
<div
|
||||
class="tab-pane flexContainer"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: tabId
|
||||
},
|
||||
visible: isActive"
|
||||
role="tabpanel"
|
||||
>
|
||||
<div class="warningErrorContainer scaleWarningContainer" data-bind="visible: shouldShowStatusBar">
|
||||
<div>
|
||||
<div class="warningErrorContent" data-bind="visible: shouldShowNotificationStatusPrompt">
|
||||
<span><img src="/info_color.svg" alt="Info"/></span>
|
||||
<span class="warningErrorDetailsLinkContainer" data-bind="html: notificationStatusInfo"></span>
|
||||
</div>
|
||||
<div class="warningErrorContent" data-bind="visible: !shouldShowNotificationStatusPrompt()">
|
||||
<span><img src="/warning.svg" alt="Warning"/></span>
|
||||
<span class="warningErrorDetailsLinkContainer" data-bind="html: warningMessage"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabForm scaleSettingScrollable">
|
||||
<!-- ko if: shouldShowKeyspaceSharedThroughputMessage -->
|
||||
<div>This table shared throughput is configured at the keyspace</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<!-- ko ifnot: hasDatabaseSharedThroughput -->
|
||||
<div>
|
||||
<div
|
||||
class="scaleDivison"
|
||||
data-bind="click:toggleScale, event: { keypress: onScaleKeyPress }, attr:{ 'aria-expanded': scaleExpanded() ? 'true' : 'false' }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Scale"
|
||||
aria-controls="scaleRegion"
|
||||
>
|
||||
<span class="themed-images" type="text/html" id="ExpandChevronRightScale" data-bind="visible: !scaleExpanded()">
|
||||
<img
|
||||
class="imgiconwidth ssExpandCollapseIcon ssCollapseIcon "
|
||||
src="/Triangle-right.svg"
|
||||
alt="Show scale properties"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span class="themed-images" type="text/html" id="ExpandChevronDownScale" data-bind="visible: scaleExpanded">
|
||||
<img class="imgiconwidth ssExpandCollapseIcon " src="/Triangle-down.svg" alt="Hide scale properties" />
|
||||
</span>
|
||||
|
||||
<span class="scaleSettingTitle">Scale</span>
|
||||
</div>
|
||||
|
||||
<div class="ssTextAllignment" data-bind="visible: scaleExpanded" id="scaleRegion">
|
||||
<!-- ko ifnot: isAutoScaleEnabled -->
|
||||
<throughput-input-autopilot-v3
|
||||
params="{
|
||||
testId: testId,
|
||||
class: 'scaleForm dirty',
|
||||
value: throughput,
|
||||
minimum: minRUs,
|
||||
maximum: maxRUThroughputInputLimit,
|
||||
isEnabled: !hasDatabaseSharedThroughput(),
|
||||
canExceedMaximumValue: canThroughputExceedMaximumValue,
|
||||
label: throughputTitle,
|
||||
ariaLabel: throughputAriaLabel,
|
||||
costsVisible: costsVisible,
|
||||
requestUnitsUsageCost: requestUnitsUsageCost,
|
||||
throughputAutoPilotRadioId: throughputAutoPilotRadioId,
|
||||
throughputProvisionedRadioId: throughputProvisionedRadioId,
|
||||
throughputModeRadioName: throughputModeRadioName,
|
||||
showAutoPilot: userCanChangeProvisioningTypes,
|
||||
isAutoPilotSelected: isAutoPilotSelected,
|
||||
maxAutoPilotThroughputSet: autoPilotThroughput,
|
||||
autoPilotUsageCost: autoPilotUsageCost,
|
||||
overrideWithAutoPilotSettings: overrideWithAutoPilotSettings,
|
||||
overrideWithProvisionedThroughputSettings: overrideWithProvisionedThroughputSettings
|
||||
}"
|
||||
>
|
||||
</throughput-input-autopilot-v3>
|
||||
|
||||
<div class="storageCapacityTitle throughputStorageValue" data-bind="html: storageCapacityTitle"></div>
|
||||
<!-- /ko -->
|
||||
|
||||
<div data-bind="visible: rupmVisible">
|
||||
<div class="formTitle">RU/m</div>
|
||||
<div class="tabs" aria-label="RU/m">
|
||||
<div class="tab">
|
||||
<label
|
||||
data-bind="
|
||||
attr:{
|
||||
for: rupmOnId
|
||||
},
|
||||
css: {
|
||||
dirty: rupm.editableIsDirty,
|
||||
selectedRadio: rupm() === 'on',
|
||||
unselectedRadio: rupm() !== 'on'
|
||||
}"
|
||||
>On</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="rupm"
|
||||
value="on"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: rupmOnId
|
||||
},
|
||||
checked: rupm"
|
||||
/>
|
||||
</div>
|
||||
<div class="tab">
|
||||
<label
|
||||
data-bind="
|
||||
attr:{
|
||||
for: rupmOffId
|
||||
},
|
||||
css: {
|
||||
dirty: rupm.editableIsDirty,
|
||||
selectedRadio: rupm() === 'off',
|
||||
unselectedRadio: rupm() !== 'off'
|
||||
}"
|
||||
>Off</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="rupm"
|
||||
value="off"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: rupmOffId
|
||||
},
|
||||
checked: rupm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TODO: Replace link with call to the Azure Support blade -->
|
||||
<div data-bind="visible: isAutoScaleEnabled">
|
||||
<div class="autoScaleThroughputTitle">Throughput (RU/s)</div>
|
||||
<input
|
||||
class="formReadOnly collid-white"
|
||||
readonly
|
||||
aria-label="Throughput input"
|
||||
data-bind="textInput: throughput"
|
||||
/>
|
||||
<div class="autoScaleDescription">
|
||||
Your account has custom settings that prevents setting throughput at the container level. Please work with
|
||||
your Cosmos DB engineering team point of contact to make changes.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
|
||||
<div data-bind="visible: hasConflictResolution">
|
||||
<div
|
||||
class="formTitle"
|
||||
data-bind="click:toggleConflictResolution, event: { keypress: onConflictResolutionKeyPress }, attr:{ 'aria-expanded': conflictResolutionExpanded() ? 'true' : 'false' }"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Conflict Resolution"
|
||||
aria-controls="conflictResolutionRegion"
|
||||
>
|
||||
<span
|
||||
class="themed-images"
|
||||
type="text/html"
|
||||
id="ExpandChevronRightConflictResolution"
|
||||
data-bind="visible: !conflictResolutionExpanded()"
|
||||
>
|
||||
<img
|
||||
class="imgiconwidth ssExpandCollapseIcon ssCollapseIcon"
|
||||
src="/Triangle-right.svg"
|
||||
alt="Show conflict resolution"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="themed-images"
|
||||
type="text/html"
|
||||
id="ExpandChevronDownConflictResolution"
|
||||
data-bind="visible: conflictResolutionExpanded"
|
||||
>
|
||||
<img class="imgiconwidth ssExpandCollapseIcon" src="/Triangle-down.svg" alt="Show conflict resolution" />
|
||||
</span>
|
||||
<span class="scaleSettingTitle">Conflict resolution</span>
|
||||
</div>
|
||||
<div id="conflictResolutionRegion" class="ssTextAllignment" data-bind="visible: conflictResolutionExpanded">
|
||||
<div class="formTitle">Mode</div>
|
||||
<div class="tabs" aria-label="Mode" role="radiogroup">
|
||||
<div class="tab">
|
||||
<label
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: conflictResolutionPolicyModeLWW,
|
||||
'aria-checked': conflictResolutionPolicyMode() !== 'Custom' ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: conflictResolutionPolicyMode.editableIsDirty,
|
||||
selectedRadio: conflictResolutionPolicyMode() === 'LastWriterWins',
|
||||
unselectedRadio: conflictResolutionPolicyMode() !== 'LastWriterWins'
|
||||
},
|
||||
event: {
|
||||
keypress: onConflictResolutionLWWKeyPress
|
||||
}"
|
||||
>Last Write Wins (default)</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="conflictresolution"
|
||||
value="LastWriterWins"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: conflictResolutionPolicyModeLWW
|
||||
},
|
||||
checked: conflictResolutionPolicyMode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<label
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: conflictResolutionPolicyModeCustom,
|
||||
'aria-checked': conflictResolutionPolicyMode() === 'Custom' ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: conflictResolutionPolicyMode.editableIsDirty,
|
||||
selectedRadio: conflictResolutionPolicyMode() === 'Custom',
|
||||
unselectedRadio: conflictResolutionPolicyMode() !== 'Custom'
|
||||
},
|
||||
event: {
|
||||
keypress: onConflictResolutionCustomKeyPress
|
||||
}"
|
||||
>Merge Procedure (custom)</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="conflictresolution"
|
||||
value="Custom"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: conflictResolutionPolicyModeCustom
|
||||
},
|
||||
checked: conflictResolutionPolicyMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: conflictResolutionPolicyMode() === 'LastWriterWins'">
|
||||
<p class="formTitle">
|
||||
Conflict Resolver Property
|
||||
<span class="infoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="tooltiptext infoTooltipWidth"
|
||||
>Gets or sets the name of a integer property in your documents which is used for the Last Write Wins
|
||||
(LWW) based conflict resolution scheme. By default, the system uses the system defined timestamp
|
||||
property, _ts to decide the winner for the conflicting versions of the document. Specify your own
|
||||
integer property if you want to override the default timestamp based conflict resolution.</span
|
||||
>
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<input
|
||||
type="text"
|
||||
aria-label="Document path for conflict resolution"
|
||||
data-bind="
|
||||
css: {
|
||||
dirty: conflictResolutionPolicyPath.editableIsDirty
|
||||
},
|
||||
textInput: conflictResolutionPolicyPath,
|
||||
enable: conflictResolutionPolicyMode() === 'LastWriterWins'"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<div data-bind="visible: conflictResolutionPolicyMode() === 'Custom'">
|
||||
<p class="formTitle">
|
||||
Stored procedure
|
||||
<span class="infoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="tooltiptext infoTooltipWidth"
|
||||
>Gets or sets the name of a stored procedure (aka merge procedure) for resolving the conflicts. You can
|
||||
write application defined logic to determine the winner of the conflicting versions of a document. The
|
||||
stored procedure will get executed transactionally, exactly once, on the server side. If you do not
|
||||
provide a stored procedure, the conflicts will be populated in the
|
||||
<a class="linkDarkBackground" href="https://aka.ms/dataexplorerconflics" target="_blank"
|
||||
>conflicts feed</a
|
||||
>. You can update/re-register the stored procedure at any time.</span
|
||||
>
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<input
|
||||
type="text"
|
||||
aria-label="Stored procedure name for conflict resolution"
|
||||
data-bind="
|
||||
css: {
|
||||
dirty: conflictResolutionPolicyProcedure.editableIsDirty
|
||||
},
|
||||
textInput: conflictResolutionPolicyProcedure,
|
||||
enable: conflictResolutionPolicyMode() === 'Custom'"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="formTitle"
|
||||
data-bind="click:toggleSettings, event: { keypress: onSettingsKeyPress }, attr:{ 'aria-expanded': settingsExpanded() ? 'true' : 'false' }, visible: shouldShowIndexingPolicyEditor() || ttlVisible()"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Settings"
|
||||
aria-controls="settingsRegion"
|
||||
>
|
||||
<span
|
||||
class="themed-images"
|
||||
type="text/html"
|
||||
id="ExpandChevronRightSettings"
|
||||
data-bind="visible: !settingsExpanded() && !hasDatabaseSharedThroughput()"
|
||||
>
|
||||
<img class="imgiconwidth ssExpandCollapseIcon ssCollapseIcon" src="/Triangle-right.svg" alt="Show settings" />
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="themed-images"
|
||||
type="text/html"
|
||||
id="ExpandChevronDownSettings"
|
||||
data-bind="visible: settingsExpanded() && !hasDatabaseSharedThroughput()"
|
||||
>
|
||||
<img class="imgiconwidth ssExpandCollapseIcon" src="/Triangle-down.svg" alt="Show settings" />
|
||||
</span>
|
||||
<span class="scaleSettingTitle">Settings</span>
|
||||
</div>
|
||||
<div class="ssTextAllignment" data-bind="visible: settingsExpanded" id="settingsRegion">
|
||||
<div data-bind="visible: ttlVisible">
|
||||
<div class="formTitle">Time to Live</div>
|
||||
<div class="tabs disableFocusDefaults" aria-label="Time to Live" role="radiogroup">
|
||||
<div class="tab">
|
||||
<label
|
||||
class="ttlIndexingPolicyFocusElement"
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: ttlOffId,
|
||||
'aria-checked': timeToLive() === 'off' ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: timeToLive.editableIsDirty,
|
||||
selectedRadio: timeToLive() === 'off',
|
||||
unselectedRadio: timeToLive() !== 'off'
|
||||
},
|
||||
event: {
|
||||
keypress: onTtlOffKeyPress
|
||||
},
|
||||
hasFocus: ttlOffFocused"
|
||||
>Off</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="ttl"
|
||||
value="off"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: ttlOffId
|
||||
},
|
||||
checked: timeToLive"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<label
|
||||
class="ttlIndexingPolicyFocusElement"
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: ttlOnNoDefaultId,
|
||||
'aria-checked': timeToLive() === 'on-nodefault' ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: timeToLive.editableIsDirty,
|
||||
selectedRadio: timeToLive() === 'on-nodefault',
|
||||
unselectedRadio: timeToLive() !== 'on-nodefault'
|
||||
},
|
||||
event: {
|
||||
keypress: onTtlOnNoDefaultKeyPress
|
||||
},
|
||||
hasFocus: ttlOnDefaultFocused"
|
||||
>On (no default)</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="ttl"
|
||||
value="on-nodefault"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: ttlOnNoDefaultId
|
||||
},
|
||||
checked: timeToLive"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<label
|
||||
class="ttlIndexingPolicyFocusElement"
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
for="ttl3"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: ttlOnId,
|
||||
'aria-checked': timeToLive() === 'on' ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: timeToLive.editableIsDirty,
|
||||
selectedRadio: timeToLive() === 'on',
|
||||
unselectedRadio: timeToLive() !== 'on'
|
||||
},
|
||||
event: {
|
||||
keypress: onTtlOnKeyPress
|
||||
},
|
||||
hasFocus: ttlOnFocused"
|
||||
>On</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="ttl"
|
||||
value="on"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: ttlOnId
|
||||
},
|
||||
checked: timeToLive"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: timeToLive() === 'on'">
|
||||
<input
|
||||
class="dirtyTextbox"
|
||||
type="number"
|
||||
required
|
||||
min="1"
|
||||
max="2147483647"
|
||||
aria-label="Time to live in seconds"
|
||||
data-bind="
|
||||
css: {
|
||||
dirty: timeToLive.editableIsDirty
|
||||
},
|
||||
textInput: timeToLiveSeconds,
|
||||
enable: timeToLive() === 'on'"
|
||||
/>
|
||||
second(s)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Geospatial - start -->
|
||||
<div data-bind="visible: geospatialVisible">
|
||||
<div class="formTitle">Geospatial Configuration</div>
|
||||
|
||||
<div class="tabs disableFocusDefaults" aria-label="Geospatial Configuration" role="radiogroup">
|
||||
<div class="tab">
|
||||
<label
|
||||
for="geography"
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
'aria-checked': geospatialConfigType().toLowerCase() !== GEOMETRY.toLowerCase() ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: geospatialConfigType.editableIsDirty,
|
||||
selectedRadio: geospatialConfigType().toLowerCase() !== GEOMETRY.toLowerCase(),
|
||||
unselectedRadio: geospatialConfigType().toLowerCase() === GEOMETRY.toLowerCase()
|
||||
},
|
||||
event: {
|
||||
keypress: onGeographyKeyPress
|
||||
}"
|
||||
>Geography</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="geospatial"
|
||||
id="geography"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr: {
|
||||
value: GEOGRAPHY
|
||||
},
|
||||
checked: geospatialConfigType"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<label
|
||||
for="geometry"
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
'aria-checked': geospatialConfigType().toLowerCase() === GEOMETRY.toLowerCase() ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: geospatialConfigType.editableIsDirty,
|
||||
selectedRadio: geospatialConfigType().toLowerCase() === GEOMETRY.toLowerCase(),
|
||||
unselectedRadio: geospatialConfigType().toLowerCase() !== GEOMETRY.toLowerCase()
|
||||
},
|
||||
event: {
|
||||
keypress: onGeometryKeyPress
|
||||
}"
|
||||
>Geometry</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="geospatial"
|
||||
id="geometry"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr: {
|
||||
value: GEOMETRY
|
||||
},
|
||||
checked: geospatialConfigType"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Geospatial - end -->
|
||||
|
||||
<div data-bind="visible: isAnalyticalStorageEnabled">
|
||||
<div class="formTitle">Analytical Storage Time to Live</div>
|
||||
<div class="tabs disableFocusDefaults" aria-label="Analytical Storage Time to Live" role="radiogroup">
|
||||
<div class="tab">
|
||||
<label tabindex="0" role="radio" class="disabledRadio">Off </label>
|
||||
</div>
|
||||
<div class="tab">
|
||||
<label
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: 'analyticalStorageTtlOnNoDefaultId',
|
||||
'aria-checked': analyticalStorageTtlSelection() === 'on-nodefault' ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: analyticalStorageTtlSelection.editableIsDirty,
|
||||
selectedRadio: analyticalStorageTtlSelection() === 'on-nodefault',
|
||||
unselectedRadio: analyticalStorageTtlSelection() !== 'on-nodefault'
|
||||
},
|
||||
event: {
|
||||
keypress: onAnalyticalStorageTtlOnNoDefaultKeyPress
|
||||
}"
|
||||
>On (no default)
|
||||
</label>
|
||||
<input
|
||||
type="radio"
|
||||
name="analyticalStorageTtl"
|
||||
value="on-nodefault"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: 'analyticalStorageTtlOnNoDefaultId'
|
||||
},
|
||||
checked: analyticalStorageTtlSelection"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tab">
|
||||
<label
|
||||
tabindex="0"
|
||||
role="radio"
|
||||
for="ttl3"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: 'analyticalStorageTtlOnId',
|
||||
'aria-checked': analyticalStorageTtlSelection() === 'on' ? 'true' : 'false'
|
||||
},
|
||||
css: {
|
||||
dirty: analyticalStorageTtlSelection.editableIsDirty,
|
||||
selectedRadio: analyticalStorageTtlSelection() === 'on',
|
||||
unselectedRadio: analyticalStorageTtlSelection() !== 'on'
|
||||
},
|
||||
event: {
|
||||
keypress: onAnalyticalStorageTtlOnKeyPress
|
||||
}"
|
||||
>On</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="analyticalStorageTtl"
|
||||
value="on"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: 'analyticalStorageTtlOnId'
|
||||
},
|
||||
checked: analyticalStorageTtlSelection"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: analyticalStorageTtlSelection() === 'on'">
|
||||
<input
|
||||
class="dirtyTextbox"
|
||||
type="number"
|
||||
required
|
||||
min="1"
|
||||
max="2147483647"
|
||||
aria-label="Time to live in seconds"
|
||||
data-bind="
|
||||
css: {
|
||||
dirty: analyticalStorageTtlSelection.editableIsDirty
|
||||
},
|
||||
textInput: analyticalStorageTtlSeconds,
|
||||
enable: analyticalStorageTtlSelection() === 'on'"
|
||||
/>
|
||||
second(s)
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: changeFeedPolicyVisible">
|
||||
<div class="formTitle">
|
||||
<span>Change feed log retention policy</span>
|
||||
<span class="infoTooltip" role="tooltip" tabindex="0">
|
||||
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
|
||||
<span class="tooltiptext infoTooltipWidth"
|
||||
>Enable change feed log retention policy to retain last 10 minutes of history for items in the container
|
||||
by default. To support this, the request unit (RU) charge for this container will be multiplied by a
|
||||
factor of two for writes. Reads are unaffected.</span
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<div class="tabs disableFocusDefaults" aria-label="Change feed selection tabs">
|
||||
<div class="tab">
|
||||
<label
|
||||
tabindex="0"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: changeFeedPolicyOffId
|
||||
},
|
||||
css: {
|
||||
dirty: changeFeedPolicyToggled.editableIsDirty,
|
||||
selectedRadio: changeFeedPolicyToggled() === 'Off',
|
||||
unselectedRadio: changeFeedPolicyToggled() === 'On'
|
||||
},
|
||||
event: {
|
||||
keypress: onChangeFeedPolicyOffKeyPress
|
||||
}"
|
||||
>Off</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="changeFeedPolicy"
|
||||
value="Off"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: changeFeedPolicyOffId
|
||||
},
|
||||
checked: changeFeedPolicyToggled"
|
||||
/>
|
||||
</div>
|
||||
<div class="tab">
|
||||
<label
|
||||
tabindex="0"
|
||||
data-bind="
|
||||
attr:{
|
||||
for: changeFeedPolicyOnId
|
||||
},
|
||||
css: {
|
||||
dirty: changeFeedPolicyToggled.editableIsDirty,
|
||||
selectedRadio: changeFeedPolicyToggled() === 'On',
|
||||
unselectedRadio: changeFeedPolicyToggled() === 'Off'
|
||||
},
|
||||
event: {
|
||||
keypress: onChangeFeedPolicyOnKeyPress
|
||||
}"
|
||||
>On</label
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="changeFeedPolicy"
|
||||
value="On"
|
||||
class="radio"
|
||||
data-bind="
|
||||
attr:{
|
||||
id: changeFeedPolicyOnId
|
||||
},
|
||||
checked: changeFeedPolicyToggled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-bind="visible: partitionKeyVisible">
|
||||
<div class="formTitle" data-bind="text: partitionKeyName">Partition Key</div>
|
||||
<input
|
||||
class="formReadOnly collid-white"
|
||||
data-bind="textInput: partitionKeyValue, attr: { 'aria-label':partitionKeyName }"
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
<div class="largePartitionKeyEnabled" data-bind="visible: isLargePartitionKeyEnabled">
|
||||
<p data-bind="visible: isLargePartitionKeyEnabled">
|
||||
Large <span data-bind="text:lowerCasePartitionKeyName"></span> has been enabled
|
||||
</p>
|
||||
</div>
|
||||
<div data-bind="visible: shouldShowIndexingPolicyEditor">
|
||||
<div class="formTitle">Indexing Policy</div>
|
||||
<div
|
||||
class="indexingPolicyEditor ttlIndexingPolicyFocusElement"
|
||||
tabindex="0"
|
||||
data-bind="setTemplateReady: true, attr:{ id: indexingPolicyEditorId }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
449
src/Explorer/Tabs/SettingsTab.test.ts
Normal file
449
src/Explorer/Tabs/SettingsTab.test.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import Collection from "../Tree/Collection";
|
||||
import Database from "../Tree/Database";
|
||||
import Explorer from "../Explorer";
|
||||
import SettingsTab from "./SettingsTab";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import { IndexingPolicies } from "../../Shared/Constants";
|
||||
|
||||
describe("Settings tab", () => {
|
||||
const baseCollection: DataModels.Collection = {
|
||||
defaultTtl: 200,
|
||||
partitionKey: null,
|
||||
conflictResolutionPolicy: {
|
||||
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||
conflictResolutionPath: "/_ts"
|
||||
},
|
||||
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||
_rid: "",
|
||||
_self: "",
|
||||
_etag: "",
|
||||
_ts: 0,
|
||||
id: "mycoll"
|
||||
};
|
||||
|
||||
const baseDatabase: DataModels.Database = {
|
||||
_rid: "",
|
||||
_self: "",
|
||||
_etag: "",
|
||||
_ts: 0,
|
||||
id: "mydb",
|
||||
collections: [baseCollection]
|
||||
};
|
||||
|
||||
const quotaInfo: DataModels.CollectionQuotaInfo = {
|
||||
storedProcedures: 0,
|
||||
triggers: 0,
|
||||
functions: 0,
|
||||
documentsSize: 0,
|
||||
documentsCount: 0,
|
||||
collectionSize: 0,
|
||||
usageSizeInKB: 0,
|
||||
numPartitions: 0
|
||||
};
|
||||
|
||||
describe("Conflict Resolution", () => {
|
||||
describe("should show conflict resolution", () => {
|
||||
let explorer: Explorer;
|
||||
const baseCollectionWithoutConflict: DataModels.Collection = {
|
||||
defaultTtl: 200,
|
||||
partitionKey: null,
|
||||
conflictResolutionPolicy: null,
|
||||
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||
_rid: "",
|
||||
_self: "",
|
||||
_etag: "",
|
||||
_ts: 0,
|
||||
id: "mycoll"
|
||||
};
|
||||
const getSettingsTab = (conflictResolution: boolean = true) => {
|
||||
return new SettingsTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(
|
||||
explorer,
|
||||
"mydb",
|
||||
conflictResolution ? baseCollection : baseCollectionWithoutConflict,
|
||||
quotaInfo,
|
||||
null
|
||||
),
|
||||
onUpdateTabsButtons: undefined
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("single master, should not show conflict resolution", () => {
|
||||
const settingsTab = getSettingsTab();
|
||||
expect(settingsTab.hasConflictResolution()).toBe(false);
|
||||
});
|
||||
|
||||
it("multi master with resolution conflict, show conflict resolution", () => {
|
||||
explorer.databaseAccount({
|
||||
id: "test",
|
||||
kind: "",
|
||||
location: "",
|
||||
name: "",
|
||||
tags: "",
|
||||
type: "",
|
||||
properties: {
|
||||
enableMultipleWriteLocations: true,
|
||||
documentEndpoint: "",
|
||||
cassandraEndpoint: "",
|
||||
gremlinEndpoint: "",
|
||||
tableEndpoint: ""
|
||||
}
|
||||
});
|
||||
|
||||
const settingsTab = getSettingsTab();
|
||||
expect(settingsTab.hasConflictResolution()).toBe(true);
|
||||
});
|
||||
|
||||
it("multi master without resolution conflict, show conflict resolution", () => {
|
||||
explorer.databaseAccount({
|
||||
id: "test",
|
||||
kind: "",
|
||||
location: "",
|
||||
name: "",
|
||||
tags: "",
|
||||
type: "",
|
||||
properties: {
|
||||
enableMultipleWriteLocations: true,
|
||||
documentEndpoint: "",
|
||||
cassandraEndpoint: "",
|
||||
gremlinEndpoint: "",
|
||||
tableEndpoint: ""
|
||||
}
|
||||
});
|
||||
|
||||
const settingsTab = getSettingsTab(false /* no resolution conflict*/);
|
||||
expect(settingsTab.hasConflictResolution()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Parse Conflict Resolution Mode from backend", () => {
|
||||
it("should parse any casing", () => {
|
||||
expect(SettingsTab.parseConflictResolutionMode("custom")).toBe(DataModels.ConflictResolutionMode.Custom);
|
||||
expect(SettingsTab.parseConflictResolutionMode("Custom")).toBe(DataModels.ConflictResolutionMode.Custom);
|
||||
expect(SettingsTab.parseConflictResolutionMode("lastWriterWins")).toBe(
|
||||
DataModels.ConflictResolutionMode.LastWriterWins
|
||||
);
|
||||
expect(SettingsTab.parseConflictResolutionMode("LastWriterWins")).toBe(
|
||||
DataModels.ConflictResolutionMode.LastWriterWins
|
||||
);
|
||||
});
|
||||
|
||||
it("should parse empty as null", () => {
|
||||
expect(SettingsTab.parseConflictResolutionMode("")).toBe(null);
|
||||
});
|
||||
|
||||
it("should parse null as null", () => {
|
||||
expect(SettingsTab.parseConflictResolutionMode(null)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Parse Conflict Resolution procedure from backend", () => {
|
||||
it("should parse path as name", () => {
|
||||
expect(SettingsTab.parseConflictResolutionProcedure("/dbs/xxxx/colls/xxxx/sprocs/validsproc")).toBe(
|
||||
"validsproc"
|
||||
);
|
||||
});
|
||||
|
||||
it("should parse name as name", () => {
|
||||
expect(SettingsTab.parseConflictResolutionProcedure("validsproc")).toBe("validsproc");
|
||||
});
|
||||
|
||||
it("should parse invalid path as null", () => {
|
||||
expect(SettingsTab.parseConflictResolutionProcedure("/not/a/valid/path")).toBe(null);
|
||||
});
|
||||
|
||||
it("should parse empty or null as null", () => {
|
||||
expect(SettingsTab.parseConflictResolutionProcedure("")).toBe(null);
|
||||
expect(SettingsTab.parseConflictResolutionProcedure(null)).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Should update collection", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("On TTL changed", () => {
|
||||
const settingsTab = new SettingsTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||
});
|
||||
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||
settingsTab.timeToLive("off");
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||
|
||||
settingsTab.onRevertClick();
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||
settingsTab.timeToLiveSeconds(100);
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||
});
|
||||
|
||||
it("On Index Policy changed", () => {
|
||||
const settingsTab = new SettingsTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||
});
|
||||
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||
settingsTab.indexingPolicyContent({ somethingDifferent: "" });
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||
});
|
||||
|
||||
it("On Conflict Resolution Mode changed", () => {
|
||||
const settingsTab = new SettingsTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||
});
|
||||
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||
settingsTab.conflictResolutionPolicyMode(DataModels.ConflictResolutionMode.Custom);
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||
|
||||
settingsTab.onRevertClick();
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||
settingsTab.conflictResolutionPolicyPath("/somethingElse");
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||
|
||||
settingsTab.onRevertClick();
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(false);
|
||||
settingsTab.conflictResolutionPolicyMode(DataModels.ConflictResolutionMode.Custom);
|
||||
settingsTab.conflictResolutionPolicyProcedure("resolver");
|
||||
expect(settingsTab.shouldUpdateCollection()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Get Conflict Resolution configuration from user", () => {
|
||||
let explorer: Explorer;
|
||||
|
||||
beforeEach(() => {
|
||||
explorer = new Explorer();
|
||||
});
|
||||
|
||||
it("null if it didnt change", () => {
|
||||
const settingsTab = new SettingsTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||
});
|
||||
|
||||
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
|
||||
});
|
||||
|
||||
it("Custom contains valid backend path", () => {
|
||||
const settingsTab = new SettingsTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||
});
|
||||
|
||||
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
|
||||
settingsTab.conflictResolutionPolicyMode(DataModels.ConflictResolutionMode.Custom);
|
||||
settingsTab.conflictResolutionPolicyProcedure("resolver");
|
||||
let updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
||||
expect(updatedPolicy.mode).toBe(DataModels.ConflictResolutionMode.Custom);
|
||||
expect(updatedPolicy.conflictResolutionProcedure).toBe("/dbs/mydb/colls/mycoll/sprocs/resolver");
|
||||
|
||||
settingsTab.conflictResolutionPolicyProcedure("");
|
||||
updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
||||
expect(updatedPolicy.conflictResolutionProcedure).toBe(undefined);
|
||||
});
|
||||
|
||||
it("LWW contains valid property path", () => {
|
||||
const settingsTab = new SettingsTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: new Collection(explorer, "mydb", baseCollection, quotaInfo, null),
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||
});
|
||||
|
||||
expect(settingsTab.getUpdatedConflictResolutionPolicy()).toBe(null);
|
||||
settingsTab.conflictResolutionPolicyPath("someAttr");
|
||||
let updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
||||
expect(updatedPolicy.conflictResolutionPath).toBe("/someAttr");
|
||||
|
||||
settingsTab.conflictResolutionPolicyPath("/someAttr");
|
||||
updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
||||
expect(updatedPolicy.conflictResolutionPath).toBe("/someAttr");
|
||||
|
||||
settingsTab.conflictResolutionPolicyPath("");
|
||||
updatedPolicy = settingsTab.getUpdatedConflictResolutionPolicy();
|
||||
expect(updatedPolicy.conflictResolutionPath).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("partitionKeyVisible", () => {
|
||||
enum PartitionKeyOption {
|
||||
None,
|
||||
System,
|
||||
NonSystem
|
||||
}
|
||||
|
||||
function getCollection(defaultApi: string, partitionKeyOption: PartitionKeyOption) {
|
||||
const explorer = new Explorer();
|
||||
explorer.defaultExperience(defaultApi);
|
||||
|
||||
const offer: DataModels.Offer = null;
|
||||
const defaultTtl = 200;
|
||||
const conflictResolutionPolicy = {
|
||||
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||
conflictResolutionPath: "/_ts"
|
||||
};
|
||||
|
||||
return new Collection(
|
||||
explorer,
|
||||
"mydb",
|
||||
{
|
||||
defaultTtl: defaultTtl,
|
||||
partitionKey:
|
||||
partitionKeyOption != PartitionKeyOption.None
|
||||
? {
|
||||
paths: ["/foo"],
|
||||
kind: "Hash",
|
||||
version: 2,
|
||||
systemKey: partitionKeyOption === PartitionKeyOption.System
|
||||
}
|
||||
: null,
|
||||
conflictResolutionPolicy: conflictResolutionPolicy,
|
||||
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||
_rid: "",
|
||||
_self: "",
|
||||
_etag: "",
|
||||
_ts: 0,
|
||||
id: "mycoll"
|
||||
},
|
||||
quotaInfo,
|
||||
offer
|
||||
);
|
||||
}
|
||||
|
||||
function getSettingsTab(defaultApi: string, partitionKeyOption: PartitionKeyOption): SettingsTab {
|
||||
return new SettingsTab({
|
||||
tabKind: ViewModels.CollectionTabKind.Settings,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
hashLocation: "",
|
||||
isActive: ko.observable(false),
|
||||
collection: getCollection(defaultApi, partitionKeyOption),
|
||||
onUpdateTabsButtons: (buttons: CommandButtonComponentProps[]): void => {}
|
||||
});
|
||||
}
|
||||
|
||||
it("on SQL container with no partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.DocumentDB, PartitionKeyOption.None);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on Mongo container with no partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.MongoDB, PartitionKeyOption.None);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on Gremlin container with no partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Graph, PartitionKeyOption.None);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on Cassandra container with no partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Cassandra, PartitionKeyOption.None);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on Table container with no partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Table, PartitionKeyOption.None);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on SQL container with system partition key should be true", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.DocumentDB, PartitionKeyOption.System);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it("on Mongo container with system partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.MongoDB, PartitionKeyOption.System);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on Gremlin container with system partition key should be true", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Graph, PartitionKeyOption.System);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it("on Cassandra container with system partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Cassandra, PartitionKeyOption.System);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on Table container with system partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Table, PartitionKeyOption.System);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on SQL container with non-system partition key should be true", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.DocumentDB, PartitionKeyOption.NonSystem);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it("on Mongo container with non-system partition key should be true", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.MongoDB, PartitionKeyOption.NonSystem);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it("on Gremlin container with non-system partition key should be true", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Graph, PartitionKeyOption.NonSystem);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it("on Cassandra container with non-system partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Cassandra, PartitionKeyOption.NonSystem);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it("on Table container with non-system partition key should be false", () => {
|
||||
const settingsTab = getSettingsTab(Constants.DefaultAccountExperience.Table, PartitionKeyOption.NonSystem);
|
||||
expect(settingsTab.partitionKeyVisible()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
1681
src/Explorer/Tabs/SettingsTab.ts
Normal file
1681
src/Explorer/Tabs/SettingsTab.ts
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user