mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-25 20:01:45 +00:00
Compare commits
8 Commits
2020-03-01
...
replace-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77f895d343 | ||
|
|
3bc2701356 | ||
|
|
35dbaeea96 | ||
|
|
18745a9ae6 | ||
|
|
5be6f982f9 | ||
|
|
4fc9393b76 | ||
|
|
ee51e873b8 | ||
|
|
206a8ef93b |
@@ -4,4 +4,3 @@ PORTAL_RUNNER_PASSWORD=
|
||||
PORTAL_RUNNER_SUBSCRIPTION=
|
||||
PORTAL_RUNNER_RESOURCE_GROUP=
|
||||
PORTAL_RUNNER_DATABASE_ACCOUNT=
|
||||
PORTAL_RUNNER_CONNECTION_STRING=
|
||||
@@ -266,6 +266,10 @@ src/ResourceProvider/ResourceProviderClientFactory.ts
|
||||
src/RouteHandlers/RouteHandler.ts
|
||||
src/RouteHandlers/TabRouteHandler.test.ts
|
||||
src/RouteHandlers/TabRouteHandler.ts
|
||||
src/Shared/AddCollectionUtility.test.ts
|
||||
src/Shared/AddCollectionUtility.ts
|
||||
src/Shared/AddDatabaseUtility.test.ts
|
||||
src/Shared/AddDatabaseUtility.ts
|
||||
src/Shared/Constants.ts
|
||||
src/Shared/DefaultExperienceUtility.test.ts
|
||||
src/Shared/DefaultExperienceUtility.ts
|
||||
@@ -275,6 +279,8 @@ src/Shared/StorageUtility.test.ts
|
||||
src/Shared/StorageUtility.ts
|
||||
src/Shared/StringUtility.test.ts
|
||||
src/Shared/StringUtility.ts
|
||||
src/Shared/Telemetry/TelemetryConstants.ts
|
||||
src/Shared/Telemetry/TelemetryProcessor.ts
|
||||
src/Shared/appInsights.ts
|
||||
src/SparkClusterManager/ArcadiaResourceManager.ts
|
||||
src/SparkClusterManager/SparkClusterManager.ts
|
||||
@@ -412,5 +418,6 @@ cypress/integration/dataexplorer/SQL/addCollection.spec.ts
|
||||
cypress/integration/dataexplorer/TABLE/addCollection.spec.ts
|
||||
cypress/integration/notebook/newNotebook.spec.ts
|
||||
cypress/integration/notebook/resourceTree.spec.ts
|
||||
__mocks__/AddDatabaseUtility.ts
|
||||
__mocks__/monaco-editor.ts
|
||||
src/Explorer/Tree/ResourceTreeAdapterForResourceToken.test.tsx
|
||||
@@ -27,7 +27,7 @@ module.exports = {
|
||||
plugins: ["react"]
|
||||
},
|
||||
{
|
||||
files: ["**/*.{test,spec}.{ts,tsx}"],
|
||||
files: ["**/*.test.{ts,tsx}"],
|
||||
env: {
|
||||
jest: true
|
||||
},
|
||||
|
||||
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@@ -3,8 +3,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- hotfix/**
|
||||
- release/**
|
||||
- hotfix/*
|
||||
- release/*
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
@@ -196,28 +196,6 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
endtoendpuppeteer:
|
||||
name: "End to end puppeteer tests"
|
||||
needs: [lint, format, compile, unittest]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: End to End Puppeteer Tests
|
||||
run: |
|
||||
npm ci
|
||||
npm start &
|
||||
npm run wait-for-server
|
||||
npm run test:e2e
|
||||
shell: bash
|
||||
env:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
PORTAL_RUNNER_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
||||
MONGO_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
||||
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
|
||||
nuget:
|
||||
name: Publish Nuget
|
||||
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
|
||||
|
||||
5
__mocks__/AddDatabaseUtility.ts
Normal file
5
__mocks__/AddDatabaseUtility.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class AddDbUtilities {
|
||||
createGremlinDatabase(params: any) {
|
||||
return Promise.resolve(1)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
"test": "cypress run",
|
||||
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
||||
"test:sql": "cypress run --browser chrome --spec \"./integration/dataexplorer/SQL/*\"",
|
||||
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser edge --headless",
|
||||
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser chrome --headless",
|
||||
"test:debug": "cypress open"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -3,9 +3,7 @@ const isCI = require("is-ci");
|
||||
module.exports = {
|
||||
launch: {
|
||||
headless: isCI,
|
||||
slowMo: 30,
|
||||
defaultViewport: null,
|
||||
ignoreHTTPSErrors: true,
|
||||
args: ["--disable-web-security"]
|
||||
slowMo: isCI ? null : 20,
|
||||
defaultViewport: null
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
@NotificationLow: #FFF4CE;
|
||||
@NotificationHigh: #F9E9B0;
|
||||
@Purple1: #8A2DA5;
|
||||
@Dirty: #9b4f96;
|
||||
|
||||
@BaseLow: #F2F2F2;
|
||||
@BaseMediumLow: #E6E6E6;
|
||||
@@ -55,7 +54,7 @@
|
||||
|
||||
@SelectionColor: #3074B0;
|
||||
|
||||
@FocusColor: #605e5c;
|
||||
@FocusColor: #00bcf2;
|
||||
|
||||
/******************************************************************************
|
||||
METRICS
|
||||
@@ -105,7 +104,6 @@
|
||||
@newCollectionPaneInputWidth: 300px;
|
||||
@tooltipTextWidth: 280px;
|
||||
@sharedCollectionThroughputTooltipTextWidth: 150px;
|
||||
@mongoWildcardIndexTooltipWidth: 150px;
|
||||
@addContainerPaneThroughputInfoWidth: 370px;
|
||||
@optionsInfoWidth: 210px;
|
||||
@noFixedCollectionsTooltipWidth: 196px;
|
||||
|
||||
@@ -1522,10 +1522,6 @@ p {
|
||||
.tooltipVisible();
|
||||
}
|
||||
|
||||
.infoTooltip a {
|
||||
color: @AccentHigh;
|
||||
}
|
||||
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1565,10 +1561,6 @@ p {
|
||||
min-width: @tooltipTextWidth;
|
||||
}
|
||||
|
||||
.mongoWildcardIndexTooltipWidth {
|
||||
min-width: @mongoWildcardIndexTooltipWidth;
|
||||
}
|
||||
|
||||
.sharedCollectionThroughputTooltipWidth {
|
||||
min-width: @sharedCollectionThroughputTooltipTextWidth;
|
||||
}
|
||||
@@ -1654,7 +1646,7 @@ p {
|
||||
}
|
||||
|
||||
.contextual-pane .collid {
|
||||
border: 1px solid #605e5c;
|
||||
border: 1px solid #bbbbbb;
|
||||
font-size: 10px;
|
||||
padding: 5px 10px;
|
||||
color: #000;
|
||||
@@ -1747,7 +1739,7 @@ input::-webkit-calendar-picker-indicator {
|
||||
padding-right: 34px;
|
||||
color: @BaseDark;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
overflow-x: hidden;
|
||||
margin: (2 * @MediumSpace) 0px;
|
||||
}
|
||||
|
||||
@@ -2082,7 +2074,7 @@ a:link {
|
||||
.resourceTreeAndTabs {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -2431,6 +2423,22 @@ a:link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder {
|
||||
color: #969696;
|
||||
}
|
||||
|
||||
::-moz-placeholder {
|
||||
color: #969696;
|
||||
}
|
||||
|
||||
:-ms-input-placeholder {
|
||||
color: #969696;
|
||||
}
|
||||
|
||||
:-moz-placeholder {
|
||||
color: #969696;
|
||||
}
|
||||
|
||||
::-ms-expand {
|
||||
color: #969696;
|
||||
}
|
||||
@@ -2980,10 +2988,6 @@ settings-pane {
|
||||
.enableAnalyticalStorageRadio:nth-child(n+2) {
|
||||
margin-left: @LargeSpace;
|
||||
}
|
||||
|
||||
.enableAnalyticalStorageRadioLabel {
|
||||
padding: 0px
|
||||
}
|
||||
}
|
||||
|
||||
.addCollectionLabel {
|
||||
@@ -3013,12 +3017,4 @@ settings-pane {
|
||||
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.warningErrorContent a {
|
||||
color: @AccentMediumHigh
|
||||
}
|
||||
|
||||
.infoBoxContent a {
|
||||
color: @AccentMediumHigh
|
||||
}
|
||||
}
|
||||
936
package-lock.json
generated
936
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -8,7 +8,7 @@
|
||||
"@azure/cosmos-language-service": "0.0.4",
|
||||
"@jupyterlab/services": "4.2.0",
|
||||
"@jupyterlab/terminal": "1.2.1",
|
||||
"@microsoft/applicationinsights-web": "2.5.8",
|
||||
"@microsoft/applicationinsights-web": "2.5.4",
|
||||
"@nteract/commutable": "7.1.4",
|
||||
"@nteract/connected-components": "6.7.8",
|
||||
"@nteract/core": "13.0.0",
|
||||
@@ -55,7 +55,6 @@
|
||||
"es6-object-assign": "1.1.0",
|
||||
"es6-symbol": "3.1.3",
|
||||
"eslint-plugin-jest": "23.13.2",
|
||||
"eslint-plugin-react": "7.20.0",
|
||||
"hasher": "1.2.0",
|
||||
"html2canvas": "1.0.0-rc.5",
|
||||
"immutable": "4.0.0-rc.12",
|
||||
@@ -67,7 +66,7 @@
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.15.6",
|
||||
"object.entries": "1.1.0",
|
||||
"office-ui-fabric-react": "7.134.1",
|
||||
"office-ui-fabric-react": "7.121.10",
|
||||
"p-retry": "4.2.0",
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"promise-polyfill": "8.1.0",
|
||||
@@ -103,15 +102,12 @@
|
||||
"@types/d3": "4.13.2",
|
||||
"@types/enzyme": "3.10.3",
|
||||
"@types/enzyme-adapter-react-16": "1.0.5",
|
||||
"@types/expect-puppeteer": "4.4.3",
|
||||
"@types/hasher": "0.0.31",
|
||||
"@types/jest": "23.3.10",
|
||||
"@types/jest-environment-puppeteer": "4.3.2",
|
||||
"@types/memoize-one": "4.1.1",
|
||||
"@types/node": "12.11.1",
|
||||
"@types/promise.prototype.finally": "2.0.3",
|
||||
"@types/prop-types": "15.5.8",
|
||||
"@types/puppeteer": "3.0.1",
|
||||
"@types/q": "1.5.1",
|
||||
"@types/react": "16.8.25",
|
||||
"@types/react-dom": "16.0.7",
|
||||
@@ -122,8 +118,8 @@
|
||||
"@types/text-encoding": "0.0.33",
|
||||
"@types/underscore": "1.7.36",
|
||||
"@types/webfontloader": "1.6.29",
|
||||
"@typescript-eslint/eslint-plugin": "4.0.1",
|
||||
"@typescript-eslint/parser": "4.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "3.2.0",
|
||||
"@typescript-eslint/parser": "3.2.0",
|
||||
"adal-angular": "1.0.15",
|
||||
"axe-puppeteer": "1.1.0",
|
||||
"babel-jest": "24.9.0",
|
||||
@@ -136,10 +132,11 @@
|
||||
"enzyme": "3.10.0",
|
||||
"enzyme-adapter-react-16": "1.15.1",
|
||||
"enzyme-to-json": "3.4.3",
|
||||
"eslint": "7.8.1",
|
||||
"eslint": "7.3.1",
|
||||
"eslint-cli": "1.1.1",
|
||||
"eslint-plugin-no-null": "1.0.2",
|
||||
"eslint-plugin-prefer-arrow": "1.2.2",
|
||||
"eslint-plugin-react": "7.20.0",
|
||||
"expose-loader": "0.7.5",
|
||||
"file-loader": "2.0.0",
|
||||
"fs-extra": "7.0.0",
|
||||
@@ -167,9 +164,8 @@
|
||||
"ts-loader": "6.2.2",
|
||||
"tslint": "5.11.0",
|
||||
"tslint-microsoft-contrib": "6.0.0",
|
||||
"typescript": "4.0.2",
|
||||
"typescript": "3.9.6",
|
||||
"url-loader": "1.1.1",
|
||||
"wait-on": "4.0.2",
|
||||
"webpack": "4.43.0",
|
||||
"webpack-bundle-analyzer": "3.6.1",
|
||||
"webpack-cli": "3.3.10",
|
||||
@@ -188,13 +184,12 @@
|
||||
"test": "rimraf coverage && jest",
|
||||
"test:e2e": "jest -c ./jest.config.e2e.js --detectOpenHandles",
|
||||
"watch": "npm run start",
|
||||
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
||||
"build:ase": "gulp build:ase",
|
||||
"compile": "tsc",
|
||||
"compile:contracts": "tsc -p ./tsconfig.contracts.json",
|
||||
"compile:strict": "tsc -p ./tsconfig.strict.json",
|
||||
"format": "prettier --write \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||
"format:check": "prettier --check \"{src,cypress,test}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||
"format": "prettier --write \"{src,cypress}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||
"format:check": "prettier --check \"{src,cypress}/**/*.{ts,tsx,html}\" \"*.{js,html}\"",
|
||||
"lint": "tslint --project tsconfig.json && eslint \"**/*.{ts,tsx}\"",
|
||||
"build:contracts": "npm run compile:contracts",
|
||||
"strictEligibleFiles": "node ./strict-migration-tools/index.js",
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"offerThroughput": 400,
|
||||
"databaseLevelThroughput": false,
|
||||
"collectionId": "Persons",
|
||||
"createNewDatabase": true,
|
||||
"partitionKey": { "kind": "Hash", "paths": ["/firstname"], "version": 1 },
|
||||
"rupmEnabled": false,
|
||||
"partitionKey": { "kind": "Hash", "paths": ["/firstname"] },
|
||||
"data": [
|
||||
{
|
||||
"firstname": "Eva",
|
||||
@@ -23,4 +23,4 @@
|
||||
"age": 23
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export class ArmApiVersions {
|
||||
public static readonly arcadiaLivy: string = "2019-11-01-preview";
|
||||
public static readonly arm: string = "2015-11-01";
|
||||
public static readonly armFeatures: string = "2014-08-01-preview";
|
||||
public static readonly publicVersion = "2020-03-01";
|
||||
public static readonly publicVersion = "2020-04-01";
|
||||
}
|
||||
|
||||
export class ArmResourceTypes {
|
||||
@@ -124,7 +124,6 @@ export class Features {
|
||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||
public static readonly enableCodeOfConduct = "enablecodeofconduct";
|
||||
public static readonly enableLinkInjection = "enablelinkinjection";
|
||||
public static readonly enableSettingsV2 = "enablesettingsv2";
|
||||
public static readonly enableSpark = "enablespark";
|
||||
public static readonly livyEndpoint = "livyendpoint";
|
||||
public static readonly notebookServerUrl = "notebookserverurl";
|
||||
@@ -171,8 +170,89 @@ export enum MongoBackendEndpointType {
|
||||
remote
|
||||
}
|
||||
|
||||
export class MongoBackend {
|
||||
public static localhostEndpoint: string = "/api/mongo/explorer";
|
||||
public static centralUsEndpoint: string = "https://main.documentdb.ext.azure.com/api/mongo/explorer";
|
||||
public static northEuropeEndpoint: string = "https://main.documentdb.ext.azure.com/api/mongo/explorer";
|
||||
public static southEastAsiaEndpoint: string = "https://main.documentdb.ext.azure.com/api/mongo/explorer";
|
||||
|
||||
public static endpointsByRegion: any = {
|
||||
default: MongoBackend.centralUsEndpoint,
|
||||
northeurope: MongoBackend.northEuropeEndpoint,
|
||||
ukwest: MongoBackend.northEuropeEndpoint,
|
||||
uksouth: MongoBackend.northEuropeEndpoint,
|
||||
westeurope: MongoBackend.northEuropeEndpoint,
|
||||
australiaeast: MongoBackend.southEastAsiaEndpoint,
|
||||
australiasoutheast: MongoBackend.southEastAsiaEndpoint,
|
||||
centralindia: MongoBackend.southEastAsiaEndpoint,
|
||||
eastasia: MongoBackend.southEastAsiaEndpoint,
|
||||
japaneast: MongoBackend.southEastAsiaEndpoint,
|
||||
japanwest: MongoBackend.southEastAsiaEndpoint,
|
||||
koreacentral: MongoBackend.southEastAsiaEndpoint,
|
||||
koreasouth: MongoBackend.southEastAsiaEndpoint,
|
||||
southeastasia: MongoBackend.southEastAsiaEndpoint,
|
||||
southindia: MongoBackend.southEastAsiaEndpoint,
|
||||
westindia: MongoBackend.southEastAsiaEndpoint
|
||||
};
|
||||
|
||||
public static endpointsByEnvironment: any = {
|
||||
default: MongoBackendEndpointType.local,
|
||||
localhost: MongoBackendEndpointType.local,
|
||||
prod1: MongoBackendEndpointType.remote,
|
||||
prod2: MongoBackendEndpointType.remote
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: 435619 Add default endpoints per cloud and use regional only when available
|
||||
export class CassandraBackend {
|
||||
public static readonly localhostEndpoint: string = "https://localhost:12901/";
|
||||
public static readonly devEndpoint: string = "https://platformproxycassandradev.azurewebsites.net/";
|
||||
|
||||
public static readonly centralUsEndpoint: string = "https://main.documentdb.ext.azure.com/";
|
||||
public static readonly northEuropeEndpoint: string = "https://main.documentdb.ext.azure.com/";
|
||||
public static readonly southEastAsiaEndpoint: string = "https://main.documentdb.ext.azure.com/";
|
||||
|
||||
public static readonly bf_default: string = "https://main.documentdb.ext.microsoftazure.de/";
|
||||
public static readonly mc_default: string = "https://main.documentdb.ext.azure.cn/";
|
||||
public static readonly ff_default: string = "https://main.documentdb.ext.azure.us/";
|
||||
|
||||
public static readonly endpointsByRegion: any = {
|
||||
default: CassandraBackend.centralUsEndpoint,
|
||||
northeurope: CassandraBackend.northEuropeEndpoint,
|
||||
ukwest: CassandraBackend.northEuropeEndpoint,
|
||||
uksouth: CassandraBackend.northEuropeEndpoint,
|
||||
westeurope: CassandraBackend.northEuropeEndpoint,
|
||||
australiaeast: CassandraBackend.southEastAsiaEndpoint,
|
||||
australiasoutheast: CassandraBackend.southEastAsiaEndpoint,
|
||||
centralindia: CassandraBackend.southEastAsiaEndpoint,
|
||||
eastasia: CassandraBackend.southEastAsiaEndpoint,
|
||||
japaneast: CassandraBackend.southEastAsiaEndpoint,
|
||||
japanwest: CassandraBackend.southEastAsiaEndpoint,
|
||||
koreacentral: CassandraBackend.southEastAsiaEndpoint,
|
||||
koreasouth: CassandraBackend.southEastAsiaEndpoint,
|
||||
southeastasia: CassandraBackend.southEastAsiaEndpoint,
|
||||
southindia: CassandraBackend.southEastAsiaEndpoint,
|
||||
westindia: CassandraBackend.southEastAsiaEndpoint,
|
||||
|
||||
// Black Forest
|
||||
germanycentral: CassandraBackend.bf_default,
|
||||
germanynortheast: CassandraBackend.bf_default,
|
||||
|
||||
// Fairfax
|
||||
usdodeast: CassandraBackend.ff_default,
|
||||
usdodcentral: CassandraBackend.ff_default,
|
||||
usgovarizona: CassandraBackend.ff_default,
|
||||
usgoviowa: CassandraBackend.ff_default,
|
||||
usgovtexas: CassandraBackend.ff_default,
|
||||
usgovvirginia: CassandraBackend.ff_default,
|
||||
|
||||
// Mooncake
|
||||
chinaeast: CassandraBackend.mc_default,
|
||||
chinaeast2: CassandraBackend.mc_default,
|
||||
chinanorth: CassandraBackend.mc_default,
|
||||
chinanorth2: CassandraBackend.mc_default
|
||||
};
|
||||
|
||||
public static readonly createOrDeleteApi: string = "api/cassandra/createordelete";
|
||||
public static readonly guestCreateOrDeleteApi: string = "api/guest/cassandra/createordelete";
|
||||
public static readonly queryApi: string = "api/cassandra";
|
||||
@@ -482,11 +562,3 @@ export class AnalyticalStorageTtl {
|
||||
public static readonly Infinite: number = -1;
|
||||
public static readonly Disabled: number = 0;
|
||||
}
|
||||
|
||||
export class TerminalQueryParams {
|
||||
public static readonly Terminal = "terminal";
|
||||
public static readonly Server = "server";
|
||||
public static readonly Token = "token";
|
||||
public static readonly SubscriptionId = "subscriptionId";
|
||||
public static readonly TerminalEndpoint = "terminalEndpoint";
|
||||
}
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
import * as _ from "underscore";
|
||||
import * as Constants from "./Constants";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as HeadersUtility from "./HeadersUtility";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import Q from "q";
|
||||
import {
|
||||
ConflictDefinition,
|
||||
FeedOptions,
|
||||
ItemDefinition,
|
||||
OfferDefinition,
|
||||
PartitionKeyDefinition,
|
||||
QueryIterator,
|
||||
Resource
|
||||
Resource,
|
||||
TriggerDefinition,
|
||||
OfferDefinition
|
||||
} 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 { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest";
|
||||
import { client } from "./CosmosClient";
|
||||
import * as HeadersUtility from "./HeadersUtility";
|
||||
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
||||
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
|
||||
import { sendCachedDataMessage } from "./MessageHandler";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { OfferUtils } from "../Utils/OfferUtils";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||
import { Platform, configContext } from "../ConfigContext";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||
|
||||
export function getCommonQueryOptions(options: FeedOptions): any {
|
||||
const storedItemPerPageSetting: number = LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage);
|
||||
@@ -53,55 +58,85 @@ export function queryDocuments(
|
||||
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 updateOffer(
|
||||
offer: DataModels.Offer,
|
||||
newOffer: DataModels.Offer,
|
||||
options?: RequestOptions
|
||||
): Q.Promise<DataModels.Offer> {
|
||||
return Q(
|
||||
client()
|
||||
.offer(offer.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)
|
||||
.then(response => {
|
||||
return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function updateDocument(
|
||||
collection: ViewModels.CollectionBase,
|
||||
documentId: DocumentId,
|
||||
newDocument: any
|
||||
): Q.Promise<any> {
|
||||
const partitionKey = documentId.partitionKeyValue;
|
||||
|
||||
export function readStoredProcedures(
|
||||
collection: ViewModels.Collection,
|
||||
options?: any
|
||||
): Q.Promise<DataModels.StoredProcedure[]> {
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), partitionKey)
|
||||
.replace(newDocument)
|
||||
.then(response => response.resource)
|
||||
.scripts.storedProcedures.readAll(options)
|
||||
.fetchAll()
|
||||
.then(response => response.resources as DataModels.StoredProcedure[])
|
||||
);
|
||||
}
|
||||
|
||||
export function readStoredProcedure(
|
||||
collection: ViewModels.Collection,
|
||||
requestedResource: DataModels.Resource,
|
||||
options?: any
|
||||
): Q.Promise<DataModels.StoredProcedure> {
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.storedProcedure(requestedResource.id)
|
||||
.read(options)
|
||||
.then(response => response.resource as DataModels.StoredProcedure)
|
||||
);
|
||||
}
|
||||
export function readUserDefinedFunctions(
|
||||
collection: ViewModels.Collection,
|
||||
options: any
|
||||
): Q.Promise<DataModels.UserDefinedFunction[]> {
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.userDefinedFunctions.readAll(options)
|
||||
.fetchAll()
|
||||
.then(response => response.resources as DataModels.UserDefinedFunction[])
|
||||
);
|
||||
}
|
||||
export function readUserDefinedFunction(
|
||||
collection: ViewModels.Collection,
|
||||
requestedResource: DataModels.Resource,
|
||||
options?: any
|
||||
): Q.Promise<DataModels.UserDefinedFunction> {
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.userDefinedFunction(requestedResource.id)
|
||||
.read(options)
|
||||
.then(response => response.resource as DataModels.UserDefinedFunction)
|
||||
);
|
||||
}
|
||||
|
||||
export function readTriggers(collection: ViewModels.Collection, options: any): Q.Promise<DataModels.Trigger[]> {
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.triggers.readAll(options)
|
||||
.fetchAll()
|
||||
.then(response => response.resources as DataModels.Trigger[])
|
||||
);
|
||||
}
|
||||
|
||||
export function readTrigger(
|
||||
collection: ViewModels.Collection,
|
||||
requestedResource: DataModels.Resource,
|
||||
options?: any
|
||||
): Q.Promise<DataModels.Trigger> {
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.trigger(requestedResource.id)
|
||||
.read(options)
|
||||
.then(response => response.resource as DataModels.Trigger)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,16 +168,6 @@ export function executeStoredProcedure(
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -156,6 +181,155 @@ export function readDocument(collection: ViewModels.CollectionBase, documentId:
|
||||
);
|
||||
}
|
||||
|
||||
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 updateOffer(
|
||||
offer: DataModels.Offer,
|
||||
newOffer: DataModels.Offer,
|
||||
options?: RequestOptions
|
||||
): Q.Promise<DataModels.Offer> {
|
||||
return Q(
|
||||
client()
|
||||
.offer(offer.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)
|
||||
.then(response => {
|
||||
return Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => response.resource);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function updateStoredProcedure(
|
||||
collection: ViewModels.Collection,
|
||||
storedProcedure: DataModels.StoredProcedure,
|
||||
options: any
|
||||
): Q.Promise<DataModels.StoredProcedure> {
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.storedProcedure(storedProcedure.id)
|
||||
.replace(storedProcedure, options)
|
||||
.then(response => response.resource as DataModels.StoredProcedure)
|
||||
);
|
||||
}
|
||||
|
||||
export function updateUserDefinedFunction(
|
||||
collection: ViewModels.Collection,
|
||||
userDefinedFunction: DataModels.UserDefinedFunction,
|
||||
options?: any
|
||||
): Q.Promise<DataModels.UserDefinedFunction> {
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.userDefinedFunction(userDefinedFunction.id)
|
||||
.replace(userDefinedFunction, options)
|
||||
.then(response => response.resource as DataModels.StoredProcedure)
|
||||
);
|
||||
}
|
||||
|
||||
export function updateTrigger(
|
||||
collection: ViewModels.Collection,
|
||||
trigger: DataModels.Trigger,
|
||||
options?: any
|
||||
): Q.Promise<DataModels.Trigger> {
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.trigger(trigger.id)
|
||||
.replace(trigger as TriggerDefinition, options)
|
||||
.then(response => response.resource as DataModels.Trigger)
|
||||
);
|
||||
}
|
||||
|
||||
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 as DataModels.StoredProcedure)
|
||||
);
|
||||
}
|
||||
|
||||
export function createStoredProcedure(
|
||||
collection: ViewModels.Collection,
|
||||
newStoredProcedure: DataModels.StoredProcedure,
|
||||
options?: any
|
||||
): Q.Promise<DataModels.StoredProcedure> {
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.storedProcedures.create(newStoredProcedure, options)
|
||||
.then(response => response.resource as DataModels.StoredProcedure)
|
||||
);
|
||||
}
|
||||
|
||||
export function createUserDefinedFunction(
|
||||
collection: ViewModels.Collection,
|
||||
newUserDefinedFunction: DataModels.UserDefinedFunction,
|
||||
options: any
|
||||
): Q.Promise<DataModels.UserDefinedFunction> {
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.userDefinedFunctions.create(newUserDefinedFunction, options)
|
||||
.then(response => response.resource as DataModels.UserDefinedFunction)
|
||||
);
|
||||
}
|
||||
|
||||
export function createTrigger(
|
||||
collection: ViewModels.Collection,
|
||||
newTrigger: DataModels.Trigger,
|
||||
options?: any
|
||||
): Q.Promise<DataModels.Trigger> {
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.triggers.create(newTrigger as TriggerDefinition, options)
|
||||
.then(response => response.resource as DataModels.Trigger)
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteDocument(collection: ViewModels.CollectionBase, documentId: DocumentId): Q.Promise<any> {
|
||||
const partitionKey = documentId.partitionKeyValue;
|
||||
|
||||
@@ -184,6 +358,191 @@ export function deleteConflict(
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteStoredProcedure(
|
||||
collection: ViewModels.Collection,
|
||||
storedProcedure: DataModels.StoredProcedure,
|
||||
options: any
|
||||
): Q.Promise<any> {
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.storedProcedure(storedProcedure.id)
|
||||
.delete()
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteUserDefinedFunction(
|
||||
collection: ViewModels.Collection,
|
||||
userDefinedFunction: DataModels.UserDefinedFunction,
|
||||
options: any
|
||||
): Q.Promise<any> {
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.userDefinedFunction(userDefinedFunction.id)
|
||||
.delete()
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteTrigger(
|
||||
collection: ViewModels.Collection,
|
||||
trigger: DataModels.Trigger,
|
||||
options: any
|
||||
): Q.Promise<any> {
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.scripts.trigger(trigger.id)
|
||||
.delete()
|
||||
);
|
||||
}
|
||||
|
||||
export function readCollectionQuotaInfo(
|
||||
collection: ViewModels.Collection,
|
||||
options: any
|
||||
): Q.Promise<DataModels.CollectionQuotaInfo> {
|
||||
options = options || {};
|
||||
options.populateQuotaInfo = true;
|
||||
options.initialHeaders = options.initialHeaders || {};
|
||||
options.initialHeaders[Constants.HttpHeaders.populatePartitionStatistics] = true;
|
||||
|
||||
return Q(
|
||||
client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.read(options)
|
||||
// TODO any needed because SDK does not properly type response.resource.statistics
|
||||
.then((response: any) => {
|
||||
let quota: DataModels.CollectionQuotaInfo = HeadersUtility.getQuota(response.headers);
|
||||
quota["usageSizeInKB"] = response.resource.statistics.reduce(
|
||||
(
|
||||
previousValue: number,
|
||||
currentValue: DataModels.Statistic,
|
||||
currentIndex: number,
|
||||
array: DataModels.Statistic[]
|
||||
) => {
|
||||
return previousValue + currentValue.sizeInKB;
|
||||
},
|
||||
0
|
||||
);
|
||||
quota["numPartitions"] = response.resource.statistics.length;
|
||||
quota["uniqueKeyPolicy"] = collection.uniqueKeyPolicy; // TODO: Remove after refactoring (#119617)
|
||||
return quota;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function readOffers(options: any): Q.Promise<DataModels.Offer[]> {
|
||||
if (options.isServerless) {
|
||||
return Q([]); // Reading offers is not supported for serverless accounts
|
||||
}
|
||||
|
||||
try {
|
||||
if (configContext.platform === Platform.Portal) {
|
||||
return sendCachedDataMessage<DataModels.Offer[]>(MessageTypes.AllOffers, [
|
||||
(<any>window).dataExplorer.databaseAccount().id,
|
||||
Constants.ClientDefaults.portalCacheTimeoutMs
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
// If error getting cached Offers, continue on and read via SDK
|
||||
}
|
||||
return Q(
|
||||
client()
|
||||
.offers.readAll()
|
||||
.fetchAll()
|
||||
.then(response => response.resources)
|
||||
.catch(error => {
|
||||
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too.
|
||||
if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function readOffer(requestedResource: DataModels.Offer, options: any): Q.Promise<DataModels.OfferWithHeaders> {
|
||||
options = options || {};
|
||||
options.initialHeaders = options.initialHeaders || {};
|
||||
if (!OfferUtils.isOfferV1(requestedResource)) {
|
||||
options.initialHeaders[Constants.HttpHeaders.populateCollectionThroughputInfo] = true;
|
||||
}
|
||||
|
||||
return Q(
|
||||
client()
|
||||
.offer(requestedResource.id)
|
||||
.read(options)
|
||||
.then(response => ({ ...response.resource, headers: response.headers }))
|
||||
);
|
||||
}
|
||||
|
||||
export function getOrCreateDatabaseAndCollection(
|
||||
request: DataModels.CreateDatabaseAndCollectionRequest,
|
||||
options: any
|
||||
): Q.Promise<DataModels.Collection> {
|
||||
const databaseOptions: any = options && _.omit(options, "sharedOfferThroughput");
|
||||
const {
|
||||
databaseId,
|
||||
databaseLevelThroughput,
|
||||
collectionId,
|
||||
partitionKey,
|
||||
indexingPolicy,
|
||||
uniqueKeyPolicy,
|
||||
offerThroughput,
|
||||
analyticalStorageTtl,
|
||||
hasAutoPilotV2FeatureFlag
|
||||
} = request;
|
||||
|
||||
const createBody: DatabaseRequest = {
|
||||
id: databaseId
|
||||
};
|
||||
|
||||
// TODO: replace when SDK support autopilot
|
||||
const initialHeaders = request.autoPilot
|
||||
? !hasAutoPilotV2FeatureFlag
|
||||
? {
|
||||
[Constants.HttpHeaders.autoPilotThroughputSDK]: JSON.stringify({
|
||||
maxThroughput: request.autoPilot.maxThroughput
|
||||
})
|
||||
}
|
||||
: {
|
||||
[Constants.HttpHeaders.autoPilotTier]: request.autoPilot.autopilotTier
|
||||
}
|
||||
: undefined;
|
||||
if (databaseLevelThroughput) {
|
||||
if (request.autoPilot) {
|
||||
databaseOptions.initialHeaders = initialHeaders;
|
||||
}
|
||||
createBody.throughput = offerThroughput;
|
||||
}
|
||||
|
||||
return Q(
|
||||
client()
|
||||
.databases.createIfNotExists(createBody, databaseOptions)
|
||||
.then(response => {
|
||||
return response.database.containers.create(
|
||||
{
|
||||
id: collectionId,
|
||||
partitionKey: (partitionKey || undefined) as PartitionKeyDefinition,
|
||||
indexingPolicy: indexingPolicy ? indexingPolicy : undefined,
|
||||
uniqueKeyPolicy: uniqueKeyPolicy ? uniqueKeyPolicy : undefined,
|
||||
analyticalStorageTtl: analyticalStorageTtl,
|
||||
throughput: databaseLevelThroughput || request.autoPilot ? undefined : offerThroughput
|
||||
} as ContainerRequest, // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
|
||||
{
|
||||
initialHeaders: databaseLevelThroughput ? undefined : initialHeaders
|
||||
}
|
||||
);
|
||||
})
|
||||
.then(containerResponse => containerResponse.resource as DataModels.Collection)
|
||||
.finally(() => refreshCachedResources(options))
|
||||
);
|
||||
}
|
||||
|
||||
export function refreshCachedOffers(): Q.Promise<void> {
|
||||
if (configContext.platform === Platform.Portal) {
|
||||
return sendCachedDataMessage(MessageTypes.RefreshOffers, []);
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import Q from "q";
|
||||
import * as Constants from "./Constants";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ErrorParserUtility from "./ErrorParserUtility";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import Q from "q";
|
||||
import { ConflictDefinition, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import * as DataAccessUtilityBase from "./DataAccessUtilityBase";
|
||||
import * as Logger from "./Logger";
|
||||
import { MinimalQueryIterator, nextPage } from "./IteratorUtilities";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||
import * as Constants from "./Constants";
|
||||
import { sendNotificationForError } from "./dataAccess/sendNotificationForError";
|
||||
import * as DataAccessUtilityBase from "./DataAccessUtilityBase";
|
||||
import { MinimalQueryIterator, nextPage } from "./IteratorUtilities";
|
||||
import * as Logger from "./Logger";
|
||||
|
||||
// TODO: Log all promise resolutions and errors with verbosity levels
|
||||
export function queryDocuments(
|
||||
@@ -41,6 +43,121 @@ export function getEntityName() {
|
||||
return "item";
|
||||
}
|
||||
|
||||
export function readStoredProcedures(
|
||||
collection: ViewModels.Collection,
|
||||
options: any = {}
|
||||
): Q.Promise<DataModels.StoredProcedure[]> {
|
||||
var deferred = Q.defer<DataModels.StoredProcedure[]>();
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Querying stored procedures for container ${collection.id()}`
|
||||
);
|
||||
DataAccessUtilityBase.readStoredProcedures(collection, options)
|
||||
.then(
|
||||
(storedProcedures: DataModels.StoredProcedure[]) => {
|
||||
deferred.resolve(storedProcedures);
|
||||
},
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to query stored procedures for container ${collection.id()}: ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "ReadStoredProcedures", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function readStoredProcedure(
|
||||
collection: ViewModels.Collection,
|
||||
requestedResource: DataModels.Resource,
|
||||
options?: any
|
||||
): Q.Promise<DataModels.StoredProcedure> {
|
||||
return DataAccessUtilityBase.readStoredProcedure(collection, requestedResource, options);
|
||||
}
|
||||
|
||||
export function readUserDefinedFunctions(
|
||||
collection: ViewModels.Collection,
|
||||
options: any = {}
|
||||
): Q.Promise<DataModels.UserDefinedFunction[]> {
|
||||
var deferred = Q.defer<DataModels.UserDefinedFunction[]>();
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Querying user defined functions for collection ${collection.id()}`
|
||||
);
|
||||
DataAccessUtilityBase.readUserDefinedFunctions(collection, options)
|
||||
.then(
|
||||
(userDefinedFunctions: DataModels.UserDefinedFunction[]) => {
|
||||
deferred.resolve(userDefinedFunctions);
|
||||
},
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to query user defined functions for container ${collection.id()}: ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "ReadUDFs", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function readUserDefinedFunction(
|
||||
collection: ViewModels.Collection,
|
||||
requestedResource: DataModels.Resource,
|
||||
options: any
|
||||
): Q.Promise<DataModels.UserDefinedFunction> {
|
||||
return DataAccessUtilityBase.readUserDefinedFunction(collection, requestedResource, options);
|
||||
}
|
||||
|
||||
export function readTriggers(collection: ViewModels.Collection, options: any): Q.Promise<DataModels.Trigger[]> {
|
||||
var deferred = Q.defer<DataModels.Trigger[]>();
|
||||
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Querying triggers for container ${collection.id()}`
|
||||
);
|
||||
DataAccessUtilityBase.readTriggers(collection, options)
|
||||
.then(
|
||||
(triggers: DataModels.Trigger[]) => {
|
||||
deferred.resolve(triggers);
|
||||
},
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to query triggers for container ${collection.id()}: ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "ReadTriggers", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function readTrigger(
|
||||
collection: ViewModels.Collection,
|
||||
requestedResource: DataModels.Resource,
|
||||
options?: any
|
||||
): Q.Promise<DataModels.Trigger> {
|
||||
return DataAccessUtilityBase.readTrigger(collection, requestedResource, options);
|
||||
}
|
||||
|
||||
export function executeStoredProcedure(
|
||||
collection: ViewModels.Collection,
|
||||
storedProcedure: StoredProcedure,
|
||||
@@ -49,17 +166,22 @@ export function executeStoredProcedure(
|
||||
): Q.Promise<any> {
|
||||
var deferred = Q.defer<any>();
|
||||
|
||||
const clearMessage = logConsoleProgress(`Executing stored procedure ${storedProcedure.id()}`);
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Executing stored procedure ${storedProcedure.id()}`
|
||||
);
|
||||
DataAccessUtilityBase.executeStoredProcedure(collection, storedProcedure, partitionKeyValue, params)
|
||||
.then(
|
||||
(response: any) => {
|
||||
deferred.resolve(response);
|
||||
logConsoleInfo(
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Finished executing stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}`
|
||||
);
|
||||
},
|
||||
(error: any) => {
|
||||
logConsoleError(
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to execute stored procedure ${storedProcedure.id()} for container ${storedProcedure.collection.id()}: ${JSON.stringify(
|
||||
error
|
||||
)}`
|
||||
@@ -70,7 +192,7 @@ export function executeStoredProcedure(
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
@@ -84,23 +206,32 @@ export function queryDocumentsPage(
|
||||
): Q.Promise<ViewModels.QueryResults> {
|
||||
var deferred = Q.defer<ViewModels.QueryResults>();
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`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}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`
|
||||
);
|
||||
deferred.resolve(result);
|
||||
},
|
||||
(error: any) => {
|
||||
logConsoleError(`Failed to query ${entityName} for container ${resourceName}: ${JSON.stringify(error)}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to query ${entityName} for container ${resourceName}: ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "QueryDocumentsPage", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
@@ -109,21 +240,27 @@ export function queryDocumentsPage(
|
||||
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()}`);
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Reading ${entityName} ${documentId.id()}`
|
||||
);
|
||||
DataAccessUtilityBase.readDocument(collection, documentId)
|
||||
.then(
|
||||
(document: any) => {
|
||||
deferred.resolve(document);
|
||||
},
|
||||
(error: any) => {
|
||||
logConsoleError(`Failed to read ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to read ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "ReadDocument", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
@@ -136,22 +273,31 @@ export function updateDocument(
|
||||
): Q.Promise<any> {
|
||||
var deferred = Q.defer<any>();
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Updating ${entityName} ${documentId.id()}`);
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Updating ${entityName} ${documentId.id()}`
|
||||
);
|
||||
DataAccessUtilityBase.updateDocument(collection, documentId, newDocument)
|
||||
.then(
|
||||
(updatedDocument: any) => {
|
||||
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully updated ${entityName} ${documentId.id()}`
|
||||
);
|
||||
deferred.resolve(updatedDocument);
|
||||
},
|
||||
(error: any) => {
|
||||
logConsoleError(`Failed to update ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to update ${entityName} ${documentId.id()}: ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "UpdateDocument", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
@@ -163,15 +309,24 @@ export function updateOffer(
|
||||
options: RequestOptions
|
||||
): Q.Promise<DataModels.Offer> {
|
||||
var deferred = Q.defer<any>();
|
||||
const clearMessage = logConsoleProgress(`Updating offer for resource ${offer.resource}`);
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Updating offer for resource ${offer.resource}`
|
||||
);
|
||||
DataAccessUtilityBase.updateOffer(offer, newOffer, options)
|
||||
.then(
|
||||
(replacedOffer: DataModels.Offer) => {
|
||||
logConsoleInfo(`Successfully updated offer for resource ${offer.resource}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully updated offer for resource ${offer.resource}`
|
||||
);
|
||||
deferred.resolve(replacedOffer);
|
||||
},
|
||||
(error: any) => {
|
||||
logConsoleError(`Error updating offer for resource ${offer.resource}: ${JSON.stringify(error)}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error updating offer for resource ${offer.resource}: ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(
|
||||
JSON.stringify({
|
||||
oldOffer: offer,
|
||||
@@ -186,7 +341,108 @@ export function updateOffer(
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function updateStoredProcedure(
|
||||
collection: ViewModels.Collection,
|
||||
storedProcedure: DataModels.StoredProcedure,
|
||||
options?: any
|
||||
): Q.Promise<DataModels.StoredProcedure> {
|
||||
var deferred = Q.defer<any>();
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Updating stored procedure ${storedProcedure.id}`
|
||||
);
|
||||
DataAccessUtilityBase.updateStoredProcedure(collection, storedProcedure, options)
|
||||
.then(
|
||||
(updatedStoredProcedure: DataModels.StoredProcedure) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully updated stored procedure ${storedProcedure.id}`
|
||||
);
|
||||
deferred.resolve(updatedStoredProcedure);
|
||||
},
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while updating stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "UpdateStoredProcedure", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function updateUserDefinedFunction(
|
||||
collection: ViewModels.Collection,
|
||||
userDefinedFunction: DataModels.UserDefinedFunction,
|
||||
options: any = {}
|
||||
): Q.Promise<DataModels.UserDefinedFunction> {
|
||||
var deferred = Q.defer<any>();
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Updating user defined function ${userDefinedFunction.id}`
|
||||
);
|
||||
DataAccessUtilityBase.updateUserDefinedFunction(collection, userDefinedFunction, options)
|
||||
.then(
|
||||
(updatedUserDefinedFunction: DataModels.UserDefinedFunction) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully updated user defined function ${userDefinedFunction.id}`
|
||||
);
|
||||
deferred.resolve(updatedUserDefinedFunction);
|
||||
},
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while updating user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "UpdateUDF", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function updateTrigger(
|
||||
collection: ViewModels.Collection,
|
||||
trigger: DataModels.Trigger
|
||||
): Q.Promise<DataModels.Trigger> {
|
||||
var deferred = Q.defer<any>();
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, `Updating trigger ${trigger.id}`);
|
||||
DataAccessUtilityBase.updateTrigger(collection, trigger)
|
||||
.then(
|
||||
(updatedTrigger: DataModels.Trigger) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Updated trigger ${trigger.id}`);
|
||||
deferred.resolve(updatedTrigger);
|
||||
},
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while updating trigger ${trigger.id}:\n ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "UpdateTrigger", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
@@ -195,15 +451,22 @@ export function updateOffer(
|
||||
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()}`);
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Creating new ${entityName} for container ${collection.id()}`
|
||||
);
|
||||
DataAccessUtilityBase.createDocument(collection, newDocument)
|
||||
.then(
|
||||
(savedDocument: any) => {
|
||||
logConsoleInfo(`Successfully created new ${entityName} for container ${collection.id()}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully created new ${entityName} for container ${collection.id()}`
|
||||
);
|
||||
deferred.resolve(savedDocument);
|
||||
},
|
||||
(error: any) => {
|
||||
logConsoleError(
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while creating new ${entityName} for container ${collection.id()}:\n ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "CreateDocument", error.code);
|
||||
@@ -212,7 +475,115 @@ export function createDocument(collection: ViewModels.CollectionBase, newDocumen
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function createStoredProcedure(
|
||||
collection: ViewModels.Collection,
|
||||
newStoredProcedure: DataModels.StoredProcedure,
|
||||
options?: any
|
||||
): Q.Promise<DataModels.StoredProcedure> {
|
||||
var deferred = Q.defer<any>();
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Creating stored procedure for container ${collection.id()}`
|
||||
);
|
||||
DataAccessUtilityBase.createStoredProcedure(collection, newStoredProcedure, options)
|
||||
.then(
|
||||
(createdStoredProcedure: DataModels.StoredProcedure) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully created stored procedure for container ${collection.id()}`
|
||||
);
|
||||
deferred.resolve(createdStoredProcedure);
|
||||
},
|
||||
error => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while creating stored procedure for container ${collection.id()}:\n ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "CreateStoredProcedure", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function createUserDefinedFunction(
|
||||
collection: ViewModels.Collection,
|
||||
newUserDefinedFunction: DataModels.UserDefinedFunction,
|
||||
options?: any
|
||||
): Q.Promise<DataModels.UserDefinedFunction> {
|
||||
var deferred = Q.defer<any>();
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Creating user defined function for container ${collection.id()}`
|
||||
);
|
||||
DataAccessUtilityBase.createUserDefinedFunction(collection, newUserDefinedFunction, options)
|
||||
.then(
|
||||
(createdUserDefinedFunction: DataModels.UserDefinedFunction) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully created user defined function for container ${collection.id()}`
|
||||
);
|
||||
deferred.resolve(createdUserDefinedFunction);
|
||||
},
|
||||
error => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while creating user defined function for container ${collection.id()}:\n ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "CreateUDF", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function createTrigger(
|
||||
collection: ViewModels.Collection,
|
||||
newTrigger: DataModels.Trigger,
|
||||
options: any = {}
|
||||
): Q.Promise<DataModels.Trigger> {
|
||||
var deferred = Q.defer<any>();
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Creating trigger for container ${collection.id()}`
|
||||
);
|
||||
DataAccessUtilityBase.createTrigger(collection, newTrigger, options)
|
||||
.then(
|
||||
(createdTrigger: DataModels.Trigger) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully created trigger for container ${collection.id()}`
|
||||
);
|
||||
deferred.resolve(createdTrigger);
|
||||
},
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while creating trigger for container ${collection.id()}:\n ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "CreateTrigger", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
@@ -221,22 +592,31 @@ export function createDocument(collection: ViewModels.CollectionBase, newDocumen
|
||||
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()}`);
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Deleting ${entityName} ${documentId.id()}`
|
||||
);
|
||||
DataAccessUtilityBase.deleteDocument(collection, documentId)
|
||||
.then(
|
||||
(response: any) => {
|
||||
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully deleted ${entityName} ${documentId.id()}`
|
||||
);
|
||||
deferred.resolve(response);
|
||||
},
|
||||
(error: any) => {
|
||||
logConsoleError(`Error while deleting ${entityName} ${documentId.id()}:\n ${JSON.stringify(error)}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while deleting ${entityName} ${documentId.id()}:\n ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "DeleteDocument", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
@@ -249,22 +629,134 @@ export function deleteConflict(
|
||||
): Q.Promise<any> {
|
||||
var deferred = Q.defer<any>();
|
||||
|
||||
const clearMessage = logConsoleProgress(`Deleting conflict ${conflictId.id()}`);
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Deleting conflict ${conflictId.id()}`
|
||||
);
|
||||
DataAccessUtilityBase.deleteConflict(collection, conflictId, options)
|
||||
.then(
|
||||
(response: any) => {
|
||||
logConsoleInfo(`Successfully deleted conflict ${conflictId.id()}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully deleted conflict ${conflictId.id()}`
|
||||
);
|
||||
deferred.resolve(response);
|
||||
},
|
||||
(error: any) => {
|
||||
logConsoleError(`Error while deleting conflict ${conflictId.id()}:\n ${JSON.stringify(error)}`);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while deleting conflict ${conflictId.id()}:\n ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "DeleteConflict", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
clearMessage();
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function deleteStoredProcedure(
|
||||
collection: ViewModels.Collection,
|
||||
storedProcedure: DataModels.StoredProcedure,
|
||||
options: any = {}
|
||||
): Q.Promise<DataModels.StoredProcedure> {
|
||||
var deferred = Q.defer<any>();
|
||||
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Deleting stored procedure ${storedProcedure.id}`
|
||||
);
|
||||
DataAccessUtilityBase.deleteStoredProcedure(collection, storedProcedure, options)
|
||||
.then(
|
||||
(response: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully deleted stored procedure ${storedProcedure.id}`
|
||||
);
|
||||
deferred.resolve(response);
|
||||
},
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while deleting stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "DeleteStoredProcedure", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function deleteUserDefinedFunction(
|
||||
collection: ViewModels.Collection,
|
||||
userDefinedFunction: DataModels.UserDefinedFunction,
|
||||
options: any = {}
|
||||
): Q.Promise<DataModels.UserDefinedFunction> {
|
||||
var deferred = Q.defer<any>();
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Deleting user defined function ${userDefinedFunction.id}`
|
||||
);
|
||||
DataAccessUtilityBase.deleteUserDefinedFunction(collection, userDefinedFunction, options)
|
||||
.then(
|
||||
(response: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully deleted user defined function ${userDefinedFunction.id}`
|
||||
);
|
||||
deferred.resolve(response);
|
||||
},
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while deleting user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "DeleteUDF", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function deleteTrigger(
|
||||
collection: ViewModels.Collection,
|
||||
trigger: DataModels.Trigger,
|
||||
options: any = {}
|
||||
): Q.Promise<DataModels.Trigger> {
|
||||
var deferred = Q.defer<any>();
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, `Deleting trigger ${trigger.id}`);
|
||||
DataAccessUtilityBase.deleteTrigger(collection, trigger, options)
|
||||
.then(
|
||||
(response: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully deleted trigger ${trigger.id}`);
|
||||
deferred.resolve(response);
|
||||
},
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while deleting trigger ${trigger.id}:\n ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "DeleteTrigger", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
@@ -277,3 +769,124 @@ export function refreshCachedResources(options: any = {}): Q.Promise<void> {
|
||||
export function refreshCachedOffers(): Q.Promise<void> {
|
||||
return DataAccessUtilityBase.refreshCachedOffers();
|
||||
}
|
||||
|
||||
export function readCollectionQuotaInfo(
|
||||
collection: ViewModels.Collection,
|
||||
options?: any
|
||||
): Q.Promise<DataModels.CollectionQuotaInfo> {
|
||||
var deferred = Q.defer<DataModels.CollectionQuotaInfo>();
|
||||
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Querying quota info for container ${collection.id}`
|
||||
);
|
||||
DataAccessUtilityBase.readCollectionQuotaInfo(collection, options)
|
||||
.then(
|
||||
(quota: DataModels.CollectionQuotaInfo) => {
|
||||
deferred.resolve(quota);
|
||||
},
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while querying quota info for container ${collection.id}:\n ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "ReadCollectionQuotaInfo", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function readOffers(options: any = {}): Q.Promise<DataModels.Offer[]> {
|
||||
var deferred = Q.defer<DataModels.Offer[]>();
|
||||
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying offers");
|
||||
DataAccessUtilityBase.readOffers(options)
|
||||
.then(
|
||||
(offers: DataModels.Offer[]) => {
|
||||
deferred.resolve(offers);
|
||||
},
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while querying offers:\n ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "ReadOffers", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function readOffer(
|
||||
requestedResource: DataModels.Offer,
|
||||
options: any = {}
|
||||
): Q.Promise<DataModels.OfferWithHeaders> {
|
||||
var deferred = Q.defer<DataModels.OfferWithHeaders>();
|
||||
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying offer");
|
||||
DataAccessUtilityBase.readOffer(requestedResource, options)
|
||||
.then(
|
||||
(offer: DataModels.OfferWithHeaders) => {
|
||||
deferred.resolve(offer);
|
||||
},
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while querying offer:\n ${JSON.stringify(error)}`
|
||||
);
|
||||
Logger.logError(JSON.stringify(error), "ReadOffer", error.code);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
export function getOrCreateDatabaseAndCollection(
|
||||
request: DataModels.CreateDatabaseAndCollectionRequest,
|
||||
options: any = {}
|
||||
): Q.Promise<DataModels.Collection> {
|
||||
const deferred: Q.Deferred<DataModels.Collection> = Q.defer<DataModels.Collection>();
|
||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Creating a new container ${request.collectionId} for database ${request.databaseId}`
|
||||
);
|
||||
|
||||
DataAccessUtilityBase.getOrCreateDatabaseAndCollection(request, options)
|
||||
.then(
|
||||
(collection: DataModels.Collection) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully created container ${request.collectionId}`
|
||||
);
|
||||
deferred.resolve(collection);
|
||||
},
|
||||
(error: any) => {
|
||||
const sanitizedError = ErrorParserUtility.replaceKnownError(JSON.stringify(error));
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while creating container ${request.collectionId}:\n ${sanitizedError}`
|
||||
);
|
||||
sendNotificationForError(error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
)
|
||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,49 @@
|
||||
import * as Constants from "../Common/Constants";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { StringUtils } from "../Utils/StringUtils";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
|
||||
export default class EnvironmentUtility {
|
||||
public static getMongoBackendEndpoint(serverId: string, location: string, extensionEndpoint: string = ""): string {
|
||||
const defaultEnvironment: string = "default";
|
||||
const defaultLocation: string = "default";
|
||||
let environment: string = serverId;
|
||||
const endpointType: Constants.MongoBackendEndpointType =
|
||||
Constants.MongoBackend.endpointsByEnvironment[environment] ||
|
||||
Constants.MongoBackend.endpointsByEnvironment[defaultEnvironment];
|
||||
if (endpointType === Constants.MongoBackendEndpointType.local) {
|
||||
return `${extensionEndpoint}${Constants.MongoBackend.localhostEndpoint}`;
|
||||
}
|
||||
|
||||
const normalizedLocation = EnvironmentUtility.normalizeRegionName(location);
|
||||
return (
|
||||
Constants.MongoBackend.endpointsByRegion[normalizedLocation] ||
|
||||
Constants.MongoBackend.endpointsByRegion[defaultLocation]
|
||||
);
|
||||
}
|
||||
|
||||
public static isAadUser(): boolean {
|
||||
return window.authType === AuthType.AAD;
|
||||
}
|
||||
|
||||
public static getCassandraBackendEndpoint(explorer: Explorer): string {
|
||||
const defaultLocation: string = "default";
|
||||
const location: string = EnvironmentUtility.normalizeRegionName(explorer.databaseAccount().location);
|
||||
return (
|
||||
Constants.CassandraBackend.endpointsByRegion[location] ||
|
||||
Constants.CassandraBackend.endpointsByRegion[defaultLocation]
|
||||
);
|
||||
}
|
||||
|
||||
public static normalizeArmEndpointUri(uri: string): string {
|
||||
if (uri && uri.slice(-1) !== "/") {
|
||||
return `${uri}/`;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
private static normalizeRegionName(region: string): string {
|
||||
return region && StringUtils.stripSpacesFromString(region.toLocaleLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Q from "q";
|
||||
import * as MessageHandler from "./MessageHandler";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
|
||||
describe("Message Handler", () => {
|
||||
it("should handle cached message", async () => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import Q from "q";
|
||||
import * as _ from "underscore";
|
||||
import * as Constants from "./Constants";
|
||||
import { getDataExplorerWindow } from "../Utils/WindowUtils";
|
||||
|
||||
export interface CachedDataPromise<T> {
|
||||
deferred: Q.Deferred<T>;
|
||||
@@ -49,14 +48,12 @@ export function sendCachedDataMessage<TResponseDataModel>(
|
||||
|
||||
export function sendMessage(data: any): void {
|
||||
if (canSendMessage()) {
|
||||
// We try to find data explorer window first, then fallback to current window
|
||||
const portalChildWindow = getDataExplorerWindow(window) || window;
|
||||
portalChildWindow.parent.postMessage(
|
||||
window.parent.postMessage(
|
||||
{
|
||||
signature: "pcIframe",
|
||||
data: data
|
||||
},
|
||||
portalChildWindow.document.referrer
|
||||
window.document.referrer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { AuthType } from "../AuthType";
|
||||
import { resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||
import { configContext, resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
||||
import {
|
||||
deleteDocument,
|
||||
getEndpoint,
|
||||
queryDocuments,
|
||||
readDocument,
|
||||
updateDocument,
|
||||
_createMongoCollectionWithARM
|
||||
} from "./MongoProxyClient";
|
||||
jest.mock("../ResourceProvider/ResourceProviderClient.ts");
|
||||
|
||||
const databaseId = "testDB";
|
||||
@@ -236,20 +244,74 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
|
||||
it("returns a production endpoint", () => {
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getEndpoint(databaseAccount as DatabaseAccount);
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||
});
|
||||
|
||||
it("returns a development endpoint", () => {
|
||||
updateConfigContext({ MONGO_BACKEND_ENDPOINT: "https://localhost:1234" });
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getEndpoint(databaseAccount as DatabaseAccount);
|
||||
expect(endpoint).toEqual("https://localhost:1234/api/mongo/explorer");
|
||||
});
|
||||
|
||||
it("returns a guest endpoint", () => {
|
||||
window.authType = AuthType.EncryptedToken;
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getEndpoint(databaseAccount as DatabaseAccount);
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createMongoCollectionWithARM", () => {
|
||||
it("should create a collection with autopilot when autopilot is selected + shared throughput is false", () => {
|
||||
const resourceProviderClientPutAsyncSpy = jest.spyOn(ResourceProviderClient.prototype, "putAsync");
|
||||
const properties = {
|
||||
pk: "state",
|
||||
coll: "abc-collection",
|
||||
cd: true,
|
||||
db: "a1-db",
|
||||
st: false,
|
||||
sid: "a2",
|
||||
rg: "c1",
|
||||
dba: "main",
|
||||
is: false
|
||||
};
|
||||
_createMongoCollectionWithARM("management.azure.com", properties, { "x-ms-cosmos-offer-autopilot-tier": "1" });
|
||||
expect(resourceProviderClientPutAsyncSpy).toHaveBeenCalledWith(
|
||||
"subscriptions/a2/resourceGroups/c1/providers/Microsoft.DocumentDB/databaseAccounts/foo/mongodbDatabases/a1-db/collections/abc-collection",
|
||||
"2020-04-01",
|
||||
{
|
||||
properties: {
|
||||
options: { "x-ms-cosmos-offer-autopilot-tier": "1" },
|
||||
resource: { id: "abc-collection" }
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
it("should create a collection with provisioned throughput when provisioned throughput is selected + shared throughput is false", () => {
|
||||
const resourceProviderClientPutAsyncSpy = jest.spyOn(ResourceProviderClient.prototype, "putAsync");
|
||||
const properties = {
|
||||
pk: "state",
|
||||
coll: "abc-collection",
|
||||
cd: true,
|
||||
db: "a1-db",
|
||||
st: false,
|
||||
sid: "a2",
|
||||
rg: "c1",
|
||||
dba: "main",
|
||||
is: false,
|
||||
offerThroughput: 400
|
||||
};
|
||||
_createMongoCollectionWithARM("management.azure.com", properties, undefined);
|
||||
expect(resourceProviderClientPutAsyncSpy).toHaveBeenCalledWith(
|
||||
"subscriptions/a2/resourceGroups/c1/providers/Microsoft.DocumentDB/databaseAccounts/foo/mongodbDatabases/a1-db/collections/abc-collection",
|
||||
"2020-04-01",
|
||||
{
|
||||
properties: {
|
||||
options: { throughput: "400" },
|
||||
resource: { id: "abc-collection" }
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||
import queryString from "querystring";
|
||||
import { AuthType } from "../AuthType";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import * as DataExplorerConstants from "../Common/Constants";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { ResourceProviderClient } from "../ResourceProvider/ResourceProviderClient";
|
||||
import { AddDbUtilities } from "../Shared/AddDatabaseUtility";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||
import { userContext } from "../UserContext";
|
||||
import EnvironmentUtility from "./EnvironmentUtility";
|
||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||
import { sendMessage } from "./MessageHandler";
|
||||
|
||||
@@ -77,7 +82,7 @@ export function queryDocuments(
|
||||
collection && collection.partitionKey && !collection.partitionKey.systemKey ? collection.partitionKeyProperty : ""
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint() || "";
|
||||
const endpoint = getEndpoint(databaseAccount) || "";
|
||||
|
||||
const headers = {
|
||||
...defaultHeaders,
|
||||
@@ -138,7 +143,7 @@ export function readDocument(
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getEndpoint(databaseAccount);
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
method: "GET",
|
||||
@@ -178,7 +183,7 @@ export function createDocument(
|
||||
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : ""
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getEndpoint(databaseAccount);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
|
||||
@@ -220,7 +225,7 @@ export function updateDocument(
|
||||
pk:
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
|
||||
};
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getEndpoint(databaseAccount);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
@@ -259,7 +264,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
||||
pk:
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey ? documentId.partitionKeyProperty : ""
|
||||
};
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getEndpoint(databaseAccount);
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
@@ -280,35 +285,43 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
||||
}
|
||||
|
||||
export function createMongoCollectionWithProxy(
|
||||
params: DataModels.CreateCollectionParams
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
offerThroughput: number,
|
||||
shardKey: string,
|
||||
createDatabase: boolean,
|
||||
sharedThroughput: boolean,
|
||||
isSharded: boolean,
|
||||
autopilotOptions?: DataModels.RpOptions
|
||||
): Promise<DataModels.Collection> {
|
||||
const databaseAccount = userContext.databaseAccount;
|
||||
const shardKey: string = params.partitionKey?.paths[0];
|
||||
const mongoParams: DataModels.MongoParameters = {
|
||||
const params: DataModels.MongoParameters = {
|
||||
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
|
||||
db: params.databaseId,
|
||||
coll: params.collectionId,
|
||||
db: databaseId,
|
||||
coll: collectionId,
|
||||
pk: shardKey,
|
||||
offerThroughput: params.offerThroughput,
|
||||
cd: params.createNewDatabase,
|
||||
st: params.databaseLevelThroughput,
|
||||
is: !!shardKey,
|
||||
offerThroughput,
|
||||
cd: createDatabase,
|
||||
st: sharedThroughput,
|
||||
is: isSharded,
|
||||
rid: "",
|
||||
rtype: "colls",
|
||||
sid: userContext.subscriptionId,
|
||||
rg: userContext.resourceGroup,
|
||||
dba: databaseAccount.name,
|
||||
isAutoPilot: !!params.autoPilotMaxThroughput,
|
||||
autoPilotThroughput: params.autoPilotMaxThroughput?.toString()
|
||||
isAutoPilot: false
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint();
|
||||
if (autopilotOptions) {
|
||||
params.isAutoPilot = true;
|
||||
params.autoPilotTier = autopilotOptions[Constants.HttpHeaders.autoPilotTier] as string;
|
||||
}
|
||||
|
||||
const endpoint = getEndpoint(databaseAccount);
|
||||
|
||||
return window
|
||||
.fetch(
|
||||
`${endpoint}/createCollection?${queryString.stringify(
|
||||
(mongoParams as unknown) as queryString.ParsedUrlQueryInput
|
||||
)}`,
|
||||
`${endpoint}/createCollection?${queryString.stringify((params as unknown) as queryString.ParsedUrlQueryInput)}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -322,13 +335,58 @@ export function createMongoCollectionWithProxy(
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
return errorHandling(response, "creating collection", mongoParams);
|
||||
return errorHandling(response, "creating collection", params);
|
||||
});
|
||||
}
|
||||
|
||||
export function getEndpoint(): string {
|
||||
export function createMongoCollectionWithARM(
|
||||
armEndpoint: string,
|
||||
databaseId: string,
|
||||
analyticalStorageTtl: number,
|
||||
collectionId: string,
|
||||
offerThroughput: number,
|
||||
shardKey: string,
|
||||
createDatabase: boolean,
|
||||
sharedThroughput: boolean,
|
||||
isSharded: boolean,
|
||||
additionalOptions?: DataModels.RpOptions
|
||||
): Promise<DataModels.CreateCollectionWithRpResponse> {
|
||||
const databaseAccount = userContext.databaseAccount;
|
||||
const params: DataModels.MongoParameters = {
|
||||
resourceUrl: databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint,
|
||||
db: databaseId,
|
||||
coll: collectionId,
|
||||
pk: shardKey,
|
||||
offerThroughput,
|
||||
cd: createDatabase,
|
||||
st: sharedThroughput,
|
||||
is: isSharded,
|
||||
rid: "",
|
||||
rtype: "colls",
|
||||
sid: userContext.subscriptionId,
|
||||
rg: userContext.resourceGroup,
|
||||
dba: databaseAccount.name,
|
||||
analyticalStorageTtl
|
||||
};
|
||||
|
||||
if (createDatabase) {
|
||||
return AddDbUtilities.createMongoDatabaseWithARM(
|
||||
armEndpoint,
|
||||
params,
|
||||
sharedThroughput ? additionalOptions : {}
|
||||
).then(() => {
|
||||
return _createMongoCollectionWithARM(armEndpoint, params, sharedThroughput ? {} : additionalOptions);
|
||||
});
|
||||
}
|
||||
return _createMongoCollectionWithARM(armEndpoint, params, additionalOptions);
|
||||
}
|
||||
|
||||
export function getEndpoint(databaseAccount: DataModels.DatabaseAccount): string {
|
||||
const serverId = window.dataExplorer.serverId();
|
||||
const extensionEndpoint = window.dataExplorer.extensionEndpoint();
|
||||
let url = (configContext.MONGO_BACKEND_ENDPOINT || extensionEndpoint) + "/api/mongo/explorer";
|
||||
let url = configContext.MONGO_BACKEND_ENDPOINT
|
||||
? configContext.MONGO_BACKEND_ENDPOINT + "/api/mongo/explorer"
|
||||
: EnvironmentUtility.getMongoBackendEndpoint(serverId, databaseAccount.location, extensionEndpoint);
|
||||
|
||||
if (window.authType === AuthType.EncryptedToken) {
|
||||
url = url.replace("api/mongo", "api/guest/mongo");
|
||||
@@ -355,3 +413,46 @@ async function errorHandling(response: Response, action: string, params: unknown
|
||||
export function getARMCreateCollectionEndpoint(params: DataModels.MongoParameters): string {
|
||||
return `subscriptions/${params.sid}/resourceGroups/${params.rg}/providers/Microsoft.DocumentDB/databaseAccounts/${userContext.databaseAccount.name}/mongodbDatabases/${params.db}/collections/${params.coll}`;
|
||||
}
|
||||
|
||||
export async function _createMongoCollectionWithARM(
|
||||
armEndpoint: string,
|
||||
params: DataModels.MongoParameters,
|
||||
rpOptions: DataModels.RpOptions
|
||||
): Promise<DataModels.CreateCollectionWithRpResponse> {
|
||||
const rpPayloadToCreateCollection: DataModels.MongoCreationRequest = {
|
||||
properties: {
|
||||
resource: {
|
||||
id: params.coll
|
||||
},
|
||||
options: {}
|
||||
}
|
||||
};
|
||||
|
||||
if (params.is) {
|
||||
rpPayloadToCreateCollection.properties.resource["shardKey"] = { [params.pk]: "Hash" };
|
||||
}
|
||||
|
||||
if (!params.st) {
|
||||
if (rpOptions) {
|
||||
rpPayloadToCreateCollection.properties.options = rpOptions;
|
||||
} else {
|
||||
rpPayloadToCreateCollection.properties.options["throughput"] =
|
||||
params.offerThroughput && params.offerThroughput.toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (params.analyticalStorageTtl) {
|
||||
rpPayloadToCreateCollection.properties.resource.analyticalStorageTtl = params.analyticalStorageTtl;
|
||||
}
|
||||
|
||||
try {
|
||||
return new ResourceProviderClient<DataModels.CreateCollectionWithRpResponse>(armEndpoint).putAsync(
|
||||
getARMCreateCollectionEndpoint(params),
|
||||
DataExplorerConstants.ArmApiVersions.publicVersion,
|
||||
rpPayloadToCreateCollection
|
||||
);
|
||||
} catch (response) {
|
||||
errorHandling(response, "creating collection", undefined);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,13 @@ import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import { QueryUtils } from "../Utils/QueryUtils";
|
||||
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
|
||||
import { userContext } from "../UserContext";
|
||||
import { createDocument, deleteDocument, queryDocuments, queryDocumentsPage } from "./DocumentClientUtilityBase";
|
||||
import { createCollection } from "./dataAccess/createCollection";
|
||||
import {
|
||||
createDocument,
|
||||
deleteDocument,
|
||||
getOrCreateDatabaseAndCollection,
|
||||
queryDocuments,
|
||||
queryDocumentsPage
|
||||
} from "./DocumentClientUtilityBase";
|
||||
import * as ErrorParserUtility from "./ErrorParserUtility";
|
||||
import * as Logger from "./Logger";
|
||||
|
||||
@@ -36,13 +41,12 @@ export class QueriesClient {
|
||||
ConsoleDataType.InProgress,
|
||||
"Setting up account for saving queries"
|
||||
);
|
||||
return createCollection({
|
||||
return getOrCreateDatabaseAndCollection({
|
||||
collectionId: SavedQueries.CollectionName,
|
||||
createNewDatabase: true,
|
||||
databaseId: SavedQueries.DatabaseName,
|
||||
partitionKey: QueriesClient.PartitionKey,
|
||||
offerThroughput: SavedQueries.OfferThroughput,
|
||||
databaseLevelThroughput: false
|
||||
databaseLevelThroughput: undefined
|
||||
})
|
||||
.then(
|
||||
(collection: DataModels.Collection) => {
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
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";
|
||||
import { armRequest } from "../../Utils/arm/request";
|
||||
import { client } from "../CosmosClient";
|
||||
import { createCollection, constructRpOptions } from "./createCollection";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
|
||||
describe("createCollection", () => {
|
||||
const createCollectionParams: CreateCollectionParams = {
|
||||
createNewDatabase: false,
|
||||
collectionId: "testContainer",
|
||||
databaseId: "testDatabase",
|
||||
databaseLevelThroughput: true,
|
||||
offerThroughput: 400
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
name: "test"
|
||||
} as DatabaseAccount,
|
||||
defaultExperience: DefaultAccountExperienceType.DocumentDB
|
||||
});
|
||||
});
|
||||
|
||||
it("should call ARM if logged in with AAD", async () => {
|
||||
window.authType = AuthType.AAD;
|
||||
await createCollection(createCollectionParams);
|
||||
expect(armRequest).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call SDK if not logged in with non-AAD method", async () => {
|
||||
window.authType = AuthType.MasterKey;
|
||||
(client as jest.Mock).mockReturnValue({
|
||||
databases: {
|
||||
createIfNotExists: () => {
|
||||
return {
|
||||
database: {
|
||||
containers: {
|
||||
create: () => ({})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
await createCollection(createCollectionParams);
|
||||
expect(client).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("constructRpOptions should return the correct options", () => {
|
||||
expect(constructRpOptions(createCollectionParams)).toEqual({});
|
||||
|
||||
const manualThroughputParams: CreateCollectionParams = {
|
||||
createNewDatabase: false,
|
||||
collectionId: "testContainer",
|
||||
databaseId: "testDatabase",
|
||||
databaseLevelThroughput: false,
|
||||
offerThroughput: 400
|
||||
};
|
||||
expect(constructRpOptions(manualThroughputParams)).toEqual({ throughput: 400 });
|
||||
|
||||
const autoPilotThroughputParams: CreateCollectionParams = {
|
||||
createNewDatabase: false,
|
||||
collectionId: "testContainer",
|
||||
databaseId: "testDatabase",
|
||||
databaseLevelThroughput: false,
|
||||
offerThroughput: 400,
|
||||
autoPilotMaxThroughput: 4000
|
||||
};
|
||||
expect(constructRpOptions(autoPilotThroughputParams)).toEqual({
|
||||
autoscaleSettings: {
|
||||
maxThroughput: 4000
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,384 +0,0 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ErrorParserUtility from "../ErrorParserUtility";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { ContainerResponse, DatabaseResponse } from "@azure/cosmos";
|
||||
import { ContainerRequest } from "@azure/cosmos/dist-esm/client/Container/ContainerRequest";
|
||||
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import * as ARMTypes from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import { client } from "../CosmosClient";
|
||||
import { createMongoCollectionWithProxy } from "../MongoProxyClient";
|
||||
import { createUpdateSqlContainer, getSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import {
|
||||
createUpdateCassandraTable,
|
||||
getCassandraTable
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||
import {
|
||||
createUpdateMongoDBCollection,
|
||||
getMongoDBCollection
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||
import {
|
||||
createUpdateGremlinGraph,
|
||||
getGremlinGraph
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||
import { createUpdateTable, getTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
||||
import { logConsoleProgress, logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { refreshCachedResources } from "../DataAccessUtilityBase";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { createDatabase } from "./createDatabase";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
|
||||
export const createCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||
let collection: DataModels.Collection;
|
||||
const clearMessage = logConsoleProgress(
|
||||
`Creating a new container ${params.collectionId} for database ${params.databaseId}`
|
||||
);
|
||||
try {
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
if (params.createNewDatabase) {
|
||||
const createDatabaseParams: DataModels.CreateDatabaseParams = {
|
||||
autoPilotMaxThroughput: params.autoPilotMaxThroughput,
|
||||
databaseId: params.databaseId,
|
||||
databaseLevelThroughput: params.databaseLevelThroughput,
|
||||
offerThroughput: params.offerThroughput
|
||||
};
|
||||
await createDatabase(createDatabaseParams);
|
||||
}
|
||||
collection = await createCollectionWithARM(params);
|
||||
} else if (userContext.defaultExperience === DefaultAccountExperienceType.MongoDB) {
|
||||
collection = await createMongoCollectionWithProxy(params);
|
||||
} else {
|
||||
collection = await createCollectionWithSDK(params);
|
||||
}
|
||||
} catch (error) {
|
||||
const sanitizedError = ErrorParserUtility.replaceKnownError(JSON.stringify(error));
|
||||
logConsoleError(`Error while creating container ${params.collectionId}:\n ${sanitizedError}`);
|
||||
logError(JSON.stringify(error), "CreateCollection", error.code);
|
||||
sendNotificationForError(error);
|
||||
clearMessage();
|
||||
throw error;
|
||||
}
|
||||
logConsoleInfo(`Successfully created container ${params.collectionId}`);
|
||||
await refreshCachedResources();
|
||||
clearMessage();
|
||||
return collection;
|
||||
};
|
||||
|
||||
const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||
const defaultExperience = userContext.defaultExperience;
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
return createSqlContainer(params);
|
||||
case DefaultAccountExperienceType.MongoDB:
|
||||
return createMongoCollection(params);
|
||||
case DefaultAccountExperienceType.Cassandra:
|
||||
return createCassandraTable(params);
|
||||
case DefaultAccountExperienceType.Graph:
|
||||
return createGraph(params);
|
||||
case DefaultAccountExperienceType.Table:
|
||||
return createTable(params);
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||
}
|
||||
};
|
||||
|
||||
const createSqlContainer = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||
try {
|
||||
const getResponse = await getSqlContainer(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
params.collectionId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(`Create container failed: container with id ${params.collectionId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
||||
const resource: ARMTypes.SqlContainerResource = {
|
||||
id: params.collectionId
|
||||
};
|
||||
if (params.analyticalStorageTtl) {
|
||||
resource.analyticalStorageTtl = params.analyticalStorageTtl;
|
||||
}
|
||||
if (params.indexingPolicy) {
|
||||
resource.indexingPolicy = params.indexingPolicy;
|
||||
}
|
||||
if (params.partitionKey) {
|
||||
resource.partitionKey = params.partitionKey;
|
||||
}
|
||||
if (params.uniqueKeyPolicy) {
|
||||
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
|
||||
}
|
||||
|
||||
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
|
||||
properties: {
|
||||
resource,
|
||||
options
|
||||
}
|
||||
};
|
||||
|
||||
const createResponse = await createUpdateSqlContainer(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
params.collectionId,
|
||||
rpPayload
|
||||
);
|
||||
return createResponse && (createResponse.properties.resource as DataModels.Collection);
|
||||
};
|
||||
|
||||
const createMongoCollection = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||
const mongoWildcardIndexOnAllFields: ARMTypes.MongoIndex[] = [{ key: { keys: ["$**"] } }, { key: { keys: ["_id"] } }];
|
||||
try {
|
||||
const getResponse = await getMongoDBCollection(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
params.collectionId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(`Create collection failed: collection with id ${params.collectionId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
||||
const resource: ARMTypes.MongoDBCollectionResource = {
|
||||
id: params.collectionId
|
||||
};
|
||||
if (params.analyticalStorageTtl) {
|
||||
resource.analyticalStorageTtl = params.analyticalStorageTtl;
|
||||
}
|
||||
if (params.partitionKey) {
|
||||
const partitionKeyPath: string = params.partitionKey.paths[0];
|
||||
resource.shardKey = { [partitionKeyPath]: "Hash" };
|
||||
}
|
||||
if (params.createMongoWildcardIndex) {
|
||||
resource.indexes = mongoWildcardIndexOnAllFields;
|
||||
}
|
||||
|
||||
const rpPayload: ARMTypes.MongoDBCollectionCreateUpdateParameters = {
|
||||
properties: {
|
||||
resource,
|
||||
options
|
||||
}
|
||||
};
|
||||
|
||||
const createResponse = await createUpdateMongoDBCollection(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
params.collectionId,
|
||||
rpPayload
|
||||
);
|
||||
|
||||
if (params.createMongoWildcardIndex) {
|
||||
TelemetryProcessor.trace(Action.CreateMongoCollectionWithWildcardIndex, ActionModifiers.Mark, {
|
||||
message: "Mongo Collection created with wildcard index on all fields."
|
||||
});
|
||||
}
|
||||
|
||||
return createResponse && (createResponse.properties.resource as DataModels.Collection);
|
||||
};
|
||||
|
||||
const createCassandraTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||
try {
|
||||
const getResponse = await getCassandraTable(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
params.collectionId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(`Create table failed: table with id ${params.collectionId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
||||
const resource: ARMTypes.CassandraTableResource = {
|
||||
id: params.collectionId
|
||||
};
|
||||
if (params.analyticalStorageTtl) {
|
||||
resource.analyticalStorageTtl = params.analyticalStorageTtl;
|
||||
}
|
||||
|
||||
const rpPayload: ARMTypes.CassandraTableCreateUpdateParameters = {
|
||||
properties: {
|
||||
resource,
|
||||
options
|
||||
}
|
||||
};
|
||||
|
||||
const createResponse = await createUpdateCassandraTable(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
params.collectionId,
|
||||
rpPayload
|
||||
);
|
||||
return createResponse && (createResponse.properties.resource as DataModels.Collection);
|
||||
};
|
||||
|
||||
const createGraph = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||
try {
|
||||
const getResponse = await getGremlinGraph(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
params.collectionId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(`Create graph failed: graph with id ${params.collectionId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
||||
const resource: ARMTypes.GremlinGraphResource = {
|
||||
id: params.collectionId
|
||||
};
|
||||
|
||||
if (params.indexingPolicy) {
|
||||
resource.indexingPolicy = params.indexingPolicy;
|
||||
}
|
||||
if (params.partitionKey) {
|
||||
resource.partitionKey = params.partitionKey;
|
||||
}
|
||||
if (params.uniqueKeyPolicy) {
|
||||
resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
|
||||
}
|
||||
|
||||
const rpPayload: ARMTypes.GremlinGraphCreateUpdateParameters = {
|
||||
properties: {
|
||||
resource,
|
||||
options
|
||||
}
|
||||
};
|
||||
|
||||
const createResponse = await createUpdateGremlinGraph(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
params.collectionId,
|
||||
rpPayload
|
||||
);
|
||||
return createResponse && (createResponse.properties.resource as DataModels.Collection);
|
||||
};
|
||||
|
||||
const createTable = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||
try {
|
||||
const getResponse = await getTable(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.collectionId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
throw new Error(`Create table failed: table with id ${params.collectionId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "NotFound") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const options: ARMTypes.CreateUpdateOptions = constructRpOptions(params);
|
||||
const resource: ARMTypes.TableResource = {
|
||||
id: params.collectionId
|
||||
};
|
||||
|
||||
const rpPayload: ARMTypes.TableCreateUpdateParameters = {
|
||||
properties: {
|
||||
resource,
|
||||
options
|
||||
}
|
||||
};
|
||||
|
||||
const createResponse = await createUpdateTable(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.collectionId,
|
||||
rpPayload
|
||||
);
|
||||
return createResponse && (createResponse.properties.resource as DataModels.Collection);
|
||||
};
|
||||
|
||||
export const constructRpOptions = (params: DataModels.CreateDatabaseParams): ARMTypes.CreateUpdateOptions => {
|
||||
if (params.databaseLevelThroughput) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (params.autoPilotMaxThroughput) {
|
||||
return {
|
||||
autoscaleSettings: {
|
||||
maxThroughput: params.autoPilotMaxThroughput
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
throughput: params.offerThroughput
|
||||
};
|
||||
};
|
||||
|
||||
const createCollectionWithSDK = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||
const createCollectionBody: ContainerRequest = {
|
||||
id: params.collectionId,
|
||||
partitionKey: params.partitionKey || undefined,
|
||||
indexingPolicy: params.indexingPolicy || undefined,
|
||||
uniqueKeyPolicy: params.uniqueKeyPolicy || undefined,
|
||||
analyticalStorageTtl: params.analyticalStorageTtl
|
||||
} as ContainerRequest; // TODO: remove cast when https://github.com/Azure/azure-cosmos-js/issues/423 is fixed
|
||||
const collectionOptions: RequestOptions = {};
|
||||
const createDatabaseBody: DatabaseRequest = { id: params.databaseId };
|
||||
|
||||
if (params.databaseLevelThroughput) {
|
||||
if (params.autoPilotMaxThroughput) {
|
||||
createDatabaseBody.maxThroughput = params.autoPilotMaxThroughput;
|
||||
} else {
|
||||
createDatabaseBody.throughput = params.offerThroughput;
|
||||
}
|
||||
} else {
|
||||
if (params.autoPilotMaxThroughput) {
|
||||
createCollectionBody.maxThroughput = params.autoPilotMaxThroughput;
|
||||
} else {
|
||||
createCollectionBody.throughput = params.offerThroughput;
|
||||
}
|
||||
}
|
||||
|
||||
const databaseResponse: DatabaseResponse = await client().databases.createIfNotExists(createDatabaseBody);
|
||||
const collectionResponse: ContainerResponse = await databaseResponse?.database.containers.create(
|
||||
createCollectionBody,
|
||||
collectionOptions
|
||||
);
|
||||
return collectionResponse?.resource as DataModels.Collection;
|
||||
};
|
||||
@@ -3,10 +3,8 @@ import { AuthType } from "../../AuthType";
|
||||
import { DatabaseResponse } from "@azure/cosmos";
|
||||
import { DatabaseRequest } from "@azure/cosmos/dist-esm/client/Database/DatabaseRequest";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import {
|
||||
CassandraKeyspaceCreateUpdateParameters,
|
||||
GremlinDatabaseCreateUpdateParameters,
|
||||
MongoDBDatabaseCreateUpdateParameters,
|
||||
SqlDatabaseCreateUpdateParameters,
|
||||
CreateUpdateOptions
|
||||
} from "../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
@@ -34,10 +32,11 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
|
||||
let database: DataModels.Database;
|
||||
const clearMessage = logConsoleProgress(`Creating a new database ${params.databaseId}`);
|
||||
try {
|
||||
if (userContext.defaultExperience === DefaultAccountExperienceType.Table) {
|
||||
throw new Error("Creating database resources is not allowed for tables accounts");
|
||||
}
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
if (
|
||||
window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
) {
|
||||
database = await createDatabaseWithARM(params);
|
||||
} else {
|
||||
database = await createDatabaseWithSDK(params);
|
||||
@@ -80,7 +79,7 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -116,7 +115,7 @@ async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Pro
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -126,7 +125,7 @@ async function createMongoDatabase(params: DataModels.CreateDatabaseParams): Pro
|
||||
}
|
||||
|
||||
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||
const rpPayload: MongoDBDatabaseCreateUpdateParameters = {
|
||||
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
||||
properties: {
|
||||
resource: {
|
||||
id: params.databaseId
|
||||
@@ -162,7 +161,7 @@ async function createCassandraKeyspace(params: DataModels.CreateDatabaseParams):
|
||||
}
|
||||
|
||||
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||
const rpPayload: CassandraKeyspaceCreateUpdateParameters = {
|
||||
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
||||
properties: {
|
||||
resource: {
|
||||
id: params.databaseId
|
||||
@@ -188,7 +187,7 @@ async function createGremlineDatabase(params: DataModels.CreateDatabaseParams):
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId
|
||||
);
|
||||
if (getResponse?.properties?.resource) {
|
||||
if (getResponse && getResponse.properties && getResponse.properties.resource) {
|
||||
throw new Error(`Create database failed: database with id ${params.databaseId} already exists`);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -198,7 +197,7 @@ async function createGremlineDatabase(params: DataModels.CreateDatabaseParams):
|
||||
}
|
||||
|
||||
const options: CreateUpdateOptions = constructRpOptions(params);
|
||||
const rpPayload: GremlinDatabaseCreateUpdateParameters = {
|
||||
const rpPayload: SqlDatabaseCreateUpdateParameters = {
|
||||
properties: {
|
||||
resource: {
|
||||
id: params.databaseId
|
||||
@@ -218,7 +217,8 @@ async function createGremlineDatabase(params: DataModels.CreateDatabaseParams):
|
||||
|
||||
async function createDatabaseWithSDK(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||
const createBody: DatabaseRequest = { id: params.databaseId };
|
||||
|
||||
const databaseOptions: RequestOptions = {};
|
||||
// TODO: replace when SDK support autopilot
|
||||
if (params.databaseLevelThroughput) {
|
||||
if (params.autoPilotMaxThroughput) {
|
||||
createBody.maxThroughput = params.autoPilotMaxThroughput;
|
||||
@@ -227,7 +227,7 @@ async function createDatabaseWithSDK(params: DataModels.CreateDatabaseParams): P
|
||||
}
|
||||
}
|
||||
|
||||
const response: DatabaseResponse = await client().databases.create(createBody);
|
||||
const response: DatabaseResponse = await client().databases.create(createBody, databaseOptions);
|
||||
return response.resource;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
|
||||
export async function createStoredProcedure(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
storedProcedure: StoredProcedureDefinition
|
||||
): Promise<StoredProcedureDefinition & Resource> {
|
||||
let createdStoredProcedure: StoredProcedureDefinition & Resource;
|
||||
const clearMessage = logConsoleProgress(`Creating stored procedure ${storedProcedure.id}`);
|
||||
try {
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.storedProcedures.create(storedProcedure);
|
||||
createdStoredProcedure = response.resource;
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while creating stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "CreateStoredProcedure", error.code);
|
||||
sendNotificationForError(error);
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return createdStoredProcedure;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Resource, TriggerDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
|
||||
export async function createTrigger(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
trigger: TriggerDefinition
|
||||
): Promise<TriggerDefinition & Resource> {
|
||||
let createdTrigger: TriggerDefinition & Resource;
|
||||
const clearMessage = logConsoleProgress(`Creating trigger ${trigger.id}`);
|
||||
try {
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.triggers.create(trigger);
|
||||
createdTrigger = response.resource;
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while creating trigger ${trigger.id}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "CreateTrigger", error.code);
|
||||
sendNotificationForError(error);
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return createdTrigger;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
|
||||
export async function createUserDefinedFunction(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
userDefinedFunction: UserDefinedFunctionDefinition
|
||||
): Promise<UserDefinedFunctionDefinition & Resource> {
|
||||
let createdUserDefinedFunction: UserDefinedFunctionDefinition & Resource;
|
||||
const clearMessage = logConsoleProgress(`Creating user defined function ${userDefinedFunction.id}`);
|
||||
try {
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.userDefinedFunctions.create(userDefinedFunction);
|
||||
createdUserDefinedFunction = response.resource;
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while creating user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "CreateUserupdateUserDefinedFunction", error.code);
|
||||
sendNotificationForError(error);
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return createdUserDefinedFunction;
|
||||
}
|
||||
@@ -15,10 +15,11 @@ export async function deleteDatabase(databaseId: string): Promise<void> {
|
||||
const clearMessage = logConsoleProgress(`Deleting database ${databaseId}`);
|
||||
|
||||
try {
|
||||
if (userContext.defaultExperience === DefaultAccountExperienceType.Table) {
|
||||
throw new Error("Deleting database resources is not allowed for tables accounts");
|
||||
}
|
||||
if (window.authType === AuthType.AAD && !userContext.useSDKOperations) {
|
||||
if (
|
||||
window.authType === AuthType.AAD &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table &&
|
||||
!userContext.useSDKOperations
|
||||
) {
|
||||
await deleteDatabaseWithARM(databaseId);
|
||||
} else {
|
||||
await client()
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
|
||||
export async function deleteStoredProcedure(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
storedProcedureId: string
|
||||
): Promise<void> {
|
||||
const clearMessage = logConsoleProgress(`Deleting stored procedure ${storedProcedureId}`);
|
||||
try {
|
||||
await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.storedProcedure(storedProcedureId)
|
||||
.delete();
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while deleting stored procedure ${storedProcedureId}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "DeleteStoredProcedure", error.code);
|
||||
sendNotificationForError(error);
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
|
||||
export async function deleteTrigger(databaseId: string, collectionId: string, triggerId: string): Promise<void> {
|
||||
const clearMessage = logConsoleProgress(`Deleting trigger ${triggerId}`);
|
||||
try {
|
||||
await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.trigger(triggerId)
|
||||
.delete();
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while deleting trigger ${triggerId}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "DeleteTrigger", error.code);
|
||||
sendNotificationForError(error);
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
|
||||
export async function deleteUserDefinedFunction(databaseId: string, collectionId: string, id: string): Promise<void> {
|
||||
const clearMessage = logConsoleProgress(`Deleting user defined function ${id}`);
|
||||
try {
|
||||
await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.userDefinedFunction(id)
|
||||
.delete();
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while deleting user defined function ${id}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "DeleteUserDefinedFunction", error.code);
|
||||
sendNotificationForError(error);
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
import { HttpHeaders } from "../Constants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getSqlContainerThroughput } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||
import { getMongoDBCollectionThroughput } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||
import { getCassandraTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||
import { getGremlinGraphThroughput } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||
import { getTableThroughput } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
||||
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { readOffers } from "./readOffers";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
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 {
|
||||
const response = await client()
|
||||
.offer(offerId)
|
||||
.read(options);
|
||||
return (
|
||||
response && {
|
||||
...response.resource,
|
||||
headers: response.headers
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while querying offer for collection ${params.collectionId}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "ReadCollectionOffer", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
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}`);
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
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 { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
|
||||
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) {
|
||||
logConsoleError(`Error while querying quota info for container ${collection.id}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "ReadCollectionQuotaInfo", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,102 +0,0 @@
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||
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 { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { readOffers } from "./readOffers";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
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) {
|
||||
if (
|
||||
window.authType === AuthType.AAD &&
|
||||
!userContext.useSDKOperations &&
|
||||
userContext.defaultExperience !== DefaultAccountExperienceType.Table
|
||||
) {
|
||||
try {
|
||||
offerId = await getDatabaseOfferIdWithARM(params.databaseId);
|
||||
} catch (error) {
|
||||
clearMessage();
|
||||
if (error.code !== "NotFound") {
|
||||
throw new error();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
offerId = await getDatabaseOfferIdWithSDK(params.databaseResourceId);
|
||||
if (!offerId) {
|
||||
clearMessage();
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const options: RequestOptions = {
|
||||
initialHeaders: {
|
||||
[HttpHeaders.populateCollectionThroughputInfo]: true
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.offer(offerId)
|
||||
.read(options);
|
||||
return (
|
||||
response && {
|
||||
...response.resource,
|
||||
headers: response.headers
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while querying offer for database ${params.databaseId}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "ReadDatabaseOffer", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
switch (defaultExperience) {
|
||||
case DefaultAccountExperienceType.DocumentDB:
|
||||
rpResponse = await getSqlDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
|
||||
break;
|
||||
case DefaultAccountExperienceType.MongoDB:
|
||||
rpResponse = await getMongoDBDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Cassandra:
|
||||
rpResponse = await getCassandraKeyspaceThroughput(subscriptionId, resourceGroup, accountName, databaseId);
|
||||
break;
|
||||
case DefaultAccountExperienceType.Graph:
|
||||
rpResponse = await getGremlinDatabaseThroughput(subscriptionId, resourceGroup, accountName, databaseId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||
}
|
||||
|
||||
return rpResponse?.name;
|
||||
};
|
||||
|
||||
const getDatabaseOfferIdWithSDK = async (databaseResourceId: string): Promise<string> => {
|
||||
const offers = await readOffers();
|
||||
const offer = offers.find(offer => offer.resource === databaseResourceId);
|
||||
return offer?.id;
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Offer } from "../../Contracts/DataModels";
|
||||
import { ClientDefaults } from "../Constants";
|
||||
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
||||
import { Platform, configContext } from "../../ConfigContext";
|
||||
import { client } from "../CosmosClient";
|
||||
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||
import { logError } from "../Logger";
|
||||
import { sendCachedDataMessage } from "../MessageHandler";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export const readOffers = async (): Promise<Offer[]> => {
|
||||
const clearMessage = logConsoleProgress(`Querying offers`);
|
||||
try {
|
||||
if (configContext.platform === Platform.Portal) {
|
||||
return sendCachedDataMessage<Offer[]>(MessageTypes.AllOffers, [
|
||||
userContext.databaseAccount.id,
|
||||
ClientDefaults.portalCacheTimeoutMs
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
// If error getting cached Offers, continue on and read via SDK
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await client()
|
||||
.offers.readAll()
|
||||
.fetchAll();
|
||||
return response?.resources;
|
||||
} catch (error) {
|
||||
// This should be removed when we can correctly identify if an account is serverless when connected using connection string too.
|
||||
if (error.message.includes("Reading or replacing offers is not supported for serverless accounts")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
logConsoleError(`Error while querying offers:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "ReadOffers", error.code);
|
||||
sendNotificationForError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
|
||||
export async function readStoredProcedures(
|
||||
databaseId: string,
|
||||
collectionId: string
|
||||
): Promise<(StoredProcedureDefinition & Resource)[]> {
|
||||
let sprocs: (StoredProcedureDefinition & Resource)[];
|
||||
const clearMessage = logConsoleProgress(`Querying stored procedures for container ${collectionId}`);
|
||||
try {
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.storedProcedures.readAll()
|
||||
.fetchAll();
|
||||
sprocs = response.resources;
|
||||
} catch (error) {
|
||||
logConsoleError(`Failed to query stored procedures for container ${collectionId}: ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "ReadStoredProcedures", error.code);
|
||||
sendNotificationForError(error);
|
||||
}
|
||||
clearMessage();
|
||||
return sprocs;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Resource, TriggerDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
|
||||
export async function readTriggers(
|
||||
databaseId: string,
|
||||
collectionId: string
|
||||
): Promise<(TriggerDefinition & Resource)[]> {
|
||||
let triggers: (TriggerDefinition & Resource)[];
|
||||
const clearMessage = logConsoleProgress(`Querying triggers for container ${collectionId}`);
|
||||
try {
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.triggers.readAll()
|
||||
.fetchAll();
|
||||
triggers = response.resources;
|
||||
} catch (error) {
|
||||
logConsoleError(`Failed to query triggers for container ${collectionId}: ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "ReadTriggers", error.code);
|
||||
sendNotificationForError(error);
|
||||
}
|
||||
clearMessage();
|
||||
return triggers;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
|
||||
export async function readUserDefinedFunctions(
|
||||
databaseId: string,
|
||||
collectionId: string
|
||||
): Promise<(UserDefinedFunctionDefinition & Resource)[]> {
|
||||
let udfs: (UserDefinedFunctionDefinition & Resource)[];
|
||||
const clearMessage = logConsoleProgress(`Querying user defined functions for container ${collectionId}`);
|
||||
try {
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.userDefinedFunctions.readAll()
|
||||
.fetchAll();
|
||||
udfs = response.resources;
|
||||
} catch (error) {
|
||||
logConsoleError(`Failed to query user defined functions for container ${collectionId}: ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "ReadUserDefinedFunctions", error.code);
|
||||
sendNotificationForError(error);
|
||||
}
|
||||
clearMessage();
|
||||
return udfs;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
|
||||
export async function updateStoredProcedure(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
storedProcedure: StoredProcedureDefinition
|
||||
): Promise<StoredProcedureDefinition & Resource> {
|
||||
let updatedStoredProcedure: StoredProcedureDefinition & Resource;
|
||||
const clearMessage = logConsoleProgress(`Updating stored procedure ${storedProcedure.id}`);
|
||||
try {
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.storedProcedure(storedProcedure.id)
|
||||
.replace(storedProcedure);
|
||||
updatedStoredProcedure = response.resource;
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while updating stored procedure ${storedProcedure.id}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "UpdateStoredProcedure", error.code);
|
||||
sendNotificationForError(error);
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return updatedStoredProcedure;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { TriggerDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
|
||||
export async function updateTrigger(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
trigger: TriggerDefinition
|
||||
): Promise<TriggerDefinition> {
|
||||
let updatedTrigger: TriggerDefinition;
|
||||
const clearMessage = logConsoleProgress(`Updating trigger ${trigger.id}`);
|
||||
try {
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.trigger(trigger.id)
|
||||
.replace(trigger);
|
||||
updatedTrigger = response.resource;
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while updating trigger ${trigger.id}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "UpdateTrigger", error.code);
|
||||
sendNotificationForError(error);
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return updatedTrigger;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import { logConsoleError, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { client } from "../CosmosClient";
|
||||
import { logError } from "../Logger";
|
||||
import { sendNotificationForError } from "./sendNotificationForError";
|
||||
|
||||
export async function updateUserDefinedFunction(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
userDefinedFunction: UserDefinedFunctionDefinition
|
||||
): Promise<UserDefinedFunctionDefinition & Resource> {
|
||||
let updatedUserDefinedFunction: UserDefinedFunctionDefinition & Resource;
|
||||
const clearMessage = logConsoleProgress(`Updating user defined function ${userDefinedFunction.id}`);
|
||||
try {
|
||||
const response = await client()
|
||||
.database(databaseId)
|
||||
.container(collectionId)
|
||||
.scripts.userDefinedFunction(userDefinedFunction.id)
|
||||
.replace(userDefinedFunction);
|
||||
updatedUserDefinedFunction = response.resource;
|
||||
} catch (error) {
|
||||
logConsoleError(`Error while updating user defined function ${userDefinedFunction.id}:\n ${JSON.stringify(error)}`);
|
||||
logError(JSON.stringify(error), "UpdateUserupdateUserDefinedFunction", error.code);
|
||||
sendNotificationForError(error);
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
return updatedUserDefinedFunction;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ export enum Platform {
|
||||
|
||||
interface ConfigContext {
|
||||
platform: Platform;
|
||||
allowedParentFrameOrigins: string[];
|
||||
allowedParentFrameOrigins: RegExp;
|
||||
gitSha?: string;
|
||||
proxyPath?: string;
|
||||
AAD_ENDPOINT: string;
|
||||
@@ -30,13 +30,7 @@ interface ConfigContext {
|
||||
// Default configuration
|
||||
let configContext: Readonly<ConfigContext> = {
|
||||
platform: Platform.Portal,
|
||||
allowedParentFrameOrigins: [
|
||||
`^https:\\/\\/cosmos\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*portal\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*ext\\.azure\\.(com|cn|us)$`,
|
||||
`^https:\\/\\/[\\.\\w]*\\.ext\\.microsoftazure\\.de$`,
|
||||
`^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$`
|
||||
],
|
||||
allowedParentFrameOrigins: /^https:\/\/portal\.azure\.com$|^https:\/\/portal\.azure\.us$|^https:\/\/portal\.azure\.cn$|^https:\/\/portal\.microsoftazure\.de$|^https:\/\/.+\.portal\.azure\.com$|^https:\/\/.+\.portal\.azure\.us$|^https:\/\/.+\.portal\.azure\.cn$|^https:\/\/.+\.portal\.microsoftazure\.de$|^https:\/\/main\.documentdb\.ext\.azure\.com$|^https:\/\/main\.documentdb\.ext\.microsoftazure\.de$|^https:\/\/main\.documentdb\.ext\.azure\.cn$|^https:\/\/main\.documentdb\.ext\.azure\.us$/,
|
||||
// Webpack injects this at build time
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
@@ -79,13 +73,8 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
|
||||
const response = await fetch("./config.json");
|
||||
if (response.status === 200) {
|
||||
try {
|
||||
const { allowedParentFrameOrigins, ...externalConfig } = await response.json();
|
||||
const externalConfig = await response.json();
|
||||
Object.assign(configContext, externalConfig);
|
||||
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
|
||||
updateConfigContext({
|
||||
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins]
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Unable to parse json in config file");
|
||||
console.error(error);
|
||||
|
||||
@@ -88,6 +88,10 @@ export interface Resource {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ResourceRequest {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface Collection extends Resource {
|
||||
defaultTtl?: number;
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
@@ -100,12 +104,39 @@ export interface Collection extends Resource {
|
||||
geospatialConfig?: GeospatialConfig;
|
||||
}
|
||||
|
||||
export interface CreateCollectionWithRpResponse extends Resource {
|
||||
properties: Collection;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface CollectionRequest extends ResourceRequest {
|
||||
defaultTtl?: number;
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
partitionKey?: PartitionKey;
|
||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||
conflictResolutionPolicy?: ConflictResolutionPolicy;
|
||||
}
|
||||
|
||||
export interface Database extends Resource {
|
||||
collections?: Collection[];
|
||||
}
|
||||
|
||||
export interface DocumentId extends Resource {}
|
||||
|
||||
export interface Script extends Resource {
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface StoredProcedure extends Script {}
|
||||
|
||||
export interface UserDefinedFunction extends Script {}
|
||||
|
||||
export interface Trigger extends Script {
|
||||
triggerType: string;
|
||||
triggerOperation: string;
|
||||
}
|
||||
|
||||
export interface ConflictId extends Resource {
|
||||
resourceId?: string;
|
||||
resourceType?: string;
|
||||
@@ -228,6 +259,28 @@ export interface ErrorDataModel {
|
||||
code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a property bag for telemetry e.g. see ITelemetryError.
|
||||
*/
|
||||
export interface ITelemetryProperties {
|
||||
[propertyName: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a property bag for telemetry e.g. see ITelemetryError.
|
||||
*/
|
||||
export interface ITelemetryEvent {
|
||||
name: string;
|
||||
properties?: ITelemetryProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines an error to be logged as telemetry data.
|
||||
*/
|
||||
export interface ITelemetryError extends ITelemetryEvent {
|
||||
error: any;
|
||||
}
|
||||
|
||||
export interface CreateDatabaseAndCollectionRequest {
|
||||
databaseId: string;
|
||||
collectionId: string;
|
||||
@@ -254,6 +307,11 @@ export enum AutopilotTier {
|
||||
Tier4 = 4
|
||||
}
|
||||
|
||||
export interface RpOptions {
|
||||
// tier is sent as string, autoscale as object (AutoPilotCreationSettings)
|
||||
[key: string]: string | AutoPilotCreationSettings;
|
||||
}
|
||||
|
||||
export interface Query {
|
||||
id: string;
|
||||
resourceId: string;
|
||||
@@ -276,31 +334,10 @@ export interface CreateDatabaseParams {
|
||||
offerThroughput?: number;
|
||||
}
|
||||
|
||||
export interface CreateCollectionParams {
|
||||
createNewDatabase: boolean;
|
||||
collectionId: string;
|
||||
databaseId: string;
|
||||
databaseLevelThroughput: boolean;
|
||||
offerThroughput: number;
|
||||
analyticalStorageTtl?: number;
|
||||
autoPilotMaxThroughput?: number;
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
partitionKey?: PartitionKey;
|
||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||
createMongoWildcardIndex?: boolean;
|
||||
}
|
||||
|
||||
export interface ReadDatabaseOfferParams {
|
||||
databaseId: string;
|
||||
databaseResourceId?: string;
|
||||
offerId?: string;
|
||||
}
|
||||
|
||||
export interface ReadCollectionOfferParams {
|
||||
collectionId: string;
|
||||
databaseId: string;
|
||||
collectionResourceId?: string;
|
||||
offerId?: string;
|
||||
export interface SharedThroughputRange {
|
||||
minimumRU: number;
|
||||
maximumRU: number;
|
||||
defaultRU: number;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
@@ -445,6 +482,25 @@ export interface NotebookConfigurationEndpointInfo {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface SparkCluster {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
properties: {
|
||||
kind: string;
|
||||
driverSize: string;
|
||||
workerSize: string;
|
||||
workerInstanceCount: number;
|
||||
creationTime: string;
|
||||
status: string;
|
||||
libraries?: SparkClusterLibrary[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SparkClusterFeedResponse {
|
||||
value: SparkCluster[];
|
||||
}
|
||||
|
||||
export interface SparkClusterConnectionInfo {
|
||||
userName: string;
|
||||
password: string;
|
||||
@@ -486,10 +542,79 @@ export interface MongoParameters extends RpParameters {
|
||||
analyticalStorageTtl?: number;
|
||||
}
|
||||
|
||||
export interface GraphParameters extends RpParameters {
|
||||
pk: string;
|
||||
coll: string;
|
||||
cd: Boolean;
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
}
|
||||
|
||||
export interface CreationRequest {
|
||||
properties: {
|
||||
resource: {
|
||||
id: string;
|
||||
};
|
||||
options: RpOptions;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SqlCollectionParameters extends RpParameters {
|
||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||
pk: string;
|
||||
coll: string;
|
||||
cd: Boolean;
|
||||
analyticalStorageTtl?: number;
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
}
|
||||
|
||||
export interface MongoCreationRequest extends CreationRequest {
|
||||
properties: {
|
||||
resource: {
|
||||
id: string;
|
||||
analyticalStorageTtl?: number;
|
||||
shardKey?: {};
|
||||
};
|
||||
options: RpOptions;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GraphCreationRequest extends CreationRequest {
|
||||
properties: {
|
||||
resource: {
|
||||
id: string;
|
||||
partitionKey: {};
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
};
|
||||
options: RpOptions;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateDatabaseWithRpResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
properties: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SparkClusterLibrary {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SqlCollectionCreationRequest extends CreationRequest {
|
||||
properties: {
|
||||
resource: {
|
||||
uniqueKeyPolicy?: UniqueKeyPolicy;
|
||||
id: string;
|
||||
partitionKey: {};
|
||||
analyticalStorageTtl?: number;
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
};
|
||||
options: RpOptions;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Library extends SparkClusterLibrary {
|
||||
properties: {
|
||||
kind: "Jar";
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import {
|
||||
QueryMetrics,
|
||||
Resource,
|
||||
StoredProcedureDefinition,
|
||||
TriggerDefinition,
|
||||
UserDefinedFunctionDefinition
|
||||
} from "@azure/cosmos";
|
||||
import * as DataModels from "./DataModels";
|
||||
import Q from "q";
|
||||
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient";
|
||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { QueryMetrics } from "@azure/cosmos";
|
||||
import { UploadDetails } from "../workers/upload/definitions";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||
import Trigger from "../Explorer/Tree/Trigger";
|
||||
import UserDefinedFunction from "../Explorer/Tree/UserDefinedFunction";
|
||||
import { UploadDetails } from "../workers/upload/definitions";
|
||||
import * as DataModels from "./DataModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||
|
||||
export interface TokenProvider {
|
||||
getAuthHeader(): Promise<Headers>;
|
||||
@@ -81,15 +75,15 @@ export interface Database extends TreeNode {
|
||||
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
|
||||
|
||||
selectDatabase(): void;
|
||||
expandDatabase(): Promise<void>;
|
||||
expandDatabase(): void;
|
||||
collapseDatabase(): void;
|
||||
|
||||
loadCollections(): Promise<void>;
|
||||
loadCollections(): Q.Promise<void>;
|
||||
findCollectionWithId(collectionRid: string): Collection;
|
||||
openAddCollection(database: Database, event: MouseEvent): void;
|
||||
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
|
||||
readSettings(): void;
|
||||
onSettingsClick: () => void;
|
||||
loadOffer(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface CollectionBase extends TreeNode {
|
||||
@@ -133,7 +127,8 @@ export interface Collection extends CollectionBase {
|
||||
onMongoDBDocumentsClick(): void;
|
||||
openTab(): void;
|
||||
|
||||
onSettingsClick: () => Promise<void>;
|
||||
onSettingsClick: () => void;
|
||||
readSettings(): Q.Promise<void>;
|
||||
onDeleteCollectionContextMenuClick(source: Collection, event: MouseEvent): void;
|
||||
|
||||
onNewGraphClick(): void;
|
||||
@@ -158,14 +153,13 @@ export interface Collection extends CollectionBase {
|
||||
collapseUserDefinedFunctions(): void;
|
||||
collapseTriggers(): void;
|
||||
|
||||
loadUserDefinedFunctions(): Promise<any>;
|
||||
loadStoredProcedures(): Promise<any>;
|
||||
loadTriggers(): Promise<any>;
|
||||
loadOffer(): Promise<void>;
|
||||
loadUserDefinedFunctions(): Q.Promise<any>;
|
||||
loadStoredProcedures(): Q.Promise<any>;
|
||||
loadTriggers(): Q.Promise<any>;
|
||||
|
||||
createStoredProcedureNode(data: StoredProcedureDefinition & Resource): StoredProcedure;
|
||||
createUserDefinedFunctionNode(data: UserDefinedFunctionDefinition & Resource): UserDefinedFunction;
|
||||
createTriggerNode(data: TriggerDefinition & Resource): Trigger;
|
||||
createStoredProcedureNode(data: DataModels.StoredProcedure): StoredProcedure;
|
||||
createUserDefinedFunctionNode(data: DataModels.UserDefinedFunction): UserDefinedFunction;
|
||||
createTriggerNode(data: DataModels.Trigger): Trigger;
|
||||
findStoredProcedureWithId(sprocRid: string): StoredProcedure;
|
||||
findTriggerWithId(triggerRid: string): Trigger;
|
||||
findUserDefinedFunctionWithId(udfRid: string): UserDefinedFunction;
|
||||
@@ -309,6 +303,10 @@ export interface ScriptTabOption extends TabOptions {
|
||||
partitionKey?: DataModels.PartitionKey;
|
||||
}
|
||||
|
||||
export interface WaitsForTemplate {
|
||||
isTemplateReady: ko.Observable<boolean>;
|
||||
}
|
||||
|
||||
export interface EditorPosition {
|
||||
line: number;
|
||||
column: number;
|
||||
@@ -355,8 +353,7 @@ export enum CollectionTabKind {
|
||||
NotebookV2 = 15,
|
||||
SparkMasterTab = 16,
|
||||
Gallery = 17,
|
||||
NotebookViewer = 18,
|
||||
SettingsV2 = 19
|
||||
NotebookViewer = 18
|
||||
}
|
||||
|
||||
export enum TerminalKind {
|
||||
|
||||
@@ -48,10 +48,6 @@ describe("Component Registerer", () => {
|
||||
expect(ko.components.isRegistered("settings-tab")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register settings-tab-v2 component", () => {
|
||||
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register query-tab component", () => {
|
||||
expect(ko.components.isRegistered("query-tab")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -34,7 +34,6 @@ ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedure
|
||||
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());
|
||||
ko.components.register("graph-tab", new TabComponents.GraphTab());
|
||||
|
||||
@@ -16,8 +16,6 @@ import Explorer from "./Explorer";
|
||||
import UserDefinedFunction from "./Tree/UserDefinedFunction";
|
||||
import StoredProcedure from "./Tree/StoredProcedure";
|
||||
import Trigger from "./Tree/Trigger";
|
||||
import { userContext } from "../UserContext";
|
||||
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
|
||||
|
||||
export interface CollectionContextMenuButtonParams {
|
||||
databaseId: string;
|
||||
@@ -31,24 +29,22 @@ export interface DatabaseContextMenuButtonParams {
|
||||
* New resource tree (in ReactJS)
|
||||
*/
|
||||
export class ResourceTreeContextMenuButtonFactory {
|
||||
public static createDatabaseContextMenu(container: Explorer): TreeNodeMenuItem[] {
|
||||
const items: TreeNodeMenuItem[] = [
|
||||
{
|
||||
iconSrc: AddCollectionIcon,
|
||||
onClick: () => container.onNewCollectionClicked(),
|
||||
label: container.addCollectionText()
|
||||
}
|
||||
];
|
||||
public static createDatabaseContextMenu(
|
||||
container: Explorer,
|
||||
selectedDatabase: ViewModels.Database
|
||||
): TreeNodeMenuItem[] {
|
||||
const newCollectionMenuItem: TreeNodeMenuItem = {
|
||||
iconSrc: AddCollectionIcon,
|
||||
onClick: () => container.onNewCollectionClicked(),
|
||||
label: container.addCollectionText()
|
||||
};
|
||||
|
||||
if (userContext.defaultExperience !== DefaultAccountExperienceType.Table) {
|
||||
items.push({
|
||||
iconSrc: DeleteDatabaseIcon,
|
||||
onClick: () => container.deleteDatabaseConfirmationPane.open(),
|
||||
label: container.deleteDatabaseText(),
|
||||
styleClass: "deleteDatabaseMenuItem"
|
||||
});
|
||||
}
|
||||
return items;
|
||||
const deleteDatabaseMenuItem = {
|
||||
iconSrc: DeleteDatabaseIcon,
|
||||
onClick: () => container.deleteDatabaseConfirmationPane.open(),
|
||||
label: container.deleteDatabaseText()
|
||||
};
|
||||
return [newCollectionMenuItem, deleteDatabaseMenuItem];
|
||||
}
|
||||
|
||||
public static createCollectionContextMenuButton(
|
||||
@@ -116,8 +112,7 @@ export class ResourceTreeContextMenuButtonFactory {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null);
|
||||
},
|
||||
label: container.deleteCollectionText(),
|
||||
styleClass: "deleteCollectionMenuItem"
|
||||
label: container.deleteCollectionText()
|
||||
});
|
||||
|
||||
return items;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { StringUtils } from "../../../Utils/StringUtils";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import CollapseChevronDownIcon from "../../../../images/QueryBuilder/CollapseChevronDown_16x.png";
|
||||
|
||||
|
||||
@@ -265,9 +265,6 @@ exports[`test render renders with filters 1`] = `
|
||||
"buttonTextDisabled": "#a19f9d",
|
||||
"buttonTextHovered": "#201f1e",
|
||||
"buttonTextPressed": "#201f1e",
|
||||
"cardShadow": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)",
|
||||
"cardShadowHovered": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)",
|
||||
"cardStandoutBackground": "#ffffff",
|
||||
"defaultStateBackground": "#faf9f8",
|
||||
"disabledBackground": "#f3f2f1",
|
||||
"disabledBodySubtext": "#c8c6c4",
|
||||
@@ -607,9 +604,6 @@ exports[`test render renders with filters 1`] = `
|
||||
"buttonTextDisabled": "#a19f9d",
|
||||
"buttonTextHovered": "#201f1e",
|
||||
"buttonTextPressed": "#201f1e",
|
||||
"cardShadow": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)",
|
||||
"cardShadowHovered": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)",
|
||||
"cardStandoutBackground": "#ffffff",
|
||||
"defaultStateBackground": "#faf9f8",
|
||||
"disabledBackground": "#f3f2f1",
|
||||
"disabledBodySubtext": "#c8c6c4",
|
||||
@@ -1003,9 +997,6 @@ exports[`test render renders with filters 1`] = `
|
||||
"buttonTextDisabled": "#a19f9d",
|
||||
"buttonTextHovered": "#201f1e",
|
||||
"buttonTextPressed": "#201f1e",
|
||||
"cardShadow": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)",
|
||||
"cardShadowHovered": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)",
|
||||
"cardStandoutBackground": "#ffffff",
|
||||
"defaultStateBackground": "#faf9f8",
|
||||
"disabledBackground": "#f3f2f1",
|
||||
"disabledBodySubtext": "#c8c6c4",
|
||||
@@ -1122,11 +1113,6 @@ exports[`test render renders with filters 1`] = `
|
||||
},
|
||||
"iconDisabled": Object {
|
||||
"color": "#a19f9d",
|
||||
"selectors": Object {
|
||||
"@media screen and (-ms-high-contrast: active)": Object {
|
||||
"color": "GrayText",
|
||||
},
|
||||
},
|
||||
},
|
||||
"label": Array [
|
||||
Object {
|
||||
@@ -1148,11 +1134,6 @@ exports[`test render renders with filters 1`] = `
|
||||
},
|
||||
"menuIconDisabled": Object {
|
||||
"color": "#a19f9d",
|
||||
"selectors": Object {
|
||||
"@media screen and (-ms-high-contrast: active)": Object {
|
||||
"color": "GrayText",
|
||||
},
|
||||
},
|
||||
},
|
||||
"root": Array [
|
||||
Object {
|
||||
@@ -1169,6 +1150,7 @@ exports[`test render renders with filters 1`] = `
|
||||
"right": 2,
|
||||
"selectors": Object {
|
||||
"@media screen and (-ms-high-contrast: active)": Object {
|
||||
"border": "none",
|
||||
"bottom": -2,
|
||||
"left": -2,
|
||||
"outlineColor": "ButtonText",
|
||||
@@ -1248,6 +1230,7 @@ exports[`test render renders with filters 1`] = `
|
||||
"right": 2,
|
||||
"selectors": Object {
|
||||
"@media screen and (-ms-high-contrast: active)": Object {
|
||||
"border": "none",
|
||||
"bottom": -2,
|
||||
"left": -2,
|
||||
"outlineColor": "ButtonText",
|
||||
@@ -1276,6 +1259,10 @@ exports[`test render renders with filters 1`] = `
|
||||
":hover": Object {
|
||||
"outline": 0,
|
||||
},
|
||||
"@media screen and (-ms-high-contrast: active)": Object {
|
||||
"borderColor": "grayText",
|
||||
"color": "grayText",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
@@ -1375,21 +1362,13 @@ exports[`test render renders with filters 1`] = `
|
||||
"selectors": Object {
|
||||
"@media screen and (-ms-high-contrast: active)": Object {
|
||||
"MsHighContrastAdjust": "none",
|
||||
"backgroundColor": "Window",
|
||||
"border": "1px solid WindowText",
|
||||
"borderRightWidth": "0",
|
||||
"color": "WindowText",
|
||||
"backgroundColor": "WindowText",
|
||||
"color": "Window",
|
||||
},
|
||||
},
|
||||
},
|
||||
".ms-Button--primary + .ms-Button": Object {
|
||||
"border": "none",
|
||||
"selectors": Object {
|
||||
"@media screen and (-ms-high-contrast: active)": Object {
|
||||
"border": "1px solid WindowText",
|
||||
"borderLeftWidth": "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1429,9 +1408,6 @@ exports[`test render renders with filters 1`] = `
|
||||
"borderColor": "GrayText",
|
||||
"color": "GrayText",
|
||||
},
|
||||
"@media screen and (forced-colors: active)": Object {
|
||||
"forcedColorAdjust": "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
"splitButtonContainerFocused": Object {
|
||||
@@ -1578,13 +1554,6 @@ exports[`test render renders with filters 1`] = `
|
||||
},
|
||||
},
|
||||
},
|
||||
".ms-Button-menuIcon": Object {
|
||||
"selectors": Object {
|
||||
"@media screen and (-ms-high-contrast: active)": Object {
|
||||
"color": "GrayText",
|
||||
},
|
||||
},
|
||||
},
|
||||
":hover": Object {
|
||||
"cursor": "default",
|
||||
},
|
||||
@@ -1806,9 +1775,6 @@ exports[`test render renders with filters 1`] = `
|
||||
"buttonTextDisabled": "#a19f9d",
|
||||
"buttonTextHovered": "#201f1e",
|
||||
"buttonTextPressed": "#201f1e",
|
||||
"cardShadow": "0 1.6px 3.6px 0 rgba(0, 0, 0, 0.132), 0 0.3px 0.9px 0 rgba(0, 0, 0, 0.108)",
|
||||
"cardShadowHovered": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)",
|
||||
"cardStandoutBackground": "#ffffff",
|
||||
"defaultStateBackground": "#faf9f8",
|
||||
"disabledBackground": "#f3f2f1",
|
||||
"disabledBodySubtext": "#c8c6c4",
|
||||
|
||||
@@ -86,7 +86,6 @@ export class DynamicListViewModel extends WaitsForTemplateViewModel {
|
||||
public onRemoveItemKeyPress = (data: any, event: KeyboardEvent, source: any): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.removeItem(data, event);
|
||||
(document.querySelector(".dynamicListItem:last-of-type input") as HTMLElement).focus();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
@@ -95,7 +94,7 @@ export class DynamicListViewModel extends WaitsForTemplateViewModel {
|
||||
|
||||
public addItem(): void {
|
||||
this.listItems.push({ value: ko.observable("") });
|
||||
(document.querySelector(".dynamicListItem:last-of-type input") as HTMLElement).focus();
|
||||
document.getElementById("uniqueKeyItems").focus();
|
||||
}
|
||||
|
||||
public onAddItemKeyPress = (source: any, event: KeyboardEvent): boolean => {
|
||||
|
||||
@@ -55,7 +55,6 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
label: "Enable Injecting Notebook Viewer Link into the first cell",
|
||||
value: "true"
|
||||
},
|
||||
{ key: "feature.enablesettingsv2", label: "Enable SettingsV2 Tab", value: "true" },
|
||||
{ key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
|
||||
{
|
||||
key: "feature.enablefixedcollectionwithsharedthroughput",
|
||||
|
||||
@@ -178,12 +178,6 @@ exports[`Feature panel renders all flags 1`] = `
|
||||
className="checkboxRow"
|
||||
horizontalAlign="space-between"
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.enablesettingsv2"
|
||||
label="Enable SettingsV2 Tab"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
key="feature.canexceedmaximumvalue"
|
||||
|
||||
@@ -6,7 +6,7 @@ import { RepoListItem } from "./GitHubReposComponent";
|
||||
import { ChildrenMargin } from "./GitHubStyleConstants";
|
||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import UrlUtility from "../../../Common/UrlUtility";
|
||||
import Explorer from "../../Explorer";
|
||||
|
||||
|
||||
@@ -66,9 +66,7 @@ export class GitHubReposComponent extends React.Component<GitHubReposComponentPr
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"firstdivbg headerline"} role="heading" aria-level={2}>
|
||||
{header}
|
||||
</div>
|
||||
<div className={"firstdivbg headerline"}>{header}</div>
|
||||
<div className={"paneMainContent"}>{content}</div>
|
||||
{!this.props.showAuthorizeAccess && (
|
||||
<>
|
||||
|
||||
@@ -8,8 +8,6 @@ import * as Logger from "../../../Common/Logger";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { StringUtils } from "../../../Utils/StringUtils";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { TerminalQueryParams } from "../../../Common/Constants";
|
||||
|
||||
export interface NotebookTerminalComponentProps {
|
||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
||||
@@ -34,11 +32,11 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
||||
|
||||
public getTerminalParams(): Map<string, string> {
|
||||
let params: Map<string, string> = new Map<string, string>();
|
||||
params.set(TerminalQueryParams.Terminal, "true");
|
||||
params.set("terminal", "true");
|
||||
|
||||
const terminalEndpoint: string = this.tryGetTerminalEndpoint();
|
||||
if (terminalEndpoint) {
|
||||
params.set(TerminalQueryParams.TerminalEndpoint, terminalEndpoint);
|
||||
params.set("terminalEndpoint", terminalEndpoint);
|
||||
}
|
||||
|
||||
return params;
|
||||
@@ -77,13 +75,11 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
||||
return "";
|
||||
}
|
||||
|
||||
params.set(TerminalQueryParams.Server, serverInfo.notebookServerEndpoint);
|
||||
params.set("server", serverInfo.notebookServerEndpoint);
|
||||
if (serverInfo.authToken && serverInfo.authToken.length > 0) {
|
||||
params.set(TerminalQueryParams.Token, serverInfo.authToken);
|
||||
params.set("token", serverInfo.authToken);
|
||||
}
|
||||
|
||||
params.set(TerminalQueryParams.SubscriptionId, userContext.subscriptionId);
|
||||
|
||||
let result: string = "terminal.html?";
|
||||
for (let key of params.keys()) {
|
||||
result += `${key}=${encodeURIComponent(params.get(key))}&`;
|
||||
|
||||
@@ -30,7 +30,6 @@ export interface NotebookViewerComponentProps {
|
||||
isFavorite?: boolean;
|
||||
backNavigationText: string;
|
||||
hideInputs?: boolean;
|
||||
hidePrompts?: boolean;
|
||||
onBackClick: () => void;
|
||||
onTagClick: (tag: string) => void;
|
||||
}
|
||||
@@ -149,8 +148,7 @@ export class NotebookViewerComponent extends React.Component<
|
||||
{this.state.showProgressBar && <ProgressIndicator />}
|
||||
|
||||
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
|
||||
hideInputs: this.props.hideInputs,
|
||||
hidePrompts: this.props.hidePrompts
|
||||
hideInputs: this.props.hideInputs
|
||||
})}
|
||||
|
||||
{this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />}
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from "office-ui-fabric-react/lib/utilities/selection/index";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import { TextField, ITextFieldProps, ITextField } from "office-ui-fabric-react/lib/TextField";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
|
||||
import { QueriesClient } from "../../../Common/QueriesClient";
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
.settingsV2MainContainer {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settingsV2ToolTip {
|
||||
padding: 10px;
|
||||
font: 12px @DataExplorerFont;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.autoPilotSelector span {
|
||||
height: 25px;
|
||||
font: 14px @DataExplorerFont;
|
||||
}
|
||||
|
||||
.settingsV2TabsContainer {
|
||||
padding: @LargeSpace @LargeSpace 30px @LargeSpace;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
font-family: @DataExplorerFont;
|
||||
}
|
||||
|
||||
.settingsV2IndexingPolicyEditor {
|
||||
width: 100%;
|
||||
height: 60vh;
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { SettingsComponentProps, SettingsComponent, SettingsComponentState } from "./SettingsComponent";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import SettingsTabV2 from "../../Tabs/SettingsTabV2";
|
||||
import { collection } from "./TestUtils";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import ko from "knockout";
|
||||
import { TtlType, isDirty, TtlOnNoDefault, TtlOn, TtlOff } from "./SettingsUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||
jest.mock("../../../Common/dataAccess/updateCollection", () => ({
|
||||
updateCollection: jest.fn().mockReturnValue({
|
||||
id: undefined,
|
||||
defaultTtl: undefined,
|
||||
indexingPolicy: undefined,
|
||||
conflictResolutionPolicy: undefined,
|
||||
changeFeedPolicy: undefined,
|
||||
analyticalStorageTtl: undefined,
|
||||
geospatialConfig: undefined
|
||||
} as DataModels.Collection)
|
||||
}));
|
||||
import { updateOffer } from "../../../Common/DocumentClientUtilityBase";
|
||||
jest.mock("../../../Common/DocumentClientUtilityBase", () => ({
|
||||
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer)
|
||||
}));
|
||||
|
||||
describe("SettingsComponent", () => {
|
||||
const baseProps: SettingsComponentProps = {
|
||||
settingsTab: new SettingsTabV2({
|
||||
collection: collection,
|
||||
tabKind: ViewModels.CollectionTabKind.SettingsV2,
|
||||
title: "Scale & Settings",
|
||||
tabPath: "",
|
||||
node: undefined,
|
||||
selfLink: undefined,
|
||||
hashLocation: "settings",
|
||||
isActive: ko.observable(false),
|
||||
onUpdateTabsButtons: undefined
|
||||
})
|
||||
};
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("dirty value enables save button and discard button", () => {
|
||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
|
||||
expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toEqual(false);
|
||||
expect(settingsComponentInstance.isDiscardSettingsButtonEnabled()).toEqual(false);
|
||||
wrapper.setState({ isScaleSaveable: true, isScaleDiscardable: true });
|
||||
wrapper.update();
|
||||
expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toEqual(true);
|
||||
expect(settingsComponentInstance.isDiscardSettingsButtonEnabled()).toEqual(true);
|
||||
wrapper.setState({
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
isSubSettingsSaveable: true,
|
||||
isSubSettingsDiscardable: true
|
||||
});
|
||||
wrapper.update();
|
||||
expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toEqual(true);
|
||||
expect(settingsComponentInstance.isDiscardSettingsButtonEnabled()).toEqual(true);
|
||||
wrapper.setState({ isSubSettingsSaveable: false, isSubSettingsDiscardable: false, isIndexingPolicyDirty: true });
|
||||
wrapper.update();
|
||||
expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toEqual(true);
|
||||
expect(settingsComponentInstance.isDiscardSettingsButtonEnabled()).toEqual(true);
|
||||
wrapper.setState({ isIndexingPolicyDirty: false, isConflictResolutionDirty: true });
|
||||
wrapper.update();
|
||||
expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toEqual(true);
|
||||
expect(settingsComponentInstance.isDiscardSettingsButtonEnabled()).toEqual(true);
|
||||
});
|
||||
|
||||
it("auto pilot helper functions pass on correct value", () => {
|
||||
const newCollection = { ...collection };
|
||||
newCollection.offer = ko.observable<DataModels.Offer>({
|
||||
content: {
|
||||
offerAutopilotSettings: {
|
||||
maxThroughput: 10000
|
||||
}
|
||||
}
|
||||
} as DataModels.Offer);
|
||||
|
||||
const props = { ...baseProps };
|
||||
props.settingsTab.collection = newCollection;
|
||||
|
||||
const wrapper = shallow(<SettingsComponent {...props} />);
|
||||
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
|
||||
expect(settingsComponentInstance.hasProvisioningTypeChanged()).toEqual(false);
|
||||
wrapper.setState({
|
||||
userCanChangeProvisioningTypes: true,
|
||||
isAutoPilotSelected: true,
|
||||
wasAutopilotOriginallySet: false,
|
||||
autoPilotThroughput: 1000
|
||||
});
|
||||
wrapper.update();
|
||||
expect(settingsComponentInstance.hasProvisioningTypeChanged()).toEqual(true);
|
||||
});
|
||||
|
||||
it("shouldShowKeyspaceSharedThroughputMessage", () => {
|
||||
let settingsComponentInstance = new SettingsComponent(baseProps);
|
||||
expect(settingsComponentInstance.shouldShowKeyspaceSharedThroughputMessage()).toEqual(false);
|
||||
|
||||
const newContainer = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
newContainer.isPreferredApiCassandra = ko.computed(() => true);
|
||||
|
||||
const newCollection = { ...collection };
|
||||
newCollection.container = newContainer;
|
||||
const newDatabase = {
|
||||
nodeKind: undefined,
|
||||
rid: undefined,
|
||||
container: newContainer,
|
||||
self: undefined,
|
||||
id: undefined,
|
||||
collections: undefined,
|
||||
offer: undefined,
|
||||
isDatabaseExpanded: undefined,
|
||||
isDatabaseShared: ko.computed(() => true),
|
||||
selectedSubnodeKind: undefined,
|
||||
selectDatabase: undefined,
|
||||
expandDatabase: undefined,
|
||||
collapseDatabase: undefined,
|
||||
loadCollections: undefined,
|
||||
findCollectionWithId: undefined,
|
||||
openAddCollection: undefined,
|
||||
onDeleteDatabaseContextMenuClick: undefined,
|
||||
readSettings: undefined,
|
||||
onSettingsClick: undefined,
|
||||
loadOffer: undefined
|
||||
} as ViewModels.Database;
|
||||
newCollection.getDatabase = () => newDatabase;
|
||||
newCollection.offer = ko.observable(undefined);
|
||||
|
||||
const props = { ...baseProps };
|
||||
props.settingsTab.collection = newCollection;
|
||||
|
||||
settingsComponentInstance = new SettingsComponent(props);
|
||||
expect(settingsComponentInstance.shouldShowKeyspaceSharedThroughputMessage()).toEqual(true);
|
||||
});
|
||||
|
||||
it("hasConflictResolution", () => {
|
||||
let settingsComponentInstance = new SettingsComponent(baseProps);
|
||||
expect(settingsComponentInstance.hasConflictResolution()).toEqual(undefined);
|
||||
|
||||
const newContainer = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
newContainer.databaseAccount = ko.observable({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
location: undefined,
|
||||
type: undefined,
|
||||
kind: undefined,
|
||||
tags: undefined,
|
||||
properties: {
|
||||
documentEndpoint: undefined,
|
||||
tableEndpoint: undefined,
|
||||
gremlinEndpoint: undefined,
|
||||
cassandraEndpoint: undefined,
|
||||
enableMultipleWriteLocations: true
|
||||
}
|
||||
});
|
||||
const newCollection = { ...collection };
|
||||
newCollection.container = newContainer;
|
||||
newCollection.conflictResolutionPolicy = ko.observable({
|
||||
mode: DataModels.ConflictResolutionMode.Custom,
|
||||
conflictResolutionProcedure: undefined
|
||||
} as DataModels.ConflictResolutionPolicy);
|
||||
|
||||
const props = { ...baseProps };
|
||||
props.settingsTab.collection = newCollection;
|
||||
|
||||
settingsComponentInstance = new SettingsComponent(props);
|
||||
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 and updateOffer", async () => {
|
||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||
wrapper.setState({ isSubSettingsSaveable: true, isScaleSaveable: true });
|
||||
wrapper.update();
|
||||
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
|
||||
await settingsComponentInstance.onSaveClick();
|
||||
expect(updateCollection).toBeCalled();
|
||||
expect(updateOffer).toBeCalled();
|
||||
});
|
||||
|
||||
it("revert resets state values", async () => {
|
||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||
wrapper.setState({ timeToLive: TtlType.OnNoDefault, throughput: 10 });
|
||||
wrapper.update();
|
||||
let state = wrapper.state() as SettingsComponentState;
|
||||
expect(isDirty(state.timeToLive, state.timeToLiveBaseline)).toEqual(true);
|
||||
expect(isDirty(state.throughput, state.throughputBaseline)).toEqual(true);
|
||||
|
||||
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
|
||||
settingsComponentInstance.onRevertClick();
|
||||
state = wrapper.state() as SettingsComponentState;
|
||||
expect(isDirty(state.timeToLive, state.timeToLiveBaseline)).toEqual(false);
|
||||
expect(isDirty(state.throughput, state.throughputBaseline)).toEqual(false);
|
||||
});
|
||||
|
||||
it("getTtlValue", async () => {
|
||||
const settingsComponentInstance = new SettingsComponent(baseProps);
|
||||
expect(settingsComponentInstance.getTtlValue(TtlType.OnNoDefault)).toEqual(TtlOnNoDefault);
|
||||
expect(settingsComponentInstance.getTtlValue(TtlType.On)).toEqual(TtlOn);
|
||||
expect(settingsComponentInstance.getTtlValue(TtlType.Off)).toEqual(TtlOff);
|
||||
});
|
||||
|
||||
it("getAnalyticalStorageTtl", () => {
|
||||
const newCollection = { ...collection };
|
||||
newCollection.analyticalStorageTtl = ko.observable(10);
|
||||
const props = { ...baseProps };
|
||||
props.settingsTab.collection = newCollection;
|
||||
const wrapper = shallow(<SettingsComponent {...props} />);
|
||||
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
|
||||
expect(settingsComponentInstance.getAnalyticalStorageTtl()).toEqual(10);
|
||||
wrapper.setState({ analyticalStorageTtlSelection: TtlType.Off });
|
||||
wrapper.update();
|
||||
expect(settingsComponentInstance.getAnalyticalStorageTtl()).toEqual(-1);
|
||||
});
|
||||
|
||||
it("getUpdatedConflictResolutionPolicy", () => {
|
||||
const wrapper = shallow(<SettingsComponent {...baseProps} />);
|
||||
const conflictResolutionPolicyPath = "_ts";
|
||||
const conflictResolutionPolicyProcedure = "sample_sproc";
|
||||
const expectSprocPath =
|
||||
"/dbs/" + collection.databaseId + "/colls/" + collection.id() + "/sprocs/" + conflictResolutionPolicyProcedure;
|
||||
|
||||
wrapper.setState({
|
||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||
conflictResolutionPolicyPath: conflictResolutionPolicyPath
|
||||
});
|
||||
wrapper.update();
|
||||
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
|
||||
let conflictResolutionPolicy = settingsComponentInstance.getUpdatedConflictResolutionPolicy();
|
||||
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.LastWriterWins);
|
||||
expect(conflictResolutionPolicy.conflictResolutionPath).toEqual(conflictResolutionPolicyPath);
|
||||
|
||||
wrapper.setState({
|
||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.Custom,
|
||||
conflictResolutionPolicyProcedure: conflictResolutionPolicyProcedure
|
||||
});
|
||||
wrapper.update();
|
||||
conflictResolutionPolicy = settingsComponentInstance.getUpdatedConflictResolutionPolicy();
|
||||
expect(conflictResolutionPolicy.mode).toEqual(DataModels.ConflictResolutionMode.Custom);
|
||||
expect(conflictResolutionPolicy.conflictResolutionProcedure).toEqual(expectSprocPath);
|
||||
});
|
||||
});
|
||||
@@ -1,912 +0,0 @@
|
||||
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 } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import Explorer from "../../Explorer";
|
||||
import { updateOffer } from "../../../Common/DocumentClientUtilityBase";
|
||||
import { updateCollection } 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 { throughputUnit } from "./SettingsRenderUtils";
|
||||
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
||||
import {
|
||||
getMaxRUs,
|
||||
hasDatabaseSharedThroughput,
|
||||
GeospatialConfigType,
|
||||
TtlType,
|
||||
ChangeFeedPolicyState,
|
||||
SettingsV2TabTypes,
|
||||
getTabTitle,
|
||||
isDirty,
|
||||
TtlOff,
|
||||
TtlOn,
|
||||
TtlOnNoDefault,
|
||||
parseConflictResolutionMode,
|
||||
parseConflictResolutionProcedure
|
||||
} from "./SettingsUtils";
|
||||
import {
|
||||
ConflictResolutionComponent,
|
||||
ConflictResolutionComponentProps
|
||||
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
||||
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
|
||||
import { Pivot, PivotItem, IPivotProps, IPivotItemProps, IChoiceGroupOption } from "office-ui-fabric-react";
|
||||
import "./SettingsComponent.less";
|
||||
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./SettingsSubComponents/IndexingPolicyComponent";
|
||||
|
||||
interface SettingsV2TabInfo {
|
||||
tab: SettingsV2TabTypes;
|
||||
content: JSX.Element;
|
||||
}
|
||||
|
||||
interface ButtonV2 {
|
||||
isVisible: () => boolean;
|
||||
isEnabled: () => boolean;
|
||||
isSelected?: () => boolean;
|
||||
}
|
||||
|
||||
export interface SettingsComponentProps {
|
||||
settingsTab: SettingsTab;
|
||||
}
|
||||
|
||||
export interface SettingsComponentState {
|
||||
throughput: number;
|
||||
throughputBaseline: number;
|
||||
autoPilotThroughput: number;
|
||||
autoPilotThroughputBaseline: number;
|
||||
isAutoPilotSelected: boolean;
|
||||
wasAutopilotOriginallySet: boolean;
|
||||
isScaleSaveable: boolean;
|
||||
isScaleDiscardable: boolean;
|
||||
|
||||
timeToLive: TtlType;
|
||||
timeToLiveBaseline: TtlType;
|
||||
timeToLiveSeconds: number;
|
||||
timeToLiveSecondsBaseline: number;
|
||||
geospatialConfigType: GeospatialConfigType;
|
||||
geospatialConfigTypeBaseline: GeospatialConfigType;
|
||||
analyticalStorageTtlSelection: TtlType;
|
||||
analyticalStorageTtlSelectionBaseline: TtlType;
|
||||
analyticalStorageTtlSeconds: number;
|
||||
analyticalStorageTtlSecondsBaseline: number;
|
||||
changeFeedPolicy: ChangeFeedPolicyState;
|
||||
changeFeedPolicyBaseline: ChangeFeedPolicyState;
|
||||
isSubSettingsSaveable: boolean;
|
||||
isSubSettingsDiscardable: boolean;
|
||||
|
||||
indexingPolicyContent: DataModels.IndexingPolicy;
|
||||
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
|
||||
shouldDiscardIndexingPolicy: boolean;
|
||||
indexingPolicyElementFocussed: boolean;
|
||||
isIndexingPolicyDirty: boolean;
|
||||
|
||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
|
||||
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
|
||||
conflictResolutionPolicyPath: string;
|
||||
conflictResolutionPolicyPathBaseline: string;
|
||||
conflictResolutionPolicyProcedure: string;
|
||||
conflictResolutionPolicyProcedureBaseline: string;
|
||||
isConflictResolutionDirty: boolean;
|
||||
|
||||
initialNotification: DataModels.Notification;
|
||||
selectedTab: SettingsV2TabTypes;
|
||||
}
|
||||
|
||||
export class SettingsComponent extends React.Component<SettingsComponentProps, SettingsComponentState> {
|
||||
private static readonly sixMonthsInSeconds = 15768000;
|
||||
private static readonly zeroSeconds = 0;
|
||||
|
||||
public saveSettingsButton: ButtonV2;
|
||||
public discardSettingsChangesButton: ButtonV2;
|
||||
|
||||
public isAnalyticalStorageEnabled: boolean;
|
||||
private collection: ViewModels.Collection;
|
||||
private container: Explorer;
|
||||
private changeFeedPolicyVisible: boolean;
|
||||
private isFixedContainer: boolean;
|
||||
private autoPilotTiersList: ViewModels.DropdownOption<DataModels.AutopilotTier>[];
|
||||
private shouldShowIndexingPolicyEditor: boolean;
|
||||
|
||||
constructor(props: SettingsComponentProps) {
|
||||
super(props);
|
||||
|
||||
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
|
||||
this.container = this.collection?.container;
|
||||
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
||||
this.shouldShowIndexingPolicyEditor =
|
||||
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB();
|
||||
|
||||
this.changeFeedPolicyVisible = this.collection?.container.isFeatureEnabled(
|
||||
Constants.Features.enableChangeFeedPolicy
|
||||
);
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
|
||||
this.isFixedContainer =
|
||||
!this.collection.partitionKey ||
|
||||
(this.container.isPreferredApiMongoDB() && this.collection.partitionKey.systemKey);
|
||||
|
||||
this.state = {
|
||||
throughput: undefined,
|
||||
throughputBaseline: undefined,
|
||||
autoPilotThroughput: undefined,
|
||||
autoPilotThroughputBaseline: undefined,
|
||||
isAutoPilotSelected: false,
|
||||
wasAutopilotOriginallySet: false,
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
|
||||
timeToLive: undefined,
|
||||
timeToLiveBaseline: undefined,
|
||||
timeToLiveSeconds: undefined,
|
||||
timeToLiveSecondsBaseline: undefined,
|
||||
geospatialConfigType: undefined,
|
||||
geospatialConfigTypeBaseline: undefined,
|
||||
analyticalStorageTtlSelection: undefined,
|
||||
analyticalStorageTtlSelectionBaseline: undefined,
|
||||
analyticalStorageTtlSeconds: undefined,
|
||||
analyticalStorageTtlSecondsBaseline: undefined,
|
||||
changeFeedPolicy: undefined,
|
||||
changeFeedPolicyBaseline: undefined,
|
||||
isSubSettingsSaveable: false,
|
||||
isSubSettingsDiscardable: false,
|
||||
|
||||
indexingPolicyContent: undefined,
|
||||
indexingPolicyContentBaseline: undefined,
|
||||
indexingPolicyElementFocussed: false,
|
||||
shouldDiscardIndexingPolicy: false,
|
||||
isIndexingPolicyDirty: false,
|
||||
|
||||
conflictResolutionPolicyMode: undefined,
|
||||
conflictResolutionPolicyModeBaseline: undefined,
|
||||
conflictResolutionPolicyPath: undefined,
|
||||
conflictResolutionPolicyPathBaseline: undefined,
|
||||
conflictResolutionPolicyProcedure: undefined,
|
||||
conflictResolutionPolicyProcedureBaseline: undefined,
|
||||
isConflictResolutionDirty: false,
|
||||
|
||||
initialNotification: undefined,
|
||||
selectedTab: SettingsV2TabTypes.ScaleTab
|
||||
};
|
||||
|
||||
this.saveSettingsButton = {
|
||||
isEnabled: this.isSaveSettingsButtonEnabled,
|
||||
isVisible: () => {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
this.discardSettingsChangesButton = {
|
||||
isEnabled: this.isDiscardSettingsButtonEnabled,
|
||||
isVisible: () => {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.setAutoPilotStates();
|
||||
this.setBaseline();
|
||||
if (this.props.settingsTab.isActive()) {
|
||||
this.props.settingsTab.getSettingsTabContainer().onUpdateTabsButtons(this.getTabsButtons());
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
if (this.props.settingsTab.isActive()) {
|
||||
this.props.settingsTab.getSettingsTabContainer().onUpdateTabsButtons(this.getTabsButtons());
|
||||
}
|
||||
}
|
||||
|
||||
public isSaveSettingsButtonEnabled = (): boolean => {
|
||||
if (this.isOfferReplacePending()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
this.state.isScaleSaveable ||
|
||||
this.state.isSubSettingsSaveable ||
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty
|
||||
);
|
||||
};
|
||||
|
||||
public isDiscardSettingsButtonEnabled = (): boolean => {
|
||||
return (
|
||||
this.state.isScaleDiscardable ||
|
||||
this.state.isSubSettingsDiscardable ||
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty
|
||||
);
|
||||
};
|
||||
|
||||
private setAutoPilotStates = (): void => {
|
||||
const offer = this.collection?.offer && this.collection.offer();
|
||||
const offerAutopilotSettings = offer?.content?.offerAutopilotSettings;
|
||||
|
||||
if (
|
||||
offerAutopilotSettings &&
|
||||
offerAutopilotSettings.maxThroughput &&
|
||||
AutoPilotUtils.isValidAutoPilotThroughput(offerAutopilotSettings.maxThroughput)
|
||||
) {
|
||||
this.setState({
|
||||
isAutoPilotSelected: true,
|
||||
wasAutopilotOriginallySet: true,
|
||||
autoPilotThroughput: offerAutopilotSettings.maxThroughput,
|
||||
autoPilotThroughputBaseline: offerAutopilotSettings.maxThroughput
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
public hasProvisioningTypeChanged = (): boolean =>
|
||||
this.state.wasAutopilotOriginallySet !== this.state.isAutoPilotSelected;
|
||||
|
||||
public shouldShowKeyspaceSharedThroughputMessage = (): boolean =>
|
||||
this.container && this.container.isPreferredApiCassandra() && hasDatabaseSharedThroughput(this.collection);
|
||||
|
||||
public hasConflictResolution = (): boolean =>
|
||||
this.container?.databaseAccount &&
|
||||
this.container.databaseAccount()?.properties?.enableMultipleWriteLocations &&
|
||||
this.collection.conflictResolutionPolicy &&
|
||||
!!this.collection.conflictResolutionPolicy();
|
||||
|
||||
public isOfferReplacePending = (): boolean => {
|
||||
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> => {
|
||||
this.props.settingsTab.isExecutionError(false);
|
||||
|
||||
this.props.settingsTab.isExecuting(true);
|
||||
const startKey: number = traceStart(Action.UpdateSettings, {
|
||||
databaseAccountName: this.container.databaseAccount()?.name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.props.settingsTab.tabTitle()
|
||||
});
|
||||
|
||||
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
|
||||
|
||||
try {
|
||||
if (
|
||||
this.state.isSubSettingsSaveable ||
|
||||
this.state.isIndexingPolicyDirty ||
|
||||
this.state.isConflictResolutionDirty
|
||||
) {
|
||||
let defaultTtl: number;
|
||||
switch (this.state.timeToLive) {
|
||||
case TtlType.On:
|
||||
defaultTtl = Number(this.state.timeToLiveSeconds);
|
||||
break;
|
||||
case TtlType.OnNoDefault:
|
||||
defaultTtl = -1;
|
||||
break;
|
||||
case TtlType.Off:
|
||||
default:
|
||||
defaultTtl = undefined;
|
||||
break;
|
||||
}
|
||||
|
||||
newCollection.defaultTtl = defaultTtl;
|
||||
|
||||
newCollection.indexingPolicy = this.state.indexingPolicyContent;
|
||||
|
||||
newCollection.changeFeedPolicy =
|
||||
this.changeFeedPolicyVisible && this.state.changeFeedPolicy === ChangeFeedPolicyState.On
|
||||
? {
|
||||
retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration
|
||||
}
|
||||
: undefined;
|
||||
|
||||
newCollection.analyticalStorageTtl = this.getAnalyticalStorageTtl();
|
||||
|
||||
newCollection.geospatialConfig = {
|
||||
type: this.state.geospatialConfigType
|
||||
};
|
||||
|
||||
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
|
||||
if (conflictResolutionChanges) {
|
||||
newCollection.conflictResolutionPolicy = conflictResolutionChanges;
|
||||
}
|
||||
|
||||
const updatedCollection: DataModels.Collection = await updateCollection(
|
||||
this.collection.databaseId,
|
||||
this.collection.id(),
|
||||
newCollection
|
||||
);
|
||||
this.collection.rawDataModel = updatedCollection;
|
||||
this.collection.defaultTtl(updatedCollection.defaultTtl);
|
||||
this.collection.analyticalStorageTtl(updatedCollection.analyticalStorageTtl);
|
||||
this.collection.id(updatedCollection.id);
|
||||
this.collection.indexingPolicy(updatedCollection.indexingPolicy);
|
||||
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
|
||||
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
||||
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
||||
this.setState({
|
||||
isSubSettingsSaveable: false,
|
||||
isSubSettingsDiscardable: false,
|
||||
isIndexingPolicyDirty: false,
|
||||
isConflictResolutionDirty: false
|
||||
});
|
||||
}
|
||||
|
||||
if (this.state.isScaleSaveable) {
|
||||
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 headerOptions: RequestOptions = { initialHeaders: {} };
|
||||
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
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({
|
||||
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
|
||||
};
|
||||
try {
|
||||
await updateOfferThroughputBeyondLimit(requestPayload);
|
||||
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
||||
this.setState({
|
||||
throughput: originalThroughputValue,
|
||||
throughputBaseline: originalThroughputValue,
|
||||
initialNotification: {
|
||||
description: `Throughput update for ${newThroughput} ${throughputUnit}`
|
||||
} as DataModels.Notification
|
||||
});
|
||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||
} catch (error) {
|
||||
traceFailure(
|
||||
Action.UpdateSettings,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
databaseName: this.collection && this.collection.databaseId,
|
||||
collectionName: this.collection && this.collection.id(),
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.props.settingsTab.tabTitle(),
|
||||
error: error
|
||||
},
|
||||
startKey
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(this.collection.offer(), newOffer, headerOptions);
|
||||
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);
|
||||
this.setBaseline();
|
||||
this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected });
|
||||
traceSuccess(
|
||||
Action.UpdateSettings,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount()?.name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.props.settingsTab.tabTitle()
|
||||
},
|
||||
startKey
|
||||
);
|
||||
} catch (reason) {
|
||||
this.container.isRefreshingExplorer(false);
|
||||
this.props.settingsTab.isExecutionError(true);
|
||||
console.error(reason);
|
||||
traceFailure(
|
||||
Action.UpdateSettings,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount()?.name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.props.settingsTab.tabTitle()
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
this.props.settingsTab.isExecuting(false);
|
||||
};
|
||||
|
||||
public onRevertClick = (): void => {
|
||||
this.setState({
|
||||
throughput: this.state.throughputBaseline,
|
||||
timeToLive: this.state.timeToLiveBaseline,
|
||||
timeToLiveSeconds: this.state.timeToLiveSecondsBaseline,
|
||||
geospatialConfigType: this.state.geospatialConfigTypeBaseline,
|
||||
indexingPolicyContent: this.state.indexingPolicyContentBaseline,
|
||||
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyModeBaseline,
|
||||
conflictResolutionPolicyPath: this.state.conflictResolutionPolicyPathBaseline,
|
||||
conflictResolutionPolicyProcedure: this.state.conflictResolutionPolicyProcedureBaseline,
|
||||
analyticalStorageTtlSelection: this.state.analyticalStorageTtlSelectionBaseline,
|
||||
analyticalStorageTtlSeconds: this.state.analyticalStorageTtlSecondsBaseline,
|
||||
changeFeedPolicy: this.state.changeFeedPolicyBaseline,
|
||||
autoPilotThroughput: this.state.autoPilotThroughputBaseline,
|
||||
isAutoPilotSelected: this.state.wasAutopilotOriginallySet,
|
||||
shouldDiscardIndexingPolicy: true,
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
isSubSettingsSaveable: false,
|
||||
isSubSettingsDiscardable: false,
|
||||
isIndexingPolicyDirty: false,
|
||||
isConflictResolutionDirty: false
|
||||
});
|
||||
};
|
||||
|
||||
private onScaleSaveableChange = (isScaleSaveable: boolean): void =>
|
||||
this.setState({ isScaleSaveable: isScaleSaveable });
|
||||
|
||||
private onScaleDiscardableChange = (isScaleDiscardable: boolean): void =>
|
||||
this.setState({ isScaleDiscardable: isScaleDiscardable });
|
||||
|
||||
private onIndexingPolicyElementFocusChange = (indexingPolicyElementFocussed: boolean): void =>
|
||||
this.setState({ indexingPolicyElementFocussed: indexingPolicyElementFocussed });
|
||||
|
||||
private onIndexingPolicyContentChange = (newIndexingPolicy: DataModels.IndexingPolicy): void =>
|
||||
this.setState({ indexingPolicyContent: newIndexingPolicy });
|
||||
|
||||
private resetShouldDiscardIndexingPolicy = (): void => this.setState({ shouldDiscardIndexingPolicy: false });
|
||||
|
||||
private logIndexingPolicySuccessMessage = (): void => {
|
||||
if (this.props.settingsTab.onLoadStartKey) {
|
||||
traceSuccess(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
databaseName: this.collection.databaseId,
|
||||
collectionName: this.collection.id(),
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.props.settingsTab.tabTitle()
|
||||
},
|
||||
this.props.settingsTab.onLoadStartKey
|
||||
);
|
||||
this.props.settingsTab.onLoadStartKey = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
private onConflictResolutionPolicyModeChange = (
|
||||
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void =>
|
||||
this.setState({
|
||||
conflictResolutionPolicyMode:
|
||||
DataModels.ConflictResolutionMode[option.key as keyof typeof DataModels.ConflictResolutionMode]
|
||||
});
|
||||
|
||||
private onConflictResolutionPolicyPathChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => this.setState({ conflictResolutionPolicyPath: newValue });
|
||||
|
||||
private onConflictResolutionPolicyProcedureChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => this.setState({ conflictResolutionPolicyProcedure: newValue });
|
||||
|
||||
private onConflictResolutionDirtyChange = (isConflictResolutionDirty: boolean): void =>
|
||||
this.setState({ isConflictResolutionDirty: isConflictResolutionDirty });
|
||||
|
||||
public getTtlValue = (value: string): TtlType => {
|
||||
switch (value) {
|
||||
case TtlOn:
|
||||
return TtlType.On;
|
||||
case TtlOff:
|
||||
return TtlType.Off;
|
||||
case TtlOnNoDefault:
|
||||
return TtlType.OnNoDefault;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private onTtlChange = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption): void =>
|
||||
this.setState({ timeToLive: this.getTtlValue(option.key) });
|
||||
|
||||
private onTimeToLiveSecondsChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
let newTimeToLiveSeconds = parseInt(newValue);
|
||||
newTimeToLiveSeconds = isNaN(newTimeToLiveSeconds) ? SettingsComponent.zeroSeconds : newTimeToLiveSeconds;
|
||||
this.setState({ timeToLiveSeconds: newTimeToLiveSeconds });
|
||||
};
|
||||
|
||||
private onGeoSpatialConfigTypeChange = (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void =>
|
||||
this.setState({ geospatialConfigType: GeospatialConfigType[option.key as keyof typeof GeospatialConfigType] });
|
||||
|
||||
private onAnalyticalStorageTtlSelectionChange = (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void => this.setState({ analyticalStorageTtlSelection: this.getTtlValue(option.key) });
|
||||
|
||||
private onAnalyticalStorageTtlSecondsChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
let newAnalyticalStorageTtlSeconds = parseInt(newValue);
|
||||
newAnalyticalStorageTtlSeconds = isNaN(newAnalyticalStorageTtlSeconds)
|
||||
? SettingsComponent.zeroSeconds
|
||||
: newAnalyticalStorageTtlSeconds;
|
||||
this.setState({ analyticalStorageTtlSeconds: newAnalyticalStorageTtlSeconds });
|
||||
};
|
||||
|
||||
private onChangeFeedPolicyChange = (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void =>
|
||||
this.setState({ changeFeedPolicy: ChangeFeedPolicyState[option.key as keyof typeof ChangeFeedPolicyState] });
|
||||
|
||||
private onSubSettingsSaveableChange = (isSubSettingsSaveable: boolean): void =>
|
||||
this.setState({ isSubSettingsSaveable: isSubSettingsSaveable });
|
||||
|
||||
private onSubSettingsDiscardableChange = (isSubSettingsDiscardable: boolean): void =>
|
||||
this.setState({ isSubSettingsDiscardable: isSubSettingsDiscardable });
|
||||
|
||||
private onIndexingPolicyDirtyChange = (isIndexingPolicyDirty: boolean): void =>
|
||||
this.setState({ isIndexingPolicyDirty: isIndexingPolicyDirty });
|
||||
|
||||
public getAnalyticalStorageTtl = (): number => {
|
||||
if (this.isAnalyticalStorageEnabled) {
|
||||
if (this.state.analyticalStorageTtlSelection === TtlType.On) {
|
||||
return Number(this.state.analyticalStorageTtlSeconds);
|
||||
} else {
|
||||
return Constants.AnalyticalStorageTtl.Infinite;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
public getUpdatedConflictResolutionPolicy = (): DataModels.ConflictResolutionPolicy => {
|
||||
if (
|
||||
!isDirty(this.state.conflictResolutionPolicyMode, this.state.conflictResolutionPolicyModeBaseline) &&
|
||||
!isDirty(this.state.conflictResolutionPolicyPath, this.state.conflictResolutionPolicyPathBaseline) &&
|
||||
!isDirty(this.state.conflictResolutionPolicyProcedure, this.state.conflictResolutionPolicyProcedureBaseline)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const policy: DataModels.ConflictResolutionPolicy = {
|
||||
mode: parseConflictResolutionMode(this.state.conflictResolutionPolicyMode)
|
||||
};
|
||||
|
||||
if (
|
||||
policy.mode === DataModels.ConflictResolutionMode.Custom &&
|
||||
this.state.conflictResolutionPolicyProcedure?.length > 0
|
||||
) {
|
||||
policy.conflictResolutionProcedure = Constants.HashRoutePrefixes.sprocWithIds(
|
||||
this.collection.databaseId,
|
||||
this.collection.id(),
|
||||
this.state.conflictResolutionPolicyProcedure,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
if (policy.mode === DataModels.ConflictResolutionMode.LastWriterWins) {
|
||||
policy.conflictResolutionPath = this.state.conflictResolutionPolicyPath;
|
||||
if (policy.conflictResolutionPath?.startsWith("/")) {
|
||||
policy.conflictResolutionPath = "/" + policy.conflictResolutionPath;
|
||||
}
|
||||
}
|
||||
|
||||
return policy;
|
||||
};
|
||||
|
||||
public setBaseline = (): void => {
|
||||
const defaultTtl = this.collection.defaultTtl();
|
||||
|
||||
let timeToLive: TtlType = this.state.timeToLive;
|
||||
let timeToLiveSeconds = this.state.timeToLiveSeconds;
|
||||
switch (defaultTtl) {
|
||||
case undefined:
|
||||
case 0:
|
||||
timeToLive = TtlType.Off;
|
||||
timeToLiveSeconds = SettingsComponent.sixMonthsInSeconds;
|
||||
break;
|
||||
case -1:
|
||||
timeToLive = TtlType.OnNoDefault;
|
||||
timeToLiveSeconds = SettingsComponent.sixMonthsInSeconds;
|
||||
break;
|
||||
default:
|
||||
timeToLive = TtlType.On;
|
||||
timeToLiveSeconds = defaultTtl;
|
||||
break;
|
||||
}
|
||||
|
||||
let analyticalStorageTtlSelection: TtlType;
|
||||
let analyticalStorageTtlSeconds: number;
|
||||
if (this.isAnalyticalStorageEnabled) {
|
||||
const analyticalStorageTtl: number = this.collection.analyticalStorageTtl();
|
||||
if (analyticalStorageTtl === Constants.AnalyticalStorageTtl.Infinite) {
|
||||
analyticalStorageTtlSelection = TtlType.OnNoDefault;
|
||||
} else {
|
||||
analyticalStorageTtlSelection = TtlType.On;
|
||||
analyticalStorageTtlSeconds = analyticalStorageTtl;
|
||||
}
|
||||
}
|
||||
|
||||
const offerThroughput = this.collection?.offer && this.collection.offer()?.content?.offerThroughput;
|
||||
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
||||
? ChangeFeedPolicyState.On
|
||||
: ChangeFeedPolicyState.Off;
|
||||
const indexingPolicyContent = this.collection.indexingPolicy();
|
||||
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
||||
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
||||
|
||||
const conflictResolutionPolicyMode = parseConflictResolutionMode(conflictResolutionPolicy?.mode);
|
||||
const conflictResolutionPolicyPath = conflictResolutionPolicy?.conflictResolutionPath;
|
||||
const conflictResolutionPolicyProcedure = parseConflictResolutionProcedure(
|
||||
conflictResolutionPolicy?.conflictResolutionProcedure
|
||||
);
|
||||
const geospatialConfigTypeString: string =
|
||||
(this.collection.geospatialConfig && this.collection.geospatialConfig()?.type) || GeospatialConfigType.Geometry;
|
||||
const geoSpatialConfigType = GeospatialConfigType[geospatialConfigTypeString as keyof typeof GeospatialConfigType];
|
||||
|
||||
this.setState({
|
||||
throughput: offerThroughput,
|
||||
throughputBaseline: offerThroughput,
|
||||
changeFeedPolicy: changeFeedPolicy,
|
||||
changeFeedPolicyBaseline: changeFeedPolicy,
|
||||
timeToLive: timeToLive,
|
||||
timeToLiveBaseline: timeToLive,
|
||||
timeToLiveSeconds: timeToLiveSeconds,
|
||||
timeToLiveSecondsBaseline: timeToLiveSeconds,
|
||||
analyticalStorageTtlSelection: analyticalStorageTtlSelection,
|
||||
analyticalStorageTtlSelectionBaseline: analyticalStorageTtlSelection,
|
||||
analyticalStorageTtlSeconds: analyticalStorageTtlSeconds,
|
||||
analyticalStorageTtlSecondsBaseline: analyticalStorageTtlSeconds,
|
||||
indexingPolicyContent: indexingPolicyContent,
|
||||
indexingPolicyContentBaseline: indexingPolicyContent,
|
||||
conflictResolutionPolicyMode: conflictResolutionPolicyMode,
|
||||
conflictResolutionPolicyModeBaseline: conflictResolutionPolicyMode,
|
||||
conflictResolutionPolicyPath: conflictResolutionPolicyPath,
|
||||
conflictResolutionPolicyPathBaseline: conflictResolutionPolicyPath,
|
||||
conflictResolutionPolicyProcedure: conflictResolutionPolicyProcedure,
|
||||
conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure,
|
||||
geospatialConfigType: geoSpatialConfigType,
|
||||
geospatialConfigTypeBaseline: geoSpatialConfigType
|
||||
});
|
||||
};
|
||||
|
||||
private getTabsButtons = (): CommandButtonComponentProps[] => {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
if (this.saveSettingsButton.isVisible()) {
|
||||
const label = "Save";
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: async () => await this.onSaveClick(),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.saveSettingsButton.isEnabled()
|
||||
});
|
||||
}
|
||||
|
||||
if (this.discardSettingsChangesButton.isVisible()) {
|
||||
const label = "Discard";
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: this.onRevertClick,
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.discardSettingsChangesButton.isEnabled()
|
||||
});
|
||||
}
|
||||
return buttons;
|
||||
};
|
||||
|
||||
private onMaxAutoPilotThroughputChange = (newThroughput: number): void =>
|
||||
this.setState({ autoPilotThroughput: newThroughput });
|
||||
|
||||
private onThroughputChange = (newThroughput: number): void => this.setState({ throughput: newThroughput });
|
||||
|
||||
private onAutoPilotSelected = (isAutoPilotSelected: boolean): void =>
|
||||
this.setState({ isAutoPilotSelected: isAutoPilotSelected });
|
||||
|
||||
private onPivotChange = (item: PivotItem): void => {
|
||||
const selectedTab = SettingsV2TabTypes[item.props.itemKey as keyof typeof SettingsV2TabTypes];
|
||||
this.setState({ selectedTab: selectedTab });
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const scaleComponentProps: ScaleComponentProps = {
|
||||
collection: this.collection,
|
||||
container: this.container,
|
||||
isFixedContainer: this.isFixedContainer,
|
||||
autoPilotTiersList: this.autoPilotTiersList,
|
||||
onThroughputChange: this.onThroughputChange,
|
||||
throughput: this.state.throughput,
|
||||
throughputBaseline: this.state.throughputBaseline,
|
||||
autoPilotThroughput: this.state.autoPilotThroughput,
|
||||
autoPilotThroughputBaseline: this.state.autoPilotThroughputBaseline,
|
||||
isAutoPilotSelected: this.state.isAutoPilotSelected,
|
||||
wasAutopilotOriginallySet: this.state.wasAutopilotOriginallySet,
|
||||
onAutoPilotSelected: this.onAutoPilotSelected,
|
||||
onMaxAutoPilotThroughputChange: this.onMaxAutoPilotThroughputChange,
|
||||
onScaleSaveableChange: this.onScaleSaveableChange,
|
||||
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
||||
initialNotification: this.props.settingsTab.pendingNotification()
|
||||
};
|
||||
|
||||
const subSettingsComponentProps: SubSettingsComponentProps = {
|
||||
collection: this.collection,
|
||||
container: this.container,
|
||||
isAnalyticalStorageEnabled: this.isAnalyticalStorageEnabled,
|
||||
changeFeedPolicyVisible: this.changeFeedPolicyVisible,
|
||||
timeToLive: this.state.timeToLive,
|
||||
timeToLiveBaseline: this.state.timeToLiveBaseline,
|
||||
onTtlChange: this.onTtlChange,
|
||||
timeToLiveSeconds: this.state.timeToLiveSeconds,
|
||||
timeToLiveSecondsBaseline: this.state.timeToLiveSecondsBaseline,
|
||||
onTimeToLiveSecondsChange: this.onTimeToLiveSecondsChange,
|
||||
geospatialConfigType: this.state.geospatialConfigType,
|
||||
geospatialConfigTypeBaseline: this.state.geospatialConfigTypeBaseline,
|
||||
onGeoSpatialConfigTypeChange: this.onGeoSpatialConfigTypeChange,
|
||||
analyticalStorageTtlSelection: this.state.analyticalStorageTtlSelection,
|
||||
analyticalStorageTtlSelectionBaseline: this.state.analyticalStorageTtlSelectionBaseline,
|
||||
onAnalyticalStorageTtlSelectionChange: this.onAnalyticalStorageTtlSelectionChange,
|
||||
analyticalStorageTtlSeconds: this.state.analyticalStorageTtlSeconds,
|
||||
analyticalStorageTtlSecondsBaseline: this.state.analyticalStorageTtlSecondsBaseline,
|
||||
onAnalyticalStorageTtlSecondsChange: this.onAnalyticalStorageTtlSecondsChange,
|
||||
changeFeedPolicy: this.state.changeFeedPolicy,
|
||||
changeFeedPolicyBaseline: this.state.changeFeedPolicyBaseline,
|
||||
onChangeFeedPolicyChange: this.onChangeFeedPolicyChange,
|
||||
onSubSettingsSaveableChange: this.onSubSettingsSaveableChange,
|
||||
onSubSettingsDiscardableChange: this.onSubSettingsDiscardableChange
|
||||
};
|
||||
|
||||
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
|
||||
shouldDiscardIndexingPolicy: this.state.shouldDiscardIndexingPolicy,
|
||||
resetShouldDiscardIndexingPolicy: this.resetShouldDiscardIndexingPolicy,
|
||||
indexingPolicyContent: this.state.indexingPolicyContent,
|
||||
indexingPolicyContentBaseline: this.state.indexingPolicyContentBaseline,
|
||||
onIndexingPolicyElementFocusChange: this.onIndexingPolicyElementFocusChange,
|
||||
onIndexingPolicyContentChange: this.onIndexingPolicyContentChange,
|
||||
logIndexingPolicySuccessMessage: this.logIndexingPolicySuccessMessage,
|
||||
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange
|
||||
};
|
||||
|
||||
const conflictResolutionPolicyComponentProps: ConflictResolutionComponentProps = {
|
||||
collection: this.collection,
|
||||
container: this.container,
|
||||
conflictResolutionPolicyMode: this.state.conflictResolutionPolicyMode,
|
||||
conflictResolutionPolicyModeBaseline: this.state.conflictResolutionPolicyModeBaseline,
|
||||
onConflictResolutionPolicyModeChange: this.onConflictResolutionPolicyModeChange,
|
||||
conflictResolutionPolicyPath: this.state.conflictResolutionPolicyPath,
|
||||
conflictResolutionPolicyPathBaseline: this.state.conflictResolutionPolicyPathBaseline,
|
||||
onConflictResolutionPolicyPathChange: this.onConflictResolutionPolicyPathChange,
|
||||
conflictResolutionPolicyProcedure: this.state.conflictResolutionPolicyProcedure,
|
||||
conflictResolutionPolicyProcedureBaseline: this.state.conflictResolutionPolicyProcedureBaseline,
|
||||
onConflictResolutionPolicyProcedureChange: this.onConflictResolutionPolicyProcedureChange,
|
||||
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange
|
||||
};
|
||||
|
||||
const tabs: SettingsV2TabInfo[] = [];
|
||||
if (!hasDatabaseSharedThroughput(this.collection)) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.ScaleTab,
|
||||
content: <ScaleComponent {...scaleComponentProps} />
|
||||
});
|
||||
}
|
||||
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.SubSettingsTab,
|
||||
content: <SubSettingsComponent {...subSettingsComponentProps} />
|
||||
});
|
||||
|
||||
if (this.shouldShowIndexingPolicyEditor) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hasConflictResolution()) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.ConflictResolutionTab,
|
||||
content: <ConflictResolutionComponent {...conflictResolutionPolicyComponentProps} />
|
||||
});
|
||||
}
|
||||
|
||||
const pivotProps: IPivotProps = {
|
||||
onLinkClick: this.onPivotChange,
|
||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab]
|
||||
};
|
||||
|
||||
const pivotItems = tabs.map(tab => {
|
||||
const pivotItemProps: IPivotItemProps = {
|
||||
itemKey: SettingsV2TabTypes[tab.tab],
|
||||
style: { marginTop: 20 },
|
||||
headerText: getTabTitle(tab.tab)
|
||||
};
|
||||
|
||||
return (
|
||||
<PivotItem key={pivotItemProps.itemKey} {...pivotItemProps}>
|
||||
{tab.content}
|
||||
</PivotItem>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="settingsV2MainContainer">
|
||||
{this.shouldShowKeyspaceSharedThroughputMessage() && (
|
||||
<div>This table shared throughput is configured at the keyspace</div>
|
||||
)}
|
||||
|
||||
<div className="settingsV2TabsContainer">
|
||||
<Pivot {...pivotProps}>{pivotItems}</Pivot>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import { SettingsComponent, SettingsComponentProps } from "./SettingsComponent";
|
||||
|
||||
export class SettingsComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
constructor(private props: SettingsComponentProps) {
|
||||
this.parameters = ko.observable<number>(Date.now());
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return <SettingsComponent {...this.props} />;
|
||||
}
|
||||
|
||||
public triggerRender(): void {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import {
|
||||
getAutoPilotV3SpendElement,
|
||||
getEstimatedSpendElement,
|
||||
getEstimatedAutoscaleSpendElement,
|
||||
manualToAutoscaleDisclaimerElement,
|
||||
ttlWarning,
|
||||
indexingPolicyTTLWarningMessage,
|
||||
updateThroughputBeyondLimitWarningMessage,
|
||||
updateThroughputDelayedApplyWarningMessage,
|
||||
getThroughputApplyDelayedMessage,
|
||||
getThroughputApplyShortDelayMessage,
|
||||
getThroughputApplyLongDelayMessage,
|
||||
getToolTipContainer,
|
||||
conflictResolutionCustomToolTip,
|
||||
changeFeedPolicyToolTip,
|
||||
conflictResolutionLwwTooltip
|
||||
} from "./SettingsRenderUtils";
|
||||
|
||||
class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{getAutoPilotV3SpendElement(1000, false)}
|
||||
{getAutoPilotV3SpendElement(undefined, false)}
|
||||
{getAutoPilotV3SpendElement(1000, true)}
|
||||
{getAutoPilotV3SpendElement(undefined, true)}
|
||||
|
||||
{getEstimatedSpendElement(1000, "mooncake", 2, false, true)}
|
||||
|
||||
{getEstimatedAutoscaleSpendElement(1000, "mooncake", 2, false)}
|
||||
|
||||
{manualToAutoscaleDisclaimerElement}
|
||||
{ttlWarning}
|
||||
{indexingPolicyTTLWarningMessage}
|
||||
{updateThroughputBeyondLimitWarningMessage}
|
||||
{updateThroughputDelayedApplyWarningMessage}
|
||||
|
||||
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
{getThroughputApplyShortDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
{getThroughputApplyLongDelayMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
|
||||
{getToolTipContainer(<span>Sample Text</span>)}
|
||||
{conflictResolutionLwwTooltip}
|
||||
{conflictResolutionCustomToolTip}
|
||||
{changeFeedPolicyToolTip}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
describe("SettingsUtils functions", () => {
|
||||
it("render", () => {
|
||||
const wrapper = shallow(<SettingsRenderUtilsTestComponent />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,349 +0,0 @@
|
||||
import * as React from "react";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
|
||||
import { Urls, StyleConstants } from "../../../Common/Constants";
|
||||
import {
|
||||
computeAutoscaleUsagePriceHourly,
|
||||
getPriceCurrency,
|
||||
getCurrencySign,
|
||||
getAutoscalePricePerRu,
|
||||
getMultimasterMultiplier,
|
||||
computeRUUsagePriceHourly,
|
||||
getPricePerRu,
|
||||
calculateEstimateNumber
|
||||
} from "../../../Utils/PricingUtils";
|
||||
import {
|
||||
ITextFieldStyles,
|
||||
ICheckboxStyles,
|
||||
IStackProps,
|
||||
IStackTokens,
|
||||
IChoiceGroupStyles,
|
||||
Link,
|
||||
Text,
|
||||
IMessageBarStyles,
|
||||
ITextStyles
|
||||
} from "office-ui-fabric-react";
|
||||
import { isDirtyTypes, isDirty } from "./SettingsUtils";
|
||||
|
||||
const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 12 } };
|
||||
|
||||
export const spendAckCheckBoxStyle: ICheckboxStyles = {
|
||||
label: {
|
||||
margin: 0,
|
||||
padding: "2 0 2 0"
|
||||
},
|
||||
text: {
|
||||
fontSize: 12
|
||||
}
|
||||
};
|
||||
|
||||
export const subComponentStackProps: Partial<IStackProps> = {
|
||||
tokens: { childrenGap: 20 }
|
||||
};
|
||||
|
||||
export const titleAndInputStackProps: Partial<IStackProps> = {
|
||||
tokens: { childrenGap: 5 }
|
||||
};
|
||||
|
||||
export const checkBoxAndInputStackProps: Partial<IStackProps> = {
|
||||
tokens: { childrenGap: 10 }
|
||||
};
|
||||
|
||||
export const toolTipLabelStackTokens: IStackTokens = {
|
||||
childrenGap: 6
|
||||
};
|
||||
|
||||
export const messageBarStyles: Partial<IMessageBarStyles> = { root: { marginTop: "5px" } };
|
||||
|
||||
export const throughputUnit = "RU/s";
|
||||
|
||||
export const getAutoPilotV3SpendElement = (
|
||||
maxAutoPilotThroughputSet: number,
|
||||
isDatabaseThroughput: boolean,
|
||||
requestUnitsUsageCostElement?: JSX.Element
|
||||
): JSX.Element => {
|
||||
if (!maxAutoPilotThroughputSet) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const resource: string = isDatabaseThroughput ? "database" : "container";
|
||||
return (
|
||||
<>
|
||||
<Text>
|
||||
Your {resource} throughput will automatically scale from{" "}
|
||||
<b>
|
||||
{AutoPilotUtils.getMinRUsBasedOnUserInput(maxAutoPilotThroughputSet)} RU/s (10% of max RU/s) -{" "}
|
||||
{maxAutoPilotThroughputSet} RU/s
|
||||
</b>{" "}
|
||||
based on usage.
|
||||
<br />
|
||||
</Text>
|
||||
{requestUnitsUsageCostElement}
|
||||
<Text>
|
||||
After the first {AutoPilotUtils.getStorageBasedOnUserInput(maxAutoPilotThroughputSet)} GB of data stored, the
|
||||
max RU/s will be automatically upgraded based on the new storage value.
|
||||
<Link href={AutopilotDocumentation.Url} target="_blank">
|
||||
{" "}
|
||||
Learn more
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getEstimatedAutoscaleSpendElement = (
|
||||
throughput: number,
|
||||
serverId: string,
|
||||
regions: number,
|
||||
multimaster: boolean
|
||||
): JSX.Element => {
|
||||
const hourlyPrice: number = computeAutoscaleUsagePriceHourly(serverId, throughput, regions, multimaster);
|
||||
const monthlyPrice: number = hourlyPrice * hoursInAMonth;
|
||||
const currency: string = getPriceCurrency(serverId);
|
||||
const currencySign: string = getCurrencySign(serverId);
|
||||
const pricePerRu =
|
||||
getAutoscalePricePerRu(serverId, getMultimasterMultiplier(regions, multimaster)) *
|
||||
getMultimasterMultiplier(regions, multimaster);
|
||||
|
||||
return (
|
||||
<Text id="autoscaleSpendElement">
|
||||
Estimated monthly cost ({currency}) is{" "}
|
||||
<b>
|
||||
{currencySign}
|
||||
{calculateEstimateNumber(monthlyPrice / 10)}
|
||||
{` - `}
|
||||
{currencySign}
|
||||
{calculateEstimateNumber(monthlyPrice)}{" "}
|
||||
</b>
|
||||
({"regions: "} {regions}, {throughput / 10} - {throughput} RU/s, {currencySign}
|
||||
{pricePerRu}/RU)
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export const getEstimatedSpendElement = (
|
||||
throughput: number,
|
||||
serverId: string,
|
||||
regions: number,
|
||||
multimaster: boolean,
|
||||
rupmEnabled: boolean
|
||||
): JSX.Element => {
|
||||
const hourlyPrice: number = computeRUUsagePriceHourly(serverId, rupmEnabled, throughput, regions, multimaster);
|
||||
const dailyPrice: number = hourlyPrice * 24;
|
||||
const monthlyPrice: number = hourlyPrice * hoursInAMonth;
|
||||
const currency: string = getPriceCurrency(serverId);
|
||||
const currencySign: string = getCurrencySign(serverId);
|
||||
const pricePerRu = getPricePerRu(serverId) * getMultimasterMultiplier(regions, multimaster);
|
||||
|
||||
return (
|
||||
<Text id="throughputSpendElement">
|
||||
Estimated cost ({currency}):{" "}
|
||||
<b>
|
||||
{currencySign}
|
||||
{calculateEstimateNumber(hourlyPrice)} hourly {` / `}
|
||||
{currencySign}
|
||||
{calculateEstimateNumber(dailyPrice)} daily {` / `}
|
||||
{currencySign}
|
||||
{calculateEstimateNumber(monthlyPrice)} monthly{" "}
|
||||
</b>
|
||||
({"regions: "} {regions}, {throughput}RU/s, {currencySign}
|
||||
{pricePerRu}/RU)
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export const manualToAutoscaleDisclaimerElement: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
|
||||
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings
|
||||
and storage of your resource. After autoscale has been enabled, you can change the max RU/s.{" "}
|
||||
<a href={Urls.autoscaleMigration}>Learn more</a>
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const ttlWarning: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete
|
||||
operation explicitly issued by a client application. For more information see,{" "}
|
||||
<Link target="_blank" href="https://aka.ms/cosmos-db-ttl">
|
||||
Time to Live (TTL) in Azure Cosmos DB
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const indexingPolicyTTLWarningMessage: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
Changing the Indexing Policy impacts query results while the index transformation occurs. When a change is made and
|
||||
the indexing mode is set to consistent or lazy, queries return eventual results until the operation completes. For
|
||||
more information see,{" "}
|
||||
<Link target="_blank" href="https://aka.ms/cosmosdb/modify-index-policy">
|
||||
Modifying Indexing Policies
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const updateThroughputBeyondLimitWarningMessage: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle} id="updateThroughputBeyondLimitWarningMessage">
|
||||
You are about to request an increase in throughput beyond the pre-allocated capacity. The service will scale out and
|
||||
increase throughput for the selected container. This operation will take 1-3 business days to complete. You can
|
||||
track the status of this request in Notifications.
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
|
||||
You are about to request an increase in throughput beyond the pre-allocated capacity. This operation will take some
|
||||
time to complete.
|
||||
</Text>
|
||||
);
|
||||
|
||||
const getCurrentThroughput = (
|
||||
isAutoscale: boolean,
|
||||
throughput: number,
|
||||
throughputUnit: string,
|
||||
targetThroughput?: number
|
||||
): string => {
|
||||
if (targetThroughput) {
|
||||
if (throughput) {
|
||||
return isAutoscale
|
||||
? `, Current autoscale throughput: ${Math.round(
|
||||
throughput / 10
|
||||
)} - ${throughput} ${throughputUnit}, Target autoscale throughput: ${Math.round(
|
||||
targetThroughput / 10
|
||||
)} - ${targetThroughput} ${throughputUnit}`
|
||||
: `, Current manual throughput: ${throughput} ${throughputUnit}, Target manual throughput: ${targetThroughput}`;
|
||||
} else {
|
||||
return isAutoscale
|
||||
? `, Target autoscale throughput: ${Math.round(targetThroughput / 10)} - ${targetThroughput} ${throughputUnit}`
|
||||
: `, Target manual throughput: ${targetThroughput} ${throughputUnit}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetThroughput && throughput) {
|
||||
return isAutoscale
|
||||
? `, Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} ${throughputUnit}`
|
||||
: `, Current manual throughput: ${throughput} ${throughputUnit}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
export const getThroughputApplyDelayedMessage = (
|
||||
isAutoscale: boolean,
|
||||
throughput: number,
|
||||
throughputUnit: string,
|
||||
databaseName: string,
|
||||
collectionName: string,
|
||||
requestedThroughput: number
|
||||
): JSX.Element => (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
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}, Container: {collectionName}{" "}
|
||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const getThroughputApplyShortDelayMessage = (
|
||||
isAutoscale: boolean,
|
||||
throughput: number,
|
||||
throughputUnit: string,
|
||||
databaseName: 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, targetThroughput)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const getThroughputApplyLongDelayMessage = (
|
||||
isAutoscale: boolean,
|
||||
throughput: number,
|
||||
throughputUnit: string,
|
||||
databaseName: string,
|
||||
collectionName: string,
|
||||
requestedThroughput: number
|
||||
): JSX.Element => (
|
||||
<Text styles={infoAndToolTipTextStyle} id="throughputApplyLongDelayMessage">
|
||||
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to
|
||||
complete. View the latest status in Notifications.
|
||||
<br />
|
||||
Database: {databaseName}, Container: {collectionName}{" "}
|
||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const getToolTipContainer = (content: string | JSX.Element): JSX.Element =>
|
||||
content ? <Text styles={infoAndToolTipTextStyle}>{content}</Text> : undefined;
|
||||
|
||||
export const conflictResolutionLwwTooltip: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
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.
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const conflictResolutionCustomToolTip: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
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
|
||||
<Link className="linkDarkBackground" href="https://aka.ms/dataexplorerconflics" target="_blank">
|
||||
{` conflicts feed`}
|
||||
</Link>
|
||||
. You can update/re-register the stored procedure at any time.
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const changeFeedPolicyToolTip: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
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.
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const getTextFieldStyles = (current: isDirtyTypes, baseline: isDirtyTypes): Partial<ITextFieldStyles> => ({
|
||||
fieldGroup: {
|
||||
height: 25,
|
||||
width: 300,
|
||||
borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : "",
|
||||
selectors: {
|
||||
":disabled": {
|
||||
backgroundColor: StyleConstants.BaseMedium,
|
||||
borderColor: StyleConstants.BaseMediumHigh
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const getChoiceGroupStyles = (current: isDirtyTypes, baseline: isDirtyTypes): Partial<IChoiceGroupStyles> => ({
|
||||
flexContainer: [
|
||||
{
|
||||
selectors: {
|
||||
".ms-ChoiceField-field.is-checked::before": {
|
||||
borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : ""
|
||||
},
|
||||
".ms-ChoiceField-field.is-checked::after": {
|
||||
borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : ""
|
||||
},
|
||||
".ms-ChoiceField-wrapper label": {
|
||||
whiteSpace: "nowrap",
|
||||
fontSize: 14,
|
||||
fontFamily: StyleConstants.DataExplorerFont,
|
||||
padding: "2px 5px"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { ConflictResolutionComponentProps, ConflictResolutionComponent } from "./ConflictResolutionComponent";
|
||||
import { container, collection } from "../TestUtils";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
|
||||
describe("ConflictResolutionComponent", () => {
|
||||
const baseProps: ConflictResolutionComponentProps = {
|
||||
collection: collection,
|
||||
container: container,
|
||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.Custom,
|
||||
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode.Custom,
|
||||
onConflictResolutionPolicyModeChange: () => {
|
||||
return;
|
||||
},
|
||||
conflictResolutionPolicyPath: "",
|
||||
conflictResolutionPolicyPathBaseline: "",
|
||||
onConflictResolutionPolicyPathChange: () => {
|
||||
return;
|
||||
},
|
||||
conflictResolutionPolicyProcedure: "",
|
||||
conflictResolutionPolicyProcedureBaseline: "",
|
||||
onConflictResolutionPolicyProcedureChange: () => {
|
||||
return;
|
||||
},
|
||||
onConflictResolutionDirtyChange: () => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
it("Sproc text field displayed", () => {
|
||||
const wrapper = shallow(<ConflictResolutionComponent {...baseProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists("#conflictResolutionCustomTextField")).toEqual(true);
|
||||
expect(wrapper.exists("#conflictResolutionLwwTextField")).toEqual(false);
|
||||
});
|
||||
|
||||
it("Path text field displayed", () => {
|
||||
const props = { ...baseProps, conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.LastWriterWins };
|
||||
const wrapper = shallow(<ConflictResolutionComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists("#conflictResolutionCustomTextField")).toEqual(false);
|
||||
expect(wrapper.exists("#conflictResolutionLwwTextField")).toEqual(true);
|
||||
});
|
||||
|
||||
it("conflict resolution policy dirty is set", () => {
|
||||
let conflictRsolutionComponent = new ConflictResolutionComponent(baseProps);
|
||||
expect(conflictRsolutionComponent.IsComponentDirty()).toEqual(false);
|
||||
|
||||
const newProps = { ...baseProps, conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.LastWriterWins };
|
||||
conflictRsolutionComponent = new ConflictResolutionComponent(newProps);
|
||||
expect(conflictRsolutionComponent.IsComponentDirty()).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -1,142 +0,0 @@
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import Explorer from "../../../Explorer";
|
||||
import {
|
||||
getTextFieldStyles,
|
||||
conflictResolutionLwwTooltip,
|
||||
conflictResolutionCustomToolTip,
|
||||
subComponentStackProps,
|
||||
getChoiceGroupStyles
|
||||
} from "../SettingsRenderUtils";
|
||||
import { TextField, ITextFieldProps, Stack, IChoiceGroupOption, ChoiceGroup } from "office-ui-fabric-react";
|
||||
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
|
||||
import { isDirty } from "../SettingsUtils";
|
||||
|
||||
export interface ConflictResolutionComponentProps {
|
||||
collection: ViewModels.Collection;
|
||||
container: Explorer;
|
||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode;
|
||||
conflictResolutionPolicyModeBaseline: DataModels.ConflictResolutionMode;
|
||||
onConflictResolutionPolicyModeChange: (
|
||||
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
) => void;
|
||||
conflictResolutionPolicyPath: string;
|
||||
conflictResolutionPolicyPathBaseline: string;
|
||||
|
||||
onConflictResolutionPolicyPathChange: (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
) => void;
|
||||
conflictResolutionPolicyProcedure: string;
|
||||
conflictResolutionPolicyProcedureBaseline: string;
|
||||
|
||||
onConflictResolutionPolicyProcedureChange: (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
) => void;
|
||||
onConflictResolutionDirtyChange: (isConflictResolutionDirty: boolean) => void;
|
||||
}
|
||||
|
||||
export class ConflictResolutionComponent extends React.Component<ConflictResolutionComponentProps> {
|
||||
private shouldCheckComponentIsDirty = true;
|
||||
private conflictResolutionChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{
|
||||
key: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||
text: "Last Write Wins (default)"
|
||||
},
|
||||
{ key: DataModels.ConflictResolutionMode.Custom, text: "Merge Procedure (custom)" }
|
||||
];
|
||||
|
||||
componentDidMount(): void {
|
||||
this.onComponentUpdate();
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
this.onComponentUpdate();
|
||||
}
|
||||
|
||||
private onComponentUpdate = (): void => {
|
||||
if (!this.shouldCheckComponentIsDirty) {
|
||||
this.shouldCheckComponentIsDirty = true;
|
||||
return;
|
||||
}
|
||||
this.props.onConflictResolutionDirtyChange(this.IsComponentDirty());
|
||||
this.shouldCheckComponentIsDirty = false;
|
||||
};
|
||||
|
||||
public IsComponentDirty = (): boolean => {
|
||||
if (
|
||||
isDirty(this.props.conflictResolutionPolicyMode, this.props.conflictResolutionPolicyModeBaseline) ||
|
||||
isDirty(this.props.conflictResolutionPolicyPath, this.props.conflictResolutionPolicyPathBaseline) ||
|
||||
isDirty(this.props.conflictResolutionPolicyProcedure, this.props.conflictResolutionPolicyProcedureBaseline)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
private getConflictResolutionModeComponent = (): JSX.Element => (
|
||||
<ChoiceGroup
|
||||
label="Mode"
|
||||
selectedKey={this.props.conflictResolutionPolicyMode}
|
||||
options={this.conflictResolutionChoiceGroupOptions}
|
||||
onChange={this.props.onConflictResolutionPolicyModeChange}
|
||||
styles={getChoiceGroupStyles(
|
||||
this.props.conflictResolutionPolicyMode,
|
||||
this.props.conflictResolutionPolicyModeBaseline
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
private onRenderLwwComponentTextField = (props: ITextFieldProps) => (
|
||||
<ToolTipLabelComponent label={props.label} toolTipElement={conflictResolutionLwwTooltip} />
|
||||
);
|
||||
|
||||
private getConflictResolutionLWWComponent = (): JSX.Element => (
|
||||
<TextField
|
||||
id="conflictResolutionLwwTextField"
|
||||
label={"Conflict Resolver Property"}
|
||||
onRenderLabel={this.onRenderLwwComponentTextField}
|
||||
styles={getTextFieldStyles(
|
||||
this.props.conflictResolutionPolicyPath,
|
||||
this.props.conflictResolutionPolicyPathBaseline
|
||||
)}
|
||||
value={this.props.conflictResolutionPolicyPath}
|
||||
onChange={this.props.onConflictResolutionPolicyPathChange}
|
||||
/>
|
||||
);
|
||||
|
||||
private onRenderCustomComponentTextField = (props: ITextFieldProps) => (
|
||||
<ToolTipLabelComponent label={props.label} toolTipElement={conflictResolutionCustomToolTip} />
|
||||
);
|
||||
|
||||
private getConflictResolutionCustomComponent = (): JSX.Element => (
|
||||
<TextField
|
||||
id="conflictResolutionCustomTextField"
|
||||
label="Stored procedure"
|
||||
onRenderLabel={this.onRenderCustomComponentTextField}
|
||||
styles={getTextFieldStyles(
|
||||
this.props.conflictResolutionPolicyProcedure,
|
||||
this.props.conflictResolutionPolicyProcedureBaseline
|
||||
)}
|
||||
value={this.props.conflictResolutionPolicyProcedure}
|
||||
onChange={this.props.onConflictResolutionPolicyProcedureChange}
|
||||
/>
|
||||
);
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Stack {...subComponentStackProps}>
|
||||
{this.getConflictResolutionModeComponent()}
|
||||
|
||||
{this.props.conflictResolutionPolicyMode === DataModels.ConflictResolutionMode.LastWriterWins &&
|
||||
this.getConflictResolutionLWWComponent()}
|
||||
|
||||
{this.props.conflictResolutionPolicyMode === DataModels.ConflictResolutionMode.Custom &&
|
||||
this.getConflictResolutionCustomComponent()}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { IndexingPolicyComponent, IndexingPolicyComponentProps } from "./IndexingPolicyComponent";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
|
||||
describe("IndexingPolicyComponent", () => {
|
||||
const initialIndexingPolicyContent: DataModels.IndexingPolicy = {
|
||||
automatic: false,
|
||||
indexingMode: "",
|
||||
includedPaths: [],
|
||||
excludedPaths: []
|
||||
};
|
||||
const baseProps: IndexingPolicyComponentProps = {
|
||||
shouldDiscardIndexingPolicy: false,
|
||||
resetShouldDiscardIndexingPolicy: () => {
|
||||
return;
|
||||
},
|
||||
indexingPolicyContent: initialIndexingPolicyContent,
|
||||
indexingPolicyContentBaseline: initialIndexingPolicyContent,
|
||||
onIndexingPolicyElementFocusChange: () => {
|
||||
return;
|
||||
},
|
||||
onIndexingPolicyContentChange: () => {
|
||||
return;
|
||||
},
|
||||
logIndexingPolicySuccessMessage: () => {
|
||||
return;
|
||||
},
|
||||
onIndexingPolicyDirtyChange: () => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = shallow(<IndexingPolicyComponent {...baseProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("indexing policy is reset", () => {
|
||||
const wrapper = shallow(<IndexingPolicyComponent {...baseProps} />);
|
||||
|
||||
const indexingPolicyComponentInstance = wrapper.instance() as IndexingPolicyComponent;
|
||||
const resetIndexingPolicyEditorMockFn = jest.fn();
|
||||
indexingPolicyComponentInstance.resetIndexingPolicyEditor = resetIndexingPolicyEditorMockFn;
|
||||
|
||||
wrapper.setProps({ shouldDiscardIndexingPolicy: true });
|
||||
wrapper.update();
|
||||
expect(resetIndexingPolicyEditorMockFn.mock.calls.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("conflict resolution policy dirty is set", () => {
|
||||
let indexingPolicyComponent = new IndexingPolicyComponent(baseProps);
|
||||
expect(indexingPolicyComponent.IsComponentDirty()).toEqual(false);
|
||||
|
||||
const newProps = { ...baseProps, indexingPolicyContent: undefined as DataModels.IndexingPolicy };
|
||||
indexingPolicyComponent = new IndexingPolicyComponent(newProps);
|
||||
expect(indexingPolicyComponent.IsComponentDirty()).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -1,121 +0,0 @@
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import * as monaco from "monaco-editor";
|
||||
import { isDirty } from "../SettingsUtils";
|
||||
import { MessageBar, MessageBarType, Stack } from "office-ui-fabric-react";
|
||||
import { indexingPolicyTTLWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils";
|
||||
|
||||
export interface IndexingPolicyComponentProps {
|
||||
shouldDiscardIndexingPolicy: boolean;
|
||||
resetShouldDiscardIndexingPolicy: () => void;
|
||||
indexingPolicyContent: DataModels.IndexingPolicy;
|
||||
indexingPolicyContentBaseline: DataModels.IndexingPolicy;
|
||||
onIndexingPolicyElementFocusChange: (indexingPolicyContentFocussed: boolean) => void;
|
||||
onIndexingPolicyContentChange: (newIndexingPolicy: DataModels.IndexingPolicy) => void;
|
||||
logIndexingPolicySuccessMessage: () => void;
|
||||
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
|
||||
}
|
||||
|
||||
interface IndexingPolicyComponentState {
|
||||
indexingPolicyContentIsValid: boolean;
|
||||
}
|
||||
|
||||
export class IndexingPolicyComponent extends React.Component<
|
||||
IndexingPolicyComponentProps,
|
||||
IndexingPolicyComponentState
|
||||
> {
|
||||
private shouldCheckComponentIsDirty = true;
|
||||
private indexingPolicyDiv = React.createRef<HTMLDivElement>();
|
||||
private indexingPolicyEditor: monaco.editor.IStandaloneCodeEditor;
|
||||
|
||||
constructor(props: IndexingPolicyComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
indexingPolicyContentIsValid: true
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
if (this.props.shouldDiscardIndexingPolicy) {
|
||||
this.resetIndexingPolicyEditor();
|
||||
this.props.resetShouldDiscardIndexingPolicy();
|
||||
}
|
||||
this.onComponentUpdate();
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.resetIndexingPolicyEditor();
|
||||
this.onComponentUpdate();
|
||||
}
|
||||
|
||||
public resetIndexingPolicyEditor = (): void => {
|
||||
if (!this.indexingPolicyEditor) {
|
||||
this.createIndexingPolicyEditor();
|
||||
} else {
|
||||
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
||||
const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4);
|
||||
indexingPolicyEditorModel.setValue(value);
|
||||
}
|
||||
this.onComponentUpdate();
|
||||
};
|
||||
|
||||
private onComponentUpdate = (): void => {
|
||||
if (!this.shouldCheckComponentIsDirty) {
|
||||
this.shouldCheckComponentIsDirty = true;
|
||||
return;
|
||||
}
|
||||
this.props.onIndexingPolicyDirtyChange(this.IsComponentDirty());
|
||||
this.shouldCheckComponentIsDirty = false;
|
||||
};
|
||||
|
||||
public IsComponentDirty = (): boolean => {
|
||||
if (
|
||||
isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) &&
|
||||
this.state.indexingPolicyContentIsValid
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
private createIndexingPolicyEditor = (): void => {
|
||||
const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4);
|
||||
|
||||
this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, {
|
||||
value: value,
|
||||
language: "json",
|
||||
readOnly: false,
|
||||
ariaLabel: "Indexing Policy"
|
||||
});
|
||||
if (this.indexingPolicyEditor) {
|
||||
this.indexingPolicyEditor.onDidFocusEditorText(() => this.props.onIndexingPolicyElementFocusChange(true));
|
||||
this.indexingPolicyEditor.onDidBlurEditorText(() => this.props.onIndexingPolicyElementFocusChange(false));
|
||||
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
||||
indexingPolicyEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this));
|
||||
this.props.logIndexingPolicySuccessMessage();
|
||||
}
|
||||
};
|
||||
|
||||
private onEditorContentChange = (): void => {
|
||||
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
||||
try {
|
||||
const newIndexingPolicyContent = JSON.parse(indexingPolicyEditorModel.getValue()) as DataModels.IndexingPolicy;
|
||||
this.props.onIndexingPolicyContentChange(newIndexingPolicyContent);
|
||||
this.setState({ indexingPolicyContentIsValid: true });
|
||||
} catch (e) {
|
||||
this.setState({ indexingPolicyContentIsValid: false });
|
||||
}
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>{indexingPolicyTTLWarningMessage}</MessageBar>
|
||||
)}
|
||||
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
|
||||
import { container, collection } from "../TestUtils";
|
||||
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
||||
import Explorer from "../../../Explorer";
|
||||
import * as Constants from "../../../../Common/Constants";
|
||||
import { PlatformType } from "../../../../PlatformType";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import { throughputUnit } from "../SettingsRenderUtils";
|
||||
import * as SharedConstants from "../../../../Shared/Constants";
|
||||
import ko from "knockout";
|
||||
|
||||
describe("ScaleComponent", () => {
|
||||
const nonNationalCloudContainer = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
nonNationalCloudContainer.getPlatformType = () => PlatformType.Portal;
|
||||
nonNationalCloudContainer.isRunningOnNationalCloud = () => false;
|
||||
|
||||
const targetThroughput = 6000;
|
||||
|
||||
const baseProps: ScaleComponentProps = {
|
||||
collection: collection,
|
||||
container: container,
|
||||
isFixedContainer: false,
|
||||
autoPilotTiersList: [],
|
||||
onThroughputChange: () => {
|
||||
return;
|
||||
},
|
||||
throughput: 1000,
|
||||
throughputBaseline: 1000,
|
||||
autoPilotThroughput: 4000,
|
||||
autoPilotThroughputBaseline: 4000,
|
||||
isAutoPilotSelected: false,
|
||||
wasAutopilotOriginallySet: true,
|
||||
onAutoPilotSelected: () => false,
|
||||
onMaxAutoPilotThroughputChange: () => {
|
||||
return;
|
||||
},
|
||||
onScaleSaveableChange: () => {
|
||||
return;
|
||||
},
|
||||
onScaleDiscardableChange: () => {
|
||||
return;
|
||||
},
|
||||
initialNotification: {
|
||||
description: `Throughput update for ${targetThroughput} ${throughputUnit}`
|
||||
} as DataModels.Notification
|
||||
};
|
||||
|
||||
it("renders with correct intiial notification", () => {
|
||||
let wrapper = shallow(<ScaleComponent {...baseProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists(ThroughputInputAutoPilotV3Component)).toEqual(true);
|
||||
expect(wrapper.exists("#throughputApplyLongDelayMessage")).toEqual(true);
|
||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(false);
|
||||
expect(wrapper.find("#throughputApplyLongDelayMessage").html()).toContain(targetThroughput);
|
||||
|
||||
const newCollection = { ...collection };
|
||||
const maxThroughput = 5000;
|
||||
const targetMaxThroughput = 50000;
|
||||
newCollection.offer = ko.observable({
|
||||
content: {
|
||||
offerAutopilotSettings: {
|
||||
maxThroughput: maxThroughput,
|
||||
targetMaxThroughput: targetMaxThroughput
|
||||
}
|
||||
},
|
||||
headers: { "x-ms-offer-replace-pending": true }
|
||||
} as DataModels.OfferWithHeaders);
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
initialNotification: undefined as DataModels.Notification,
|
||||
collection: newCollection
|
||||
};
|
||||
wrapper = shallow(<ScaleComponent {...newProps} />);
|
||||
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", () => {
|
||||
const scaleComponent = new ScaleComponent(baseProps);
|
||||
expect(scaleComponent.isAutoScaleEnabled()).toEqual(false);
|
||||
});
|
||||
|
||||
it("autoScale enabled", () => {
|
||||
const newContainer = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
|
||||
newContainer.databaseAccount({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
location: undefined,
|
||||
type: undefined,
|
||||
kind: "documentdb",
|
||||
tags: undefined,
|
||||
properties: {
|
||||
documentEndpoint: undefined,
|
||||
tableEndpoint: undefined,
|
||||
gremlinEndpoint: undefined,
|
||||
cassandraEndpoint: undefined,
|
||||
capabilities: [
|
||||
{
|
||||
name: Constants.CapabilityNames.EnableAutoScale.toLowerCase(),
|
||||
description: undefined
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
const props = { ...baseProps, container: newContainer };
|
||||
const scaleComponent = new ScaleComponent(props);
|
||||
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 - 40,000 RU/s)");
|
||||
|
||||
let newProps = { ...baseProps, container: nonNationalCloudContainer };
|
||||
scaleComponent = new ScaleComponent(newProps);
|
||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (6,000 - unlimited RU/s)");
|
||||
|
||||
newProps = { ...baseProps, isAutoPilotSelected: true };
|
||||
scaleComponent = new ScaleComponent(newProps);
|
||||
expect(scaleComponent.getThroughputTitle()).toEqual("Throughput (autoscale)");
|
||||
});
|
||||
|
||||
it("canThroughputExceedMaximumValue", () => {
|
||||
let scaleComponent = new ScaleComponent(baseProps);
|
||||
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(false);
|
||||
|
||||
const newProps = { ...baseProps, container: nonNationalCloudContainer };
|
||||
scaleComponent = new ScaleComponent(newProps);
|
||||
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
|
||||
});
|
||||
|
||||
it("getThroughputWarningMessage", () => {
|
||||
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
|
||||
const throughputBeyondMaxRus = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million - 1000;
|
||||
|
||||
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -1,235 +0,0 @@
|
||||
import * as React from "react";
|
||||
import * as Constants from "../../../../Common/Constants";
|
||||
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import * as SharedConstants from "../../../../Shared/Constants";
|
||||
import Explorer from "../../../Explorer";
|
||||
import { PlatformType } from "../../../../PlatformType";
|
||||
import {
|
||||
getTextFieldStyles,
|
||||
subComponentStackProps,
|
||||
titleAndInputStackProps,
|
||||
throughputUnit,
|
||||
getThroughputApplyLongDelayMessage,
|
||||
getThroughputApplyShortDelayMessage,
|
||||
updateThroughputBeyondLimitWarningMessage,
|
||||
updateThroughputDelayedApplyWarningMessage
|
||||
} from "../SettingsRenderUtils";
|
||||
import { getMaxRUs, getMinRUs, hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
||||
import { Text, TextField, Stack, Label, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||
|
||||
export interface ScaleComponentProps {
|
||||
collection: ViewModels.Collection;
|
||||
container: Explorer;
|
||||
isFixedContainer: boolean;
|
||||
autoPilotTiersList: ViewModels.DropdownOption<DataModels.AutopilotTier>[];
|
||||
onThroughputChange: (newThroughput: number) => void;
|
||||
throughput: number;
|
||||
throughputBaseline: number;
|
||||
autoPilotThroughput: number;
|
||||
autoPilotThroughputBaseline: number;
|
||||
isAutoPilotSelected: boolean;
|
||||
wasAutopilotOriginallySet: boolean;
|
||||
onAutoPilotSelected: (isAutoPilotSelected: boolean) => void;
|
||||
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
|
||||
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||
initialNotification: DataModels.Notification;
|
||||
}
|
||||
|
||||
export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
private isEmulator: boolean;
|
||||
constructor(props: ScaleComponentProps) {
|
||||
super(props);
|
||||
this.isEmulator = this.props.container.isEmulator;
|
||||
}
|
||||
|
||||
public isAutoScaleEnabled = (): boolean => {
|
||||
const accountCapabilities: DataModels.Capability[] = this.props.container?.databaseAccount()?.properties
|
||||
?.capabilities;
|
||||
const enableAutoScaleCapability =
|
||||
accountCapabilities &&
|
||||
accountCapabilities.find((capability: DataModels.Capability) => {
|
||||
return (
|
||||
capability &&
|
||||
capability.name &&
|
||||
capability.name.toLowerCase() === Constants.CapabilityNames.EnableAutoScale.toLowerCase()
|
||||
);
|
||||
});
|
||||
|
||||
return !!enableAutoScaleCapability;
|
||||
};
|
||||
|
||||
private getStorageCapacityTitle = (): JSX.Element => {
|
||||
// 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>
|
||||
<Text>{capacity}</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
public getMaxRUThroughputInputLimit = (): number => {
|
||||
if (this.props.container?.getPlatformType() === PlatformType.Hosted && this.props.collection.partitionKey) {
|
||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||
}
|
||||
|
||||
return getMaxRUs(this.props.collection, this.props.container);
|
||||
};
|
||||
|
||||
public getThroughputTitle = (): string => {
|
||||
if (this.props.isAutoPilotSelected) {
|
||||
return AutoPilotUtils.getAutoPilotHeaderText(false);
|
||||
}
|
||||
|
||||
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)`;
|
||||
};
|
||||
|
||||
public canThroughputExceedMaximumValue = (): boolean => {
|
||||
const isPublicAzurePortal: boolean =
|
||||
this.props.container.getPlatformType() === PlatformType.Portal &&
|
||||
!this.props.container.isRunningOnNationalCloud();
|
||||
const hasPartitionKey = !!this.props.collection.partitionKey;
|
||||
|
||||
return isPublicAzurePortal && hasPartitionKey;
|
||||
};
|
||||
|
||||
public getInitialNotificationElement = (): JSX.Element => {
|
||||
if (this.props.initialNotification) {
|
||||
return this.getLongDelayMessage();
|
||||
}
|
||||
|
||||
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(),
|
||||
targetThroughput
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
public getLongDelayMessage = (): JSX.Element => {
|
||||
const matches: string[] = this.props.initialNotification?.description.match(
|
||||
`Throughput update for (.*) ${throughputUnit}`
|
||||
);
|
||||
|
||||
const throughput = this.props.throughputBaseline;
|
||||
const targetThroughput: number = matches.length > 1 && Number(matches[1]);
|
||||
if (targetThroughput) {
|
||||
return getThroughputApplyLongDelayMessage(
|
||||
this.props.wasAutopilotOriginallySet,
|
||||
throughput,
|
||||
throughputUnit,
|
||||
this.props.collection.databaseId,
|
||||
this.props.collection.id(),
|
||||
targetThroughput
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
|
||||
private getThroughputInputComponent = (): JSX.Element => (
|
||||
<ThroughputInputAutoPilotV3Component
|
||||
databaseAccount={this.props.container.databaseAccount()}
|
||||
serverId={this.props.container.serverId()}
|
||||
throughput={this.props.throughput}
|
||||
throughputBaseline={this.props.throughputBaseline}
|
||||
onThroughputChange={this.props.onThroughputChange}
|
||||
minimum={getMinRUs(this.props.collection, this.props.container)}
|
||||
maximum={this.getMaxRUThroughputInputLimit()}
|
||||
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
||||
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
||||
label={this.getThroughputTitle()}
|
||||
isEmulator={this.isEmulator}
|
||||
isFixed={this.props.isFixedContainer}
|
||||
isAutoPilotSelected={this.props.isAutoPilotSelected}
|
||||
onAutoPilotSelected={this.props.onAutoPilotSelected}
|
||||
wasAutopilotOriginallySet={this.props.wasAutopilotOriginallySet}
|
||||
maxAutoPilotThroughput={this.props.autoPilotThroughput}
|
||||
maxAutoPilotThroughputBaseline={this.props.autoPilotThroughputBaseline}
|
||||
onMaxAutoPilotThroughputChange={this.props.onMaxAutoPilotThroughputChange}
|
||||
spendAckChecked={false}
|
||||
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
||||
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
||||
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
||||
/>
|
||||
);
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Stack {...subComponentStackProps}>
|
||||
{this.getInitialNotificationElement() && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>{this.getInitialNotificationElement()}</MessageBar>
|
||||
)}
|
||||
{!this.isAutoScaleEnabled() && (
|
||||
<Stack {...subComponentStackProps}>
|
||||
{this.getThroughputInputComponent()}
|
||||
{this.getStorageCapacityTitle()}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* TODO: Replace link with call to the Azure Support blade */}
|
||||
{this.isAutoScaleEnabled() && (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<Text>Throughput (RU/s)</Text>
|
||||
<TextField disabled styles={getTextFieldStyles(undefined, undefined)} />
|
||||
<Text>
|
||||
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.
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { SubSettingsComponent, SubSettingsComponentProps } from "./SubSettingsComponent";
|
||||
import { container, collection } from "../TestUtils";
|
||||
import { TtlType, GeospatialConfigType, ChangeFeedPolicyState } from "../SettingsUtils";
|
||||
import ko from "knockout";
|
||||
import Explorer from "../../../Explorer";
|
||||
|
||||
describe("SubSettingsComponent", () => {
|
||||
container.isPreferredApiDocumentDB = ko.computed(() => true);
|
||||
|
||||
const baseProps: SubSettingsComponentProps = {
|
||||
collection: collection,
|
||||
container: container,
|
||||
|
||||
timeToLive: TtlType.On,
|
||||
timeToLiveBaseline: TtlType.On,
|
||||
onTtlChange: () => {
|
||||
return;
|
||||
},
|
||||
timeToLiveSeconds: 1000,
|
||||
timeToLiveSecondsBaseline: 1000,
|
||||
onTimeToLiveSecondsChange: () => {
|
||||
return;
|
||||
},
|
||||
|
||||
geospatialConfigType: GeospatialConfigType.Geography,
|
||||
geospatialConfigTypeBaseline: GeospatialConfigType.Geography,
|
||||
onGeoSpatialConfigTypeChange: () => {
|
||||
return;
|
||||
},
|
||||
|
||||
isAnalyticalStorageEnabled: true,
|
||||
analyticalStorageTtlSelection: TtlType.On,
|
||||
analyticalStorageTtlSelectionBaseline: TtlType.On,
|
||||
onAnalyticalStorageTtlSelectionChange: () => {
|
||||
return;
|
||||
},
|
||||
analyticalStorageTtlSeconds: 2000,
|
||||
analyticalStorageTtlSecondsBaseline: 2000,
|
||||
onAnalyticalStorageTtlSecondsChange: () => {
|
||||
return;
|
||||
},
|
||||
|
||||
changeFeedPolicyVisible: true,
|
||||
changeFeedPolicy: ChangeFeedPolicyState.On,
|
||||
changeFeedPolicyBaseline: ChangeFeedPolicyState.On,
|
||||
|
||||
onChangeFeedPolicyChange: () => {
|
||||
return;
|
||||
},
|
||||
onSubSettingsSaveableChange: () => {
|
||||
return;
|
||||
},
|
||||
onSubSettingsDiscardableChange: () => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = shallow(<SubSettingsComponent {...baseProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists("#timeToLive")).toEqual(true);
|
||||
expect(wrapper.exists("#timeToLiveSeconds")).toEqual(true);
|
||||
expect(wrapper.exists("#geoSpatialConfig")).toEqual(true);
|
||||
expect(wrapper.exists("#analyticalStorageTimeToLive")).toEqual(true);
|
||||
expect(wrapper.exists("#analyticalStorageTimeToLiveSeconds")).toEqual(true);
|
||||
expect(wrapper.exists("#changeFeedPolicy")).toEqual(true);
|
||||
});
|
||||
|
||||
it("timeToLiveSeconds hidden", () => {
|
||||
const props = { ...baseProps, timeToLive: TtlType.Off };
|
||||
const wrapper = shallow(<SubSettingsComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists("#timeToLive")).toEqual(true);
|
||||
expect(wrapper.exists("#timeToLiveSeconds")).toEqual(false);
|
||||
});
|
||||
|
||||
it("analyticalTimeToLive hidden", () => {
|
||||
const props = { ...baseProps, isAnalyticalStorageEnabled: false };
|
||||
const wrapper = shallow(<SubSettingsComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists("#analyticalStorageTimeToLive")).toEqual(false);
|
||||
expect(wrapper.exists("#analyticalStorageTimeToLiveSeconds")).toEqual(false);
|
||||
});
|
||||
|
||||
it("analyticalTimeToLiveSeconds hidden", () => {
|
||||
const props = { ...baseProps, analyticalStorageTtlSelection: TtlType.OnNoDefault };
|
||||
const wrapper = shallow(<SubSettingsComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists("#analyticalStorageTimeToLive")).toEqual(true);
|
||||
expect(wrapper.exists("#analyticalStorageTimeToLiveSeconds")).toEqual(false);
|
||||
});
|
||||
|
||||
it("changeFeedPolicy hidden", () => {
|
||||
const props = { ...baseProps, changeFeedPolicyVisible: false };
|
||||
const wrapper = shallow(<SubSettingsComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists("#changeFeedPolicy")).toEqual(false);
|
||||
});
|
||||
|
||||
it("partitionKey is visible", () => {
|
||||
const subSettingsComponent = new SubSettingsComponent(baseProps);
|
||||
expect(subSettingsComponent.getPartitionKeyVisible()).toEqual(true);
|
||||
});
|
||||
|
||||
it("partitionKey not visible", () => {
|
||||
const newContainer = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
|
||||
newContainer.isPreferredApiCassandra = ko.computed(() => true);
|
||||
const props = { ...baseProps, container: newContainer };
|
||||
const subSettingsComponent = new SubSettingsComponent(props);
|
||||
expect(subSettingsComponent.getPartitionKeyVisible()).toEqual(false);
|
||||
});
|
||||
|
||||
it("largePartitionKey is enabled", () => {
|
||||
const subSettingsComponent = new SubSettingsComponent(baseProps);
|
||||
expect(subSettingsComponent.isLargePartitionKeyEnabled()).toEqual(true);
|
||||
});
|
||||
|
||||
it("sub settings saveable and discardable are set", () => {
|
||||
let subSettingsComponent = new SubSettingsComponent(baseProps);
|
||||
let isComponentDirtyResult = subSettingsComponent.IsComponentDirty();
|
||||
expect(isComponentDirtyResult.isSaveable).toEqual(false);
|
||||
expect(isComponentDirtyResult.isDiscardable).toEqual(false);
|
||||
|
||||
const newProps = { ...baseProps, timeToLive: TtlType.OnNoDefault };
|
||||
subSettingsComponent = new SubSettingsComponent(newProps);
|
||||
isComponentDirtyResult = subSettingsComponent.IsComponentDirty();
|
||||
expect(isComponentDirtyResult.isSaveable).toEqual(true);
|
||||
expect(isComponentDirtyResult.isDiscardable).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -1,296 +0,0 @@
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import {
|
||||
GeospatialConfigType,
|
||||
TtlType,
|
||||
ChangeFeedPolicyState,
|
||||
isDirty,
|
||||
IsComponentDirtyResult
|
||||
} from "../SettingsUtils";
|
||||
import Explorer from "../../../Explorer";
|
||||
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
import {
|
||||
Label,
|
||||
Text,
|
||||
TextField,
|
||||
Stack,
|
||||
IChoiceGroupOption,
|
||||
ChoiceGroup,
|
||||
MessageBar,
|
||||
MessageBarType
|
||||
} from "office-ui-fabric-react";
|
||||
import {
|
||||
getTextFieldStyles,
|
||||
changeFeedPolicyToolTip,
|
||||
subComponentStackProps,
|
||||
titleAndInputStackProps,
|
||||
getChoiceGroupStyles,
|
||||
ttlWarning,
|
||||
messageBarStyles
|
||||
} from "../SettingsRenderUtils";
|
||||
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
|
||||
|
||||
export interface SubSettingsComponentProps {
|
||||
collection: ViewModels.Collection;
|
||||
container: Explorer;
|
||||
|
||||
timeToLive: TtlType;
|
||||
timeToLiveBaseline: TtlType;
|
||||
|
||||
onTtlChange: (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption) => void;
|
||||
timeToLiveSeconds: number;
|
||||
timeToLiveSecondsBaseline: number;
|
||||
onTimeToLiveSecondsChange: (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
) => void;
|
||||
|
||||
geospatialConfigType: GeospatialConfigType;
|
||||
geospatialConfigTypeBaseline: GeospatialConfigType;
|
||||
onGeoSpatialConfigTypeChange: (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
) => void;
|
||||
|
||||
isAnalyticalStorageEnabled: boolean;
|
||||
analyticalStorageTtlSelection: TtlType;
|
||||
analyticalStorageTtlSelectionBaseline: TtlType;
|
||||
onAnalyticalStorageTtlSelectionChange: (
|
||||
ev?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
) => void;
|
||||
|
||||
analyticalStorageTtlSeconds: number;
|
||||
analyticalStorageTtlSecondsBaseline: number;
|
||||
onAnalyticalStorageTtlSecondsChange: (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
) => void;
|
||||
|
||||
changeFeedPolicyVisible: boolean;
|
||||
changeFeedPolicy: ChangeFeedPolicyState;
|
||||
changeFeedPolicyBaseline: ChangeFeedPolicyState;
|
||||
onChangeFeedPolicyChange: (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, option?: IChoiceGroupOption) => void;
|
||||
onSubSettingsSaveableChange: (isSubSettingsSaveable: boolean) => void;
|
||||
onSubSettingsDiscardableChange: (isSubSettingsDiscardable: boolean) => void;
|
||||
}
|
||||
|
||||
export class SubSettingsComponent extends React.Component<SubSettingsComponentProps> {
|
||||
private shouldCheckComponentIsDirty = true;
|
||||
private ttlVisible: boolean;
|
||||
private geospatialVisible: boolean;
|
||||
private partitionKeyValue: string;
|
||||
private partitionKeyName: string;
|
||||
|
||||
constructor(props: SubSettingsComponentProps) {
|
||||
super(props);
|
||||
this.ttlVisible = (this.props.container && !this.props.container.isPreferredApiCassandra()) || false;
|
||||
this.geospatialVisible = this.props.container.isPreferredApiDocumentDB();
|
||||
this.partitionKeyValue = "/" + this.props.collection.partitionKeyProperty;
|
||||
this.partitionKeyName = this.props.container.isPreferredApiMongoDB() ? "Shard key" : "Partition key";
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.onComponentUpdate();
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
this.onComponentUpdate();
|
||||
}
|
||||
|
||||
private onComponentUpdate = (): void => {
|
||||
if (!this.shouldCheckComponentIsDirty) {
|
||||
this.shouldCheckComponentIsDirty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const isComponentDirtyResult = this.IsComponentDirty();
|
||||
this.props.onSubSettingsSaveableChange(isComponentDirtyResult.isSaveable);
|
||||
this.props.onSubSettingsDiscardableChange(isComponentDirtyResult.isDiscardable);
|
||||
|
||||
this.shouldCheckComponentIsDirty = false;
|
||||
};
|
||||
|
||||
public IsComponentDirty = (): IsComponentDirtyResult => {
|
||||
if (
|
||||
(this.props.timeToLive === TtlType.On && !this.props.timeToLiveSeconds) ||
|
||||
(this.props.analyticalStorageTtlSelection === TtlType.On && !this.props.analyticalStorageTtlSeconds)
|
||||
) {
|
||||
return { isSaveable: false, isDiscardable: true };
|
||||
} else if (
|
||||
isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) ||
|
||||
(this.props.timeToLive === TtlType.On &&
|
||||
isDirty(this.props.timeToLiveSeconds, this.props.timeToLiveSecondsBaseline)) ||
|
||||
isDirty(this.props.analyticalStorageTtlSelection, this.props.analyticalStorageTtlSelectionBaseline) ||
|
||||
(this.props.analyticalStorageTtlSelection === TtlType.On &&
|
||||
isDirty(this.props.analyticalStorageTtlSeconds, this.props.analyticalStorageTtlSecondsBaseline)) ||
|
||||
isDirty(this.props.geospatialConfigType, this.props.geospatialConfigTypeBaseline) ||
|
||||
isDirty(this.props.changeFeedPolicy, this.props.changeFeedPolicyBaseline)
|
||||
) {
|
||||
return { isSaveable: true, isDiscardable: true };
|
||||
}
|
||||
|
||||
return { isSaveable: false, isDiscardable: false };
|
||||
};
|
||||
|
||||
private ttlChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: TtlType.Off, text: "Off" },
|
||||
{ key: TtlType.OnNoDefault, text: "On (no default)" },
|
||||
{ key: TtlType.On, text: "On" }
|
||||
];
|
||||
|
||||
private getTtlComponent = (): JSX.Element => (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<ChoiceGroup
|
||||
id="timeToLive"
|
||||
label="Time to Live"
|
||||
selectedKey={this.props.timeToLive}
|
||||
options={this.ttlChoiceGroupOptions}
|
||||
onChange={this.props.onTtlChange}
|
||||
styles={getChoiceGroupStyles(this.props.timeToLive, this.props.timeToLiveBaseline)}
|
||||
/>
|
||||
{isDirty(this.props.timeToLive, this.props.timeToLiveBaseline) && this.props.timeToLive === TtlType.On && (
|
||||
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
||||
{ttlWarning}
|
||||
</MessageBar>
|
||||
)}
|
||||
{this.props.timeToLive === TtlType.On && (
|
||||
<TextField
|
||||
id="timeToLiveSeconds"
|
||||
styles={getTextFieldStyles(this.props.timeToLiveSeconds, this.props.timeToLiveSecondsBaseline)}
|
||||
type="number"
|
||||
required
|
||||
min={1}
|
||||
max={Int32.Max}
|
||||
value={this.props.timeToLiveSeconds?.toString()}
|
||||
onChange={this.props.onTimeToLiveSecondsChange}
|
||||
suffix="second(s)"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: TtlType.Off, text: "Off", disabled: true },
|
||||
{ key: TtlType.OnNoDefault, text: "On (no default)" },
|
||||
{ key: TtlType.On, text: "On" }
|
||||
];
|
||||
|
||||
private getAnalyticalStorageTtlComponent = (): JSX.Element => (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<ChoiceGroup
|
||||
id="analyticalStorageTimeToLive"
|
||||
label="Analytical Storage Time to Live"
|
||||
selectedKey={this.props.analyticalStorageTtlSelection}
|
||||
options={this.analyticalTtlChoiceGroupOptions}
|
||||
onChange={this.props.onAnalyticalStorageTtlSelectionChange}
|
||||
styles={getChoiceGroupStyles(
|
||||
this.props.analyticalStorageTtlSelection,
|
||||
this.props.analyticalStorageTtlSelectionBaseline
|
||||
)}
|
||||
/>
|
||||
{this.props.analyticalStorageTtlSelection === TtlType.On && (
|
||||
<TextField
|
||||
id="analyticalStorageTimeToLiveSeconds"
|
||||
styles={getTextFieldStyles(
|
||||
this.props.analyticalStorageTtlSeconds,
|
||||
this.props.analyticalStorageTtlSecondsBaseline
|
||||
)}
|
||||
type="number"
|
||||
required
|
||||
min={1}
|
||||
max={Int32.Max}
|
||||
value={this.props.analyticalStorageTtlSeconds?.toString()}
|
||||
suffix="second(s)"
|
||||
onChange={this.props.onAnalyticalStorageTtlSecondsChange}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: GeospatialConfigType.Geography, text: "Geography" },
|
||||
{ key: GeospatialConfigType.Geometry, text: "Geometry" }
|
||||
];
|
||||
|
||||
private getGeoSpatialComponent = (): JSX.Element => (
|
||||
<ChoiceGroup
|
||||
id="geoSpatialConfig"
|
||||
label="Geospatial Configuration"
|
||||
selectedKey={this.props.geospatialConfigType}
|
||||
options={this.geoSpatialConfigTypeChoiceGroupOptions}
|
||||
onChange={this.props.onGeoSpatialConfigTypeChange}
|
||||
styles={getChoiceGroupStyles(this.props.geospatialConfigType, this.props.geospatialConfigTypeBaseline)}
|
||||
/>
|
||||
);
|
||||
|
||||
private changeFeedChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: ChangeFeedPolicyState.Off, text: "Off" },
|
||||
{ key: ChangeFeedPolicyState.On, text: "On" }
|
||||
];
|
||||
|
||||
private getChangeFeedComponent = (): JSX.Element => {
|
||||
const labelId = "settingsV2ChangeFeedLabelId";
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Label id={labelId}>
|
||||
<ToolTipLabelComponent label="Change feed log retention policy" toolTipElement={changeFeedPolicyToolTip} />
|
||||
</Label>
|
||||
<ChoiceGroup
|
||||
id="changeFeedPolicy"
|
||||
selectedKey={this.props.changeFeedPolicy}
|
||||
options={this.changeFeedChoiceGroupOptions}
|
||||
onChange={this.props.onChangeFeedPolicyChange}
|
||||
styles={getChoiceGroupStyles(this.props.changeFeedPolicy, this.props.changeFeedPolicyBaseline)}
|
||||
aria-labelledby={labelId}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
private getPartitionKeyComponent = (): JSX.Element => (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
{this.getPartitionKeyVisible() && (
|
||||
<TextField
|
||||
label={this.partitionKeyName}
|
||||
disabled
|
||||
styles={getTextFieldStyles(undefined, undefined)}
|
||||
defaultValue={this.partitionKeyValue}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.isLargePartitionKeyEnabled() && <Text>Large {this.partitionKeyName.toLowerCase()} has been enabled</Text>}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
public getPartitionKeyVisible = (): boolean => {
|
||||
if (
|
||||
this.props.container.isPreferredApiCassandra() ||
|
||||
this.props.container.isPreferredApiTable() ||
|
||||
!this.props.collection.partitionKeyProperty ||
|
||||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
public isLargePartitionKeyEnabled = (): boolean => this.props.collection.partitionKey?.version >= 2;
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Stack {...subComponentStackProps}>
|
||||
{this.ttlVisible && this.getTtlComponent()}
|
||||
|
||||
{this.geospatialVisible && this.getGeoSpatialComponent()}
|
||||
|
||||
{this.props.isAnalyticalStorageEnabled && this.getAnalyticalStorageTtlComponent()}
|
||||
|
||||
{this.props.changeFeedPolicyVisible && this.getChangeFeedComponent()}
|
||||
|
||||
{this.getPartitionKeyComponent()}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import {
|
||||
ThroughputInputAutoPilotV3Component,
|
||||
ThroughputInputAutoPilotV3Props
|
||||
} from "./ThroughputInputAutoPilotV3Component";
|
||||
import * as DataModels from "../../../../../Contracts/DataModels";
|
||||
|
||||
describe("ThroughputInputAutoPilotV3Component", () => {
|
||||
const baseProps: ThroughputInputAutoPilotV3Props = {
|
||||
databaseAccount: {} as DataModels.DatabaseAccount,
|
||||
serverId: undefined,
|
||||
wasAutopilotOriginallySet: false,
|
||||
throughput: 100,
|
||||
throughputBaseline: 100,
|
||||
onThroughputChange: undefined,
|
||||
minimum: 10000,
|
||||
maximum: 400,
|
||||
step: 100,
|
||||
isEnabled: true,
|
||||
isEmulator: false,
|
||||
spendAckChecked: false,
|
||||
spendAckId: "spendAckId",
|
||||
spendAckText: "spendAckText",
|
||||
spendAckVisible: false,
|
||||
showAsMandatory: true,
|
||||
isFixed: false,
|
||||
label: "label",
|
||||
infoBubbleText: "infoBubbleText",
|
||||
canExceedMaximumValue: true,
|
||||
onAutoPilotSelected: undefined,
|
||||
isAutoPilotSelected: false,
|
||||
maxAutoPilotThroughput: 4000,
|
||||
maxAutoPilotThroughputBaseline: 4000,
|
||||
onMaxAutoPilotThroughputChange: undefined,
|
||||
onScaleSaveableChange: () => {
|
||||
return;
|
||||
},
|
||||
onScaleDiscardableChange: () => {
|
||||
return;
|
||||
},
|
||||
getThroughputWarningMessage: () => undefined
|
||||
};
|
||||
|
||||
it("throughput input visible", () => {
|
||||
const wrapper = shallow(<ThroughputInputAutoPilotV3Component {...baseProps} />);
|
||||
const throughputComponent = wrapper.instance() as ThroughputInputAutoPilotV3Component;
|
||||
expect(throughputComponent.hasProvisioningTypeChanged()).toEqual(false);
|
||||
expect(throughputComponent.overrideWithProvisionedThroughputSettings()).toEqual(false);
|
||||
expect(throughputComponent.overrideWithAutoPilotSettings()).toEqual(false);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists("#throughputInput")).toEqual(true);
|
||||
expect(wrapper.exists("#autopilotInput")).toEqual(false);
|
||||
expect(wrapper.exists("#throughputSpendElement")).toEqual(true);
|
||||
expect(wrapper.exists("#autoscaleSpendElement")).toEqual(false);
|
||||
});
|
||||
|
||||
it("autopilot input visible", () => {
|
||||
const newProps = { ...baseProps, isAutoPilotSelected: true };
|
||||
const wrapper = shallow(<ThroughputInputAutoPilotV3Component {...newProps} />);
|
||||
const throughputComponent = wrapper.instance() as ThroughputInputAutoPilotV3Component;
|
||||
expect(throughputComponent.hasProvisioningTypeChanged()).toEqual(true);
|
||||
expect(throughputComponent.overrideWithProvisionedThroughputSettings()).toEqual(true);
|
||||
expect(throughputComponent.overrideWithAutoPilotSettings()).toEqual(false);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists("#autopilotInput")).toEqual(true);
|
||||
expect(wrapper.exists("#throughputInput")).toEqual(false);
|
||||
expect(wrapper.exists("#manualToAutoscaleDisclaimerElement")).toEqual(true);
|
||||
|
||||
wrapper.setProps({ wasAutopilotOriginallySet: true });
|
||||
wrapper.update();
|
||||
expect(wrapper.exists("#autoscaleSpendElement")).toEqual(true);
|
||||
expect(wrapper.exists("#throughputSpendElement")).toEqual(false);
|
||||
});
|
||||
|
||||
it("spendAck checkbox visible", () => {
|
||||
const newProps = { ...baseProps, spendAckVisible: true };
|
||||
const wrapper = shallow(<ThroughputInputAutoPilotV3Component {...newProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.exists("#spendAckCheckBox")).toEqual(true);
|
||||
});
|
||||
|
||||
it("scale saveable and discardable are set", () => {
|
||||
let throughputComponent = new ThroughputInputAutoPilotV3Component(baseProps);
|
||||
let isComponentDirtyResult = throughputComponent.IsComponentDirty();
|
||||
expect(isComponentDirtyResult.isSaveable).toEqual(false);
|
||||
expect(isComponentDirtyResult.isDiscardable).toEqual(false);
|
||||
|
||||
const newProps = { ...baseProps, throughput: 1000000 };
|
||||
throughputComponent = new ThroughputInputAutoPilotV3Component(newProps);
|
||||
isComponentDirtyResult = throughputComponent.IsComponentDirty();
|
||||
expect(isComponentDirtyResult.isSaveable).toEqual(true);
|
||||
expect(isComponentDirtyResult.isDiscardable).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -1,342 +0,0 @@
|
||||
import React from "react";
|
||||
import * as AutoPilotUtils from "../../../../../Utils/AutoPilotUtils";
|
||||
import {
|
||||
getTextFieldStyles,
|
||||
getToolTipContainer,
|
||||
spendAckCheckBoxStyle,
|
||||
titleAndInputStackProps,
|
||||
checkBoxAndInputStackProps,
|
||||
getChoiceGroupStyles,
|
||||
messageBarStyles,
|
||||
getEstimatedSpendElement,
|
||||
getEstimatedAutoscaleSpendElement,
|
||||
getAutoPilotV3SpendElement,
|
||||
manualToAutoscaleDisclaimerElement
|
||||
} from "../../SettingsRenderUtils";
|
||||
import {
|
||||
Text,
|
||||
TextField,
|
||||
ChoiceGroup,
|
||||
IChoiceGroupOption,
|
||||
Checkbox,
|
||||
Stack,
|
||||
Label,
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarType
|
||||
} from "office-ui-fabric-react";
|
||||
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
|
||||
import { IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
|
||||
import * as SharedConstants from "../../../../../Shared/Constants";
|
||||
import * as DataModels from "../../../../../Contracts/DataModels";
|
||||
|
||||
export interface ThroughputInputAutoPilotV3Props {
|
||||
databaseAccount: DataModels.DatabaseAccount;
|
||||
serverId: string;
|
||||
throughput: number;
|
||||
throughputBaseline: number;
|
||||
onThroughputChange: (newThroughput: number) => void;
|
||||
minimum: number;
|
||||
maximum: number;
|
||||
step?: number;
|
||||
isEnabled?: boolean;
|
||||
spendAckChecked?: boolean;
|
||||
spendAckId?: string;
|
||||
spendAckText?: string;
|
||||
spendAckVisible?: boolean;
|
||||
showAsMandatory?: boolean;
|
||||
isFixed: boolean;
|
||||
isEmulator: boolean;
|
||||
label: string;
|
||||
infoBubbleText?: string;
|
||||
canExceedMaximumValue?: boolean;
|
||||
onAutoPilotSelected: (isAutoPilotSelected: boolean) => void;
|
||||
isAutoPilotSelected: boolean;
|
||||
wasAutopilotOriginallySet: boolean;
|
||||
maxAutoPilotThroughput: number;
|
||||
maxAutoPilotThroughputBaseline: number;
|
||||
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
|
||||
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||
getThroughputWarningMessage: () => JSX.Element;
|
||||
}
|
||||
|
||||
interface ThroughputInputAutoPilotV3State {
|
||||
spendAckChecked: boolean;
|
||||
}
|
||||
|
||||
export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
ThroughputInputAutoPilotV3Props,
|
||||
ThroughputInputAutoPilotV3State
|
||||
> {
|
||||
private shouldCheckComponentIsDirty = true;
|
||||
private static readonly defaultStep = 100;
|
||||
private static readonly zeroThroughput = 0;
|
||||
private step: number;
|
||||
private choiceGroupFixedStyle = getChoiceGroupStyles(undefined, undefined);
|
||||
private options: IChoiceGroupOption[] = [
|
||||
{ key: "true", text: "Autoscale" },
|
||||
{ key: "false", text: "Manual" }
|
||||
];
|
||||
|
||||
componentDidMount(): void {
|
||||
this.onComponentUpdate();
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
this.onComponentUpdate();
|
||||
}
|
||||
|
||||
private onComponentUpdate = (): void => {
|
||||
if (!this.shouldCheckComponentIsDirty) {
|
||||
this.shouldCheckComponentIsDirty = true;
|
||||
return;
|
||||
}
|
||||
const isComponentDirtyResult = this.IsComponentDirty();
|
||||
this.props.onScaleSaveableChange(isComponentDirtyResult.isSaveable);
|
||||
this.props.onScaleDiscardableChange(isComponentDirtyResult.isDiscardable);
|
||||
|
||||
this.shouldCheckComponentIsDirty = false;
|
||||
};
|
||||
|
||||
public IsComponentDirty = (): IsComponentDirtyResult => {
|
||||
let isSaveable = false;
|
||||
let isDiscardable = false;
|
||||
|
||||
if (this.props.isEnabled) {
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
isSaveable = true;
|
||||
isDiscardable = true;
|
||||
} else if (this.props.isAutoPilotSelected) {
|
||||
if (isDirty(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)) {
|
||||
isDiscardable = true;
|
||||
if (AutoPilotUtils.isValidAutoPilotThroughput(this.props.maxAutoPilotThroughput)) {
|
||||
isSaveable = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isDirty(this.props.throughput, this.props.throughputBaseline)) {
|
||||
isDiscardable = true;
|
||||
isSaveable = true;
|
||||
if (
|
||||
!this.props.throughput ||
|
||||
this.props.throughput < this.props.minimum ||
|
||||
(this.props.throughput > this.props.maximum && (this.props.isEmulator || this.props.isFixed)) ||
|
||||
(this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
!this.props.canExceedMaximumValue)
|
||||
) {
|
||||
isSaveable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { isSaveable, isDiscardable };
|
||||
};
|
||||
|
||||
public constructor(props: ThroughputInputAutoPilotV3Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
spendAckChecked: this.props.spendAckChecked
|
||||
};
|
||||
|
||||
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
|
||||
}
|
||||
|
||||
public hasProvisioningTypeChanged = (): boolean =>
|
||||
this.props.wasAutopilotOriginallySet !== this.props.isAutoPilotSelected;
|
||||
|
||||
public overrideWithAutoPilotSettings = (): boolean =>
|
||||
this.hasProvisioningTypeChanged() && this.props.wasAutopilotOriginallySet;
|
||||
|
||||
public overrideWithProvisionedThroughputSettings = (): boolean =>
|
||||
this.hasProvisioningTypeChanged() && !this.props.wasAutopilotOriginallySet;
|
||||
|
||||
private getRequestUnitsUsageCost = (): JSX.Element => {
|
||||
const account = this.props.databaseAccount;
|
||||
if (!account) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const serverId: string = this.props.serverId;
|
||||
const offerThroughput: number = this.props.throughput;
|
||||
|
||||
const regions = account?.properties?.readLocations?.length || 1;
|
||||
const multimaster = account?.properties?.enableMultipleWriteLocations || false;
|
||||
|
||||
let estimatedSpend: JSX.Element;
|
||||
|
||||
if (!this.props.isAutoPilotSelected) {
|
||||
estimatedSpend = getEstimatedSpendElement(
|
||||
// if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set...
|
||||
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : offerThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
estimatedSpend = getEstimatedAutoscaleSpendElement(
|
||||
this.props.maxAutoPilotThroughput,
|
||||
serverId,
|
||||
regions,
|
||||
multimaster
|
||||
);
|
||||
}
|
||||
return estimatedSpend;
|
||||
};
|
||||
|
||||
private getAutoPilotUsageCost = (): JSX.Element => {
|
||||
if (!this.props.maxAutoPilotThroughput) {
|
||||
return <></>;
|
||||
}
|
||||
return getAutoPilotV3SpendElement(
|
||||
this.props.maxAutoPilotThroughput,
|
||||
false /* isDatabaseThroughput */,
|
||||
!this.props.isEmulator ? this.getRequestUnitsUsageCost() : <></>
|
||||
);
|
||||
};
|
||||
|
||||
private onAutoPilotThroughputChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
let newThroughput = parseInt(newValue);
|
||||
newThroughput = isNaN(newThroughput) ? ThroughputInputAutoPilotV3Component.zeroThroughput : newThroughput;
|
||||
this.props.onMaxAutoPilotThroughputChange(newThroughput);
|
||||
};
|
||||
|
||||
private onThroughputChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
let newThroughput = parseInt(newValue);
|
||||
newThroughput = isNaN(newThroughput) ? ThroughputInputAutoPilotV3Component.zeroThroughput : newThroughput;
|
||||
|
||||
if (this.overrideWithAutoPilotSettings()) {
|
||||
this.props.onMaxAutoPilotThroughputChange(newThroughput);
|
||||
} else {
|
||||
this.props.onThroughputChange(newThroughput);
|
||||
}
|
||||
};
|
||||
|
||||
private onChoiceGroupChange = (
|
||||
event?: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||
option?: IChoiceGroupOption
|
||||
): void => this.props.onAutoPilotSelected(option.key === "true");
|
||||
|
||||
private renderThroughputModeChoices = (): JSX.Element => {
|
||||
const labelId = "settingsV2RadioButtonLabelId";
|
||||
return (
|
||||
<Stack>
|
||||
<Label id={labelId}>
|
||||
<ToolTipLabelComponent
|
||||
label={this.props.label}
|
||||
toolTipElement={getToolTipContainer(this.props.infoBubbleText)}
|
||||
/>
|
||||
</Label>
|
||||
{this.overrideWithProvisionedThroughputSettings() && (
|
||||
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
||||
{manualToAutoscaleDisclaimerElement}
|
||||
</MessageBar>
|
||||
)}
|
||||
<ChoiceGroup
|
||||
selectedKey={this.props.isAutoPilotSelected.toString()}
|
||||
options={this.options}
|
||||
onChange={this.onChoiceGroupChange}
|
||||
required={this.props.showAsMandatory}
|
||||
ariaLabelledBy={labelId}
|
||||
styles={this.choiceGroupFixedStyle}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
private onSpendAckChecked = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean): void =>
|
||||
this.setState({ spendAckChecked: checked });
|
||||
|
||||
private renderAutoPilotInput = (): JSX.Element => (
|
||||
<>
|
||||
<Text>
|
||||
Provision maximum RU/s required by this resource. Estimate your required RU/s with
|
||||
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
|
||||
{` capacity calculator`}
|
||||
</Link>
|
||||
</Text>
|
||||
<TextField
|
||||
label="Max RU/s"
|
||||
required
|
||||
type="number"
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
|
||||
disabled={this.overrideWithProvisionedThroughputSettings()}
|
||||
step={this.step}
|
||||
min={AutoPilotUtils.minAutoPilotThroughput}
|
||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
||||
onChange={this.onAutoPilotThroughputChange}
|
||||
/>
|
||||
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
||||
{this.props.spendAckVisible && (
|
||||
<Checkbox
|
||||
id="spendAckCheckBox"
|
||||
styles={spendAckCheckBoxStyle}
|
||||
label={this.props.spendAckText}
|
||||
checked={this.state.spendAckChecked}
|
||||
onChange={this.onSpendAckChecked}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
private renderThroughputInput = (): JSX.Element => (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<TextField
|
||||
required
|
||||
type="number"
|
||||
id="throughputInput"
|
||||
key="provisioned throughput input"
|
||||
styles={getTextFieldStyles(this.props.throughput, this.props.throughputBaseline)}
|
||||
disabled={this.overrideWithAutoPilotSettings()}
|
||||
step={this.step}
|
||||
min={this.props.minimum}
|
||||
max={this.props.canExceedMaximumValue ? undefined : this.props.maximum}
|
||||
value={
|
||||
this.overrideWithAutoPilotSettings()
|
||||
? this.props.maxAutoPilotThroughputBaseline?.toString()
|
||||
: this.props.throughput?.toString()
|
||||
}
|
||||
onChange={this.onThroughputChange}
|
||||
/>
|
||||
|
||||
{this.props.getThroughputWarningMessage() && (
|
||||
<MessageBar messageBarType={MessageBarType.warning} styles={messageBarStyles}>
|
||||
{this.props.getThroughputWarningMessage()}
|
||||
</MessageBar>
|
||||
)}
|
||||
|
||||
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
|
||||
|
||||
{this.props.spendAckVisible && (
|
||||
<Checkbox
|
||||
id="spendAckCheckBox"
|
||||
styles={spendAckCheckBoxStyle}
|
||||
label={this.props.spendAckText}
|
||||
checked={this.state.spendAckChecked}
|
||||
onChange={this.onSpendAckChecked}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.props.isFixed && <p>Choose unlimited storage capacity for more than 10,000 RU/s.</p>}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Stack {...checkBoxAndInputStackProps}>
|
||||
{!this.props.isFixed && this.renderThroughputModeChoices()}
|
||||
|
||||
{this.props.isAutoPilotSelected ? this.renderAutoPilotInput() : this.renderThroughputInput()}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,434 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack>
|
||||
<StyledLabelBase
|
||||
id="settingsV2RadioButtonLabelId"
|
||||
>
|
||||
<ToolTipLabelComponent
|
||||
label="label"
|
||||
toolTipElement={
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
infoBubbleText
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</StyledLabelBase>
|
||||
<StyledMessageBarBase
|
||||
messageBarType={5}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"marginTop": "5px",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
id="manualToAutoscaleDisclaimerElement"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s.
|
||||
|
||||
<a
|
||||
href="https://aka.ms/cosmos-autoscale-migration"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</Text>
|
||||
</StyledMessageBarBase>
|
||||
<StyledChoiceGroupBase
|
||||
ariaLabelledBy="settingsV2RadioButtonLabelId"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "true",
|
||||
"text": "Autoscale",
|
||||
},
|
||||
Object {
|
||||
"key": "false",
|
||||
"text": "Manual",
|
||||
},
|
||||
]
|
||||
}
|
||||
required={true}
|
||||
selectedKey="true"
|
||||
styles={
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
},
|
||||
".ms-ChoiceField-field.is-checked::before": Object {
|
||||
"borderColor": "",
|
||||
},
|
||||
".ms-ChoiceField-wrapper label": Object {
|
||||
"fontFamily": undefined,
|
||||
"fontSize": 14,
|
||||
"padding": "2px 5px",
|
||||
"whiteSpace": "nowrap",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Text>
|
||||
Provision maximum RU/s required by this resource. Estimate your required RU/s with
|
||||
<StyledLinkBase
|
||||
href="https://cosmos.azure.com/capacitycalculator/"
|
||||
target="_blank"
|
||||
>
|
||||
capacity calculator
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
<StyledTextFieldBase
|
||||
disabled={true}
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
label="Max RU/s"
|
||||
min={4000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
step={100}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"borderColor": "",
|
||||
"height": 25,
|
||||
"selectors": Object {
|
||||
":disabled": Object {
|
||||
"backgroundColor": undefined,
|
||||
"borderColor": undefined,
|
||||
},
|
||||
},
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
type="number"
|
||||
value=""
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack>
|
||||
<StyledLabelBase
|
||||
id="settingsV2RadioButtonLabelId"
|
||||
>
|
||||
<ToolTipLabelComponent
|
||||
label="label"
|
||||
toolTipElement={
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
infoBubbleText
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</StyledLabelBase>
|
||||
<StyledChoiceGroupBase
|
||||
ariaLabelledBy="settingsV2RadioButtonLabelId"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "true",
|
||||
"text": "Autoscale",
|
||||
},
|
||||
Object {
|
||||
"key": "false",
|
||||
"text": "Manual",
|
||||
},
|
||||
]
|
||||
}
|
||||
required={true}
|
||||
selectedKey="false"
|
||||
styles={
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
},
|
||||
".ms-ChoiceField-field.is-checked::before": Object {
|
||||
"borderColor": "",
|
||||
},
|
||||
".ms-ChoiceField-wrapper label": Object {
|
||||
"fontFamily": undefined,
|
||||
"fontSize": 14,
|
||||
"padding": "2px 5px",
|
||||
"whiteSpace": "nowrap",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
disabled={false}
|
||||
id="throughputInput"
|
||||
key="provisioned throughput input"
|
||||
min={10000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
step={100}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"borderColor": "",
|
||||
"height": 25,
|
||||
"selectors": Object {
|
||||
":disabled": Object {
|
||||
"backgroundColor": undefined,
|
||||
"borderColor": undefined,
|
||||
},
|
||||
},
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
type="number"
|
||||
value="100"
|
||||
/>
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
>
|
||||
Estimated cost (
|
||||
USD
|
||||
):
|
||||
|
||||
<b>
|
||||
$
|
||||
0.0080
|
||||
hourly
|
||||
/
|
||||
$
|
||||
0.19
|
||||
daily
|
||||
/
|
||||
$
|
||||
5.84
|
||||
monthly
|
||||
|
||||
</b>
|
||||
(
|
||||
regions:
|
||||
|
||||
1
|
||||
,
|
||||
100
|
||||
RU/s,
|
||||
$
|
||||
0.00008
|
||||
/RU)
|
||||
</Text>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
id="spendAckCheckBox"
|
||||
label="spendAckText"
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"label": Object {
|
||||
"margin": 0,
|
||||
"padding": "2 0 2 0",
|
||||
},
|
||||
"text": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack>
|
||||
<StyledLabelBase
|
||||
id="settingsV2RadioButtonLabelId"
|
||||
>
|
||||
<ToolTipLabelComponent
|
||||
label="label"
|
||||
toolTipElement={
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
infoBubbleText
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</StyledLabelBase>
|
||||
<StyledChoiceGroupBase
|
||||
ariaLabelledBy="settingsV2RadioButtonLabelId"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "true",
|
||||
"text": "Autoscale",
|
||||
},
|
||||
Object {
|
||||
"key": "false",
|
||||
"text": "Manual",
|
||||
},
|
||||
]
|
||||
}
|
||||
required={true}
|
||||
selectedKey="false"
|
||||
styles={
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
},
|
||||
".ms-ChoiceField-field.is-checked::before": Object {
|
||||
"borderColor": "",
|
||||
},
|
||||
".ms-ChoiceField-wrapper label": Object {
|
||||
"fontFamily": undefined,
|
||||
"fontSize": 14,
|
||||
"padding": "2px 5px",
|
||||
"whiteSpace": "nowrap",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
disabled={false}
|
||||
id="throughputInput"
|
||||
key="provisioned throughput input"
|
||||
min={10000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
step={100}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"borderColor": "",
|
||||
"height": 25,
|
||||
"selectors": Object {
|
||||
":disabled": Object {
|
||||
"backgroundColor": undefined,
|
||||
"borderColor": undefined,
|
||||
},
|
||||
},
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
type="number"
|
||||
value="100"
|
||||
/>
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
>
|
||||
Estimated cost (
|
||||
USD
|
||||
):
|
||||
|
||||
<b>
|
||||
$
|
||||
0.0080
|
||||
hourly
|
||||
/
|
||||
$
|
||||
0.19
|
||||
daily
|
||||
/
|
||||
$
|
||||
5.84
|
||||
monthly
|
||||
|
||||
</b>
|
||||
(
|
||||
regions:
|
||||
|
||||
1
|
||||
,
|
||||
100
|
||||
RU/s,
|
||||
$
|
||||
0.00008
|
||||
/RU)
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -1,15 +0,0 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { ToolTipLabelComponent, ToolTipLabelComponentProps } from "./ToolTipLabelComponent";
|
||||
|
||||
describe("ToolTipLabelComponent", () => {
|
||||
const props: ToolTipLabelComponentProps = {
|
||||
label: "sample tool tip label",
|
||||
toolTipElement: <span>sample tool tip text</span>
|
||||
};
|
||||
|
||||
it("renders", () => {
|
||||
const wrapper = shallow(<ToolTipLabelComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { Stack, Text, IIconStyles, Icon, TooltipHost, DirectionalHint } from "office-ui-fabric-react";
|
||||
import { toolTipLabelStackTokens } from "../SettingsRenderUtils";
|
||||
|
||||
export interface ToolTipLabelComponentProps {
|
||||
label: string;
|
||||
toolTipElement: JSX.Element;
|
||||
}
|
||||
|
||||
const iconButtonStyles: Partial<IIconStyles> = { root: { marginBottom: -3 } };
|
||||
|
||||
export class ToolTipLabelComponent extends React.Component<ToolTipLabelComponentProps> {
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Stack horizontal verticalAlign="center" tokens={toolTipLabelStackTokens}>
|
||||
{this.props.label && <Text style={{ fontWeight: 600 }}>{this.props.label}</Text>}
|
||||
{this.props.toolTipElement && (
|
||||
<TooltipHost
|
||||
content={this.props.toolTipElement}
|
||||
directionalHint={DirectionalHint.rightCenter}
|
||||
calloutProps={{ gapSpace: 0 }}
|
||||
styles={{ root: { display: "inline-block", float: "right" } }}
|
||||
>
|
||||
<Icon iconName="Info" ariaLabel="Info" styles={iconButtonStyles} />
|
||||
</TooltipHost>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ConflictResolutionComponent Path text field displayed 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledChoiceGroupBase
|
||||
label="Mode"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "LastWriterWins",
|
||||
"text": "Last Write Wins (default)",
|
||||
},
|
||||
Object {
|
||||
"key": "Custom",
|
||||
"text": "Merge Procedure (custom)",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey="LastWriterWins"
|
||||
styles={
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": undefined,
|
||||
},
|
||||
".ms-ChoiceField-field.is-checked::before": Object {
|
||||
"borderColor": undefined,
|
||||
},
|
||||
".ms-ChoiceField-wrapper label": Object {
|
||||
"fontFamily": undefined,
|
||||
"fontSize": 14,
|
||||
"padding": "2px 5px",
|
||||
"whiteSpace": "nowrap",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
/>
|
||||
<StyledTextFieldBase
|
||||
id="conflictResolutionLwwTextField"
|
||||
label="Conflict Resolver Property"
|
||||
onChange={[Function]}
|
||||
onRenderLabel={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"borderColor": "",
|
||||
"height": 25,
|
||||
"selectors": Object {
|
||||
":disabled": Object {
|
||||
"backgroundColor": undefined,
|
||||
"borderColor": undefined,
|
||||
},
|
||||
},
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
value=""
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`ConflictResolutionComponent Sproc text field displayed 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledChoiceGroupBase
|
||||
label="Mode"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "LastWriterWins",
|
||||
"text": "Last Write Wins (default)",
|
||||
},
|
||||
Object {
|
||||
"key": "Custom",
|
||||
"text": "Merge Procedure (custom)",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey="Custom"
|
||||
styles={
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
},
|
||||
".ms-ChoiceField-field.is-checked::before": Object {
|
||||
"borderColor": "",
|
||||
},
|
||||
".ms-ChoiceField-wrapper label": Object {
|
||||
"fontFamily": undefined,
|
||||
"fontSize": 14,
|
||||
"padding": "2px 5px",
|
||||
"whiteSpace": "nowrap",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
/>
|
||||
<StyledTextFieldBase
|
||||
id="conflictResolutionCustomTextField"
|
||||
label="Stored procedure"
|
||||
onChange={[Function]}
|
||||
onRenderLabel={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"fieldGroup": Object {
|
||||
"borderColor": "",
|
||||
"height": 25,
|
||||
"selectors": Object {
|
||||
":disabled": Object {
|
||||
"backgroundColor": undefined,
|
||||
"borderColor": undefined,
|
||||
},
|
||||
},
|
||||
"width": 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
value=""
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -1,16 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`IndexingPolicyComponent renders 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="settingsV2IndexingPolicyEditor"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -1,79 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ScaleComponent renders with correct intiial notification 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledMessageBarBase
|
||||
messageBarType={5}
|
||||
>
|
||||
<Text
|
||||
id="throughputApplyLongDelayMessage"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
|
||||
<br />
|
||||
Database:
|
||||
test
|
||||
, Container:
|
||||
test
|
||||
|
||||
, Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s
|
||||
</Text>
|
||||
</StyledMessageBarBase>
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ThroughputInputAutoPilotV3Component
|
||||
canExceedMaximumValue={false}
|
||||
getThroughputWarningMessage={[Function]}
|
||||
isAutoPilotSelected={false}
|
||||
isEmulator={false}
|
||||
isEnabled={true}
|
||||
isFixed={false}
|
||||
label="Throughput (6,000 - 40,000 RU/s)"
|
||||
maxAutoPilotThroughput={4000}
|
||||
maxAutoPilotThroughputBaseline={4000}
|
||||
maximum={40000}
|
||||
minimum={6000}
|
||||
onAutoPilotSelected={[Function]}
|
||||
onMaxAutoPilotThroughputChange={[Function]}
|
||||
onScaleDiscardableChange={[Function]}
|
||||
onScaleSaveableChange={[Function]}
|
||||
onThroughputChange={[Function]}
|
||||
spendAckChecked={false}
|
||||
throughput={1000}
|
||||
throughputBaseline={1000}
|
||||
wasAutopilotOriginallySet={true}
|
||||
/>
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledLabelBase>
|
||||
Storage capacity
|
||||
</StyledLabelBase>
|
||||
<Text>
|
||||
Unlimited
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,58 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ToolTipLabelComponent renders 1`] = `
|
||||
<Fragment>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 6,
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontWeight": 600,
|
||||
}
|
||||
}
|
||||
>
|
||||
sample tool tip label
|
||||
</Text>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
Object {
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
content={
|
||||
<span>
|
||||
sample tool tip text
|
||||
</span>
|
||||
}
|
||||
directionalHint={12}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"display": "inline-block",
|
||||
"float": "right",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Memo(StyledIconBase)
|
||||
ariaLabel="Info"
|
||||
iconName="Info"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"marginBottom": -3,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</Stack>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -1,89 +0,0 @@
|
||||
import { collection, container } from "./TestUtils";
|
||||
import {
|
||||
getMaxRUs,
|
||||
getMinRUs,
|
||||
hasDatabaseSharedThroughput,
|
||||
isDirty,
|
||||
isDirtyTypes,
|
||||
parseConflictResolutionMode,
|
||||
parseConflictResolutionProcedure
|
||||
} from "./SettingsUtils";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
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);
|
||||
|
||||
const newCollection = { ...collection };
|
||||
newCollection.getDatabase = () => {
|
||||
return {
|
||||
nodeKind: undefined,
|
||||
rid: undefined,
|
||||
container: undefined,
|
||||
self: "",
|
||||
id: ko.observable(""),
|
||||
collections: ko.observableArray(undefined),
|
||||
offer: ko.observable(undefined),
|
||||
isDatabaseExpanded: ko.observable(false),
|
||||
isDatabaseShared: ko.computed(() => true),
|
||||
selectedSubnodeKind: ko.observable(undefined),
|
||||
selectDatabase: undefined,
|
||||
expandDatabase: undefined,
|
||||
collapseDatabase: undefined,
|
||||
loadCollections: undefined,
|
||||
findCollectionWithId: undefined,
|
||||
openAddCollection: undefined,
|
||||
onDeleteDatabaseContextMenuClick: undefined,
|
||||
readSettings: undefined,
|
||||
onSettingsClick: undefined,
|
||||
loadOffer: undefined
|
||||
} as ViewModels.Database;
|
||||
};
|
||||
newCollection.offer(undefined);
|
||||
expect(hasDatabaseSharedThroughput(newCollection)).toEqual(true);
|
||||
});
|
||||
|
||||
it("parseConflictResolutionMode", () => {
|
||||
expect(parseConflictResolutionMode("custom")).toEqual(DataModels.ConflictResolutionMode.Custom);
|
||||
expect(parseConflictResolutionMode("lastwriterwins")).toEqual(DataModels.ConflictResolutionMode.LastWriterWins);
|
||||
});
|
||||
|
||||
it("parseConflictResolutionProcedure", () => {
|
||||
expect(parseConflictResolutionProcedure("/dbs/db/colls/coll/sprocs/conflictResSproc")).toEqual("conflictResSproc");
|
||||
expect(parseConflictResolutionProcedure("conflictResSproc")).toEqual("conflictResSproc");
|
||||
});
|
||||
|
||||
describe("isDirty", () => {
|
||||
const indexingPolicy = {
|
||||
automatic: true,
|
||||
indexingMode: "consistent",
|
||||
includedPaths: [],
|
||||
excludedPaths: []
|
||||
} as DataModels.IndexingPolicy;
|
||||
|
||||
const cases = [
|
||||
["baseline", "current"],
|
||||
[0, 1],
|
||||
[true, false],
|
||||
[undefined, indexingPolicy],
|
||||
[indexingPolicy, { ...indexingPolicy, automatic: false }]
|
||||
];
|
||||
|
||||
test.each(cases)("", (baseline: isDirtyTypes, current: isDirtyTypes) => {
|
||||
expect(isDirty(baseline, baseline)).toEqual(false);
|
||||
expect(isDirty(baseline, current)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,171 +0,0 @@
|
||||
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";
|
||||
|
||||
export type isDirtyTypes = boolean | string | number | DataModels.IndexingPolicy;
|
||||
export const TtlOff = "off";
|
||||
export const TtlOn = "on";
|
||||
export const TtlOnNoDefault = "on-nodefault";
|
||||
|
||||
export enum ChangeFeedPolicyState {
|
||||
Off = "Off",
|
||||
On = "On"
|
||||
}
|
||||
|
||||
export enum TtlType {
|
||||
Off = "off",
|
||||
On = "on",
|
||||
OnNoDefault = "on-nodefault"
|
||||
}
|
||||
|
||||
export enum GeospatialConfigType {
|
||||
Geography = "Geography",
|
||||
Geometry = "Geometry"
|
||||
}
|
||||
|
||||
export enum SettingsV2TabTypes {
|
||||
ScaleTab,
|
||||
ConflictResolutionTab,
|
||||
SubSettingsTab,
|
||||
IndexingPolicyTab
|
||||
}
|
||||
|
||||
export interface IsComponentDirtyResult {
|
||||
isSaveable: boolean;
|
||||
isDiscardable: boolean;
|
||||
}
|
||||
|
||||
export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection): boolean => {
|
||||
const database: ViewModels.Database = collection.getDatabase();
|
||||
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) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const modeAsLowerCase: string = modeFromBackend.toLowerCase();
|
||||
if (modeAsLowerCase === DataModels.ConflictResolutionMode.Custom.toLowerCase()) {
|
||||
return DataModels.ConflictResolutionMode.Custom;
|
||||
}
|
||||
|
||||
// Default is LWW
|
||||
return DataModels.ConflictResolutionMode.LastWriterWins;
|
||||
};
|
||||
|
||||
export const parseConflictResolutionProcedure = (procedureFromBackEnd: string): string => {
|
||||
// Backend data comes in /dbs/xxxx/colls/xxxx/sprocs/{name}, to make it easier for users, we just use the name
|
||||
if (!procedureFromBackEnd) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (procedureFromBackEnd.indexOf("/") >= 0) {
|
||||
const sprocsIndex: number = procedureFromBackEnd.indexOf(Constants.HashRoutePrefixes.sprocHash);
|
||||
if (sprocsIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return procedureFromBackEnd.substr(sprocsIndex + Constants.HashRoutePrefixes.sprocHash.length);
|
||||
}
|
||||
|
||||
// No path, just a name, in case backend returns just the name
|
||||
return procedureFromBackEnd;
|
||||
};
|
||||
|
||||
export const isDirty = (current: isDirtyTypes, baseline: isDirtyTypes): boolean => {
|
||||
const currentType = typeof current;
|
||||
const baselineType = typeof baseline;
|
||||
|
||||
if (currentType !== "undefined" && baselineType !== "undefined" && currentType !== baselineType) {
|
||||
throw new Error("current and baseline values are not of the same type.");
|
||||
}
|
||||
const currentStringValue = getStringValue(current, currentType);
|
||||
const baselineStringValue = getStringValue(baseline, baselineType);
|
||||
|
||||
return currentStringValue !== baselineStringValue;
|
||||
};
|
||||
|
||||
const getStringValue = (value: isDirtyTypes, type: string): string => {
|
||||
switch (type) {
|
||||
case "string":
|
||||
case "undefined":
|
||||
case "number":
|
||||
case "boolean":
|
||||
return value?.toString();
|
||||
|
||||
default:
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
};
|
||||
|
||||
export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
||||
switch (tab) {
|
||||
case SettingsV2TabTypes.ScaleTab:
|
||||
return "Scale";
|
||||
case SettingsV2TabTypes.ConflictResolutionTab:
|
||||
return "Conflict Resolution";
|
||||
case SettingsV2TabTypes.SubSettingsTab:
|
||||
return "Settings";
|
||||
case SettingsV2TabTypes.IndexingPolicyTab:
|
||||
return "Indexing Policy";
|
||||
default:
|
||||
throw new Error(`Unknown tab ${tab}`);
|
||||
}
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import Explorer from "../../Explorer";
|
||||
import ko from "knockout";
|
||||
|
||||
export const container = new Explorer({
|
||||
notificationsClient: undefined,
|
||||
isEmulator: false
|
||||
});
|
||||
|
||||
export const collection = ({
|
||||
container: container,
|
||||
databaseId: "test",
|
||||
id: ko.observable<string>("test"),
|
||||
defaultTtl: ko.observable<number>(5),
|
||||
analyticalStorageTtl: ko.observable<number>(undefined),
|
||||
indexingPolicy: ko.observable<DataModels.IndexingPolicy>({
|
||||
automatic: true,
|
||||
indexingMode: "default",
|
||||
includedPaths: [],
|
||||
excludedPaths: []
|
||||
}),
|
||||
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
|
||||
quotaInfo: ko.observable<DataModels.CollectionQuotaInfo>({} as DataModels.CollectionQuotaInfo),
|
||||
offer: ko.observable<DataModels.Offer>({
|
||||
content: {
|
||||
offerThroughput: 10000,
|
||||
offerIsRUPerMinuteThroughputEnabled: false,
|
||||
collectionThroughputInfo: {
|
||||
minimumRUForCollection: 6000,
|
||||
numPhysicalPartitions: 4
|
||||
} as DataModels.OfferThroughputInfo
|
||||
}
|
||||
} as DataModels.Offer),
|
||||
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
|
||||
{} as DataModels.ConflictResolutionPolicy
|
||||
),
|
||||
changeFeedPolicy: ko.observable<DataModels.ChangeFeedPolicy>({} as DataModels.ChangeFeedPolicy),
|
||||
geospatialConfig: ko.observable<DataModels.GeospatialConfig>({} as DataModels.GeospatialConfig),
|
||||
getDatabase: () => {
|
||||
return;
|
||||
},
|
||||
partitionKey: {
|
||||
paths: [],
|
||||
kind: "hash",
|
||||
version: 2
|
||||
},
|
||||
partitionKeyProperty: "partitionKey",
|
||||
readSettings: () => {
|
||||
return;
|
||||
}
|
||||
} as unknown) as ViewModels.Collection;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,314 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SettingsUtils functions render 1`] = `
|
||||
<Fragment>
|
||||
<Text>
|
||||
Your
|
||||
container
|
||||
throughput will automatically scale from
|
||||
|
||||
<b>
|
||||
100
|
||||
RU/s (10% of max RU/s) -
|
||||
|
||||
1000
|
||||
RU/s
|
||||
</b>
|
||||
|
||||
based on usage.
|
||||
<br />
|
||||
</Text>
|
||||
<Text>
|
||||
After the first
|
||||
10
|
||||
GB of data stored, the max RU/s will be automatically upgraded based on the new storage value.
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmos-autoscale-info"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
.
|
||||
</Text>
|
||||
<Text>
|
||||
Your
|
||||
database
|
||||
throughput will automatically scale from
|
||||
|
||||
<b>
|
||||
100
|
||||
RU/s (10% of max RU/s) -
|
||||
|
||||
1000
|
||||
RU/s
|
||||
</b>
|
||||
|
||||
based on usage.
|
||||
<br />
|
||||
</Text>
|
||||
<Text>
|
||||
After the first
|
||||
10
|
||||
GB of data stored, the max RU/s will be automatically upgraded based on the new storage value.
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmos-autoscale-info"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
.
|
||||
</Text>
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
>
|
||||
Estimated cost (
|
||||
RMB
|
||||
):
|
||||
|
||||
<b>
|
||||
¥
|
||||
1.29
|
||||
hourly
|
||||
/
|
||||
¥
|
||||
31.06
|
||||
daily
|
||||
/
|
||||
¥
|
||||
944.60
|
||||
monthly
|
||||
|
||||
</b>
|
||||
(
|
||||
regions:
|
||||
|
||||
2
|
||||
,
|
||||
1000
|
||||
RU/s,
|
||||
¥
|
||||
0.00051
|
||||
/RU)
|
||||
</Text>
|
||||
<Text
|
||||
id="autoscaleSpendElement"
|
||||
>
|
||||
Estimated monthly cost (
|
||||
RMB
|
||||
) is
|
||||
|
||||
<b>
|
||||
¥
|
||||
111.69
|
||||
-
|
||||
¥
|
||||
1116.90
|
||||
|
||||
</b>
|
||||
(
|
||||
regions:
|
||||
|
||||
2
|
||||
,
|
||||
100
|
||||
-
|
||||
1000
|
||||
RU/s,
|
||||
¥
|
||||
0.000765
|
||||
/RU)
|
||||
</Text>
|
||||
<Text
|
||||
id="manualToAutoscaleDisclaimerElement"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s.
|
||||
|
||||
<a
|
||||
href="https://aka.ms/cosmos-autoscale-migration"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete operation explicitly issued by a client application. For more information see,
|
||||
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmos-db-ttl"
|
||||
target="_blank"
|
||||
>
|
||||
Time to Live (TTL) in Azure Cosmos DB
|
||||
</StyledLinkBase>
|
||||
.
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
Changing the Indexing Policy impacts query results while the index transformation occurs. When a change is made and the indexing mode is set to consistent or lazy, queries return eventual results until the operation completes. For more information see,
|
||||
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmosdb/modify-index-policy"
|
||||
target="_blank"
|
||||
>
|
||||
Modifying Indexing Policies
|
||||
</StyledLinkBase>
|
||||
.
|
||||
</Text>
|
||||
<Text
|
||||
id="updateThroughputBeyondLimitWarningMessage"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
You are about to request an increase in throughput beyond the pre-allocated capacity. The service will scale out and increase throughput for the selected container. This operation will take 1-3 business days to complete. You can track the status of this request in Notifications.
|
||||
</Text>
|
||||
<Text
|
||||
id="updateThroughputDelayedApplyWarningMessage"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
You are about to request an increase in throughput beyond the pre-allocated capacity. This operation will take some time to complete.
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
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:
|
||||
sampleDb
|
||||
, Container:
|
||||
sampleCollection
|
||||
|
||||
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
|
||||
</Text>
|
||||
<Text
|
||||
id="throughputApplyShortDelayMessage"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
||||
<br />
|
||||
Database:
|
||||
sampleDb
|
||||
, Container:
|
||||
sampleCollection
|
||||
|
||||
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
|
||||
</Text>
|
||||
<Text
|
||||
id="throughputApplyLongDelayMessage"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
|
||||
<br />
|
||||
Database:
|
||||
sampleDb
|
||||
, Container:
|
||||
sampleCollection
|
||||
|
||||
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<span>
|
||||
Sample Text
|
||||
</span>
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
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.
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
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
|
||||
<StyledLinkBase
|
||||
className="linkDarkBackground"
|
||||
href="https://aka.ms/dataexplorerconflics"
|
||||
target="_blank"
|
||||
>
|
||||
conflicts feed
|
||||
</StyledLinkBase>
|
||||
. You can update/re-register the stored procedure at any time.
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
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.
|
||||
</Text>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -84,7 +84,7 @@
|
||||
step: step,
|
||||
'class':'migration collid select-font-size',
|
||||
min: minAutoPilotThroughput,
|
||||
'aria-label': 'Max request units per second',
|
||||
'aria-label': ariaLabel,
|
||||
type: isAutoscaleThroughputInputFieldRequired() ? 'number' : 'hidden',
|
||||
css: {
|
||||
dirty: maxAutoPilotThroughputSet.editableIsDirty
|
||||
|
||||
@@ -159,20 +159,4 @@ describe("TreeNodeComponent", () => {
|
||||
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders loading icon", () => {
|
||||
const node: TreeNode = {
|
||||
label: "label",
|
||||
children: [],
|
||||
isExpanded: true
|
||||
};
|
||||
|
||||
const props = {
|
||||
node,
|
||||
generation: 2,
|
||||
paddingLeft: 9
|
||||
};
|
||||
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,14 +17,12 @@ import {
|
||||
|
||||
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
|
||||
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
||||
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||
|
||||
export interface TreeNodeMenuItem {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
iconSrc?: string;
|
||||
isDisabled?: boolean;
|
||||
styleClass?: string;
|
||||
}
|
||||
|
||||
export interface TreeNode {
|
||||
@@ -39,7 +37,6 @@ export interface TreeNode {
|
||||
data?: any; // Piece of data corresponding to this node
|
||||
timestamp?: number;
|
||||
isLeavesParentsSeparate?: boolean; // Display parents together first, then leaves
|
||||
isLoading?: boolean;
|
||||
isSelected?: () => boolean;
|
||||
onClick?: (isExpanded: boolean) => void; // Only if a leaf, other click will expand/collapse
|
||||
onExpanded?: () => void;
|
||||
@@ -186,9 +183,6 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
)}
|
||||
{node.contextMenu && this.renderContextMenuButton(node)}
|
||||
</div>
|
||||
<div className="loadingIconContainer">
|
||||
<img className="loadingIcon" src={LoadingIndicator_3Squares} hidden={!this.props.node.isLoading} />
|
||||
</div>
|
||||
{node.children && (
|
||||
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
|
||||
<div className="nodeChildren" data-test={node.label}>
|
||||
@@ -262,20 +256,13 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
onContextMenu={e => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())}
|
||||
>
|
||||
{props.item.onRenderIcon()}
|
||||
<span
|
||||
className={
|
||||
"treeComponentMenuItemLabel" + (props.item.className ? ` ${props.item.className}Label` : "")
|
||||
}
|
||||
>
|
||||
{props.item.text}
|
||||
</span>
|
||||
<span className="treeComponentMenuItemLabel">{props.item.text}</span>
|
||||
</div>
|
||||
),
|
||||
items: node.contextMenu.map((menuItem: TreeNodeMenuItem) => ({
|
||||
key: menuItem.label,
|
||||
text: menuItem.label,
|
||||
disabled: menuItem.isDisabled,
|
||||
className: menuItem.styleClass,
|
||||
onClick: menuItem.onClick,
|
||||
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
|
||||
}))
|
||||
@@ -295,7 +282,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
<img
|
||||
className="expandCollapseIcon"
|
||||
src={this.state.isExpanded ? TriangleDownIcon : TriangleRightIcon}
|
||||
alt={this.state.isExpanded ? `${node.label} branch is expanded` : `${node.label} branch is collapsed`}
|
||||
alt={this.state.isExpanded ? "Branch is expanded" : "Branch is collapsed"}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) => this.onCollapseExpandIconKeyPress(event, node)}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
|
||||
@@ -49,7 +49,7 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
|
||||
tabIndex={-1}
|
||||
>
|
||||
<img
|
||||
alt="label branch is collapsed"
|
||||
alt="Branch is collapsed"
|
||||
className="expandCollapseIcon"
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
@@ -63,15 +63,6 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
|
||||
label
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="loadingIconContainer"
|
||||
>
|
||||
<img
|
||||
className="loadingIcon"
|
||||
hidden={true}
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
<AnimateHeight
|
||||
animateOpacity={false}
|
||||
animationStateClasses={
|
||||
@@ -149,7 +140,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
tabIndex={-1}
|
||||
>
|
||||
<img
|
||||
alt="label branch is expanded"
|
||||
alt="Branch is expanded"
|
||||
className="expandCollapseIcon"
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
@@ -188,7 +179,6 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
"isBeakVisible": false,
|
||||
"items": Array [
|
||||
Object {
|
||||
"className": undefined,
|
||||
"disabled": true,
|
||||
"key": "menuLabel",
|
||||
"onClick": undefined,
|
||||
@@ -211,15 +201,6 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="loadingIconContainer"
|
||||
>
|
||||
<img
|
||||
className="loadingIcon"
|
||||
hidden={true}
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
<AnimateHeight
|
||||
animateOpacity={false}
|
||||
animationStateClasses={
|
||||
@@ -280,77 +261,6 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`TreeNodeComponent renders loading icon 1`] = `
|
||||
<div
|
||||
className=" main2 nodeItem "
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
>
|
||||
<div
|
||||
className="treeNodeHeader "
|
||||
data-test="label"
|
||||
style={
|
||||
Object {
|
||||
"paddingLeft": 9,
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<img
|
||||
alt="label branch is expanded"
|
||||
className="expandCollapseIcon"
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
src=""
|
||||
tabIndex={0}
|
||||
/>
|
||||
<span
|
||||
className="nodeLabel"
|
||||
title="label"
|
||||
>
|
||||
label
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="loadingIconContainer"
|
||||
>
|
||||
<img
|
||||
className="loadingIcon"
|
||||
hidden={true}
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
<AnimateHeight
|
||||
animateOpacity={false}
|
||||
animationStateClasses={
|
||||
Object {
|
||||
"animating": "rah-animating",
|
||||
"animatingDown": "rah-animating--down",
|
||||
"animatingToHeightAuto": "rah-animating--to-height-auto",
|
||||
"animatingToHeightSpecific": "rah-animating--to-height-specific",
|
||||
"animatingToHeightZero": "rah-animating--to-height-zero",
|
||||
"animatingUp": "rah-animating--up",
|
||||
"static": "rah-static",
|
||||
"staticHeightAuto": "rah-static--height-auto",
|
||||
"staticHeightSpecific": "rah-static--height-specific",
|
||||
"staticHeightZero": "rah-static--height-zero",
|
||||
}
|
||||
}
|
||||
applyInlineTransitions={true}
|
||||
delay={0}
|
||||
duration={200}
|
||||
easing="ease"
|
||||
height="auto"
|
||||
style={Object {}}
|
||||
>
|
||||
<div
|
||||
className="nodeChildren"
|
||||
data-test="label"
|
||||
/>
|
||||
</AnimateHeight>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents separated 1`] = `
|
||||
<div
|
||||
className="nodeClassname main12 nodeItem "
|
||||
@@ -368,7 +278,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
||||
tabIndex={-1}
|
||||
>
|
||||
<img
|
||||
alt="label branch is expanded"
|
||||
alt="Branch is expanded"
|
||||
className="expandCollapseIcon"
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
@@ -421,15 +331,6 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="loadingIconContainer"
|
||||
>
|
||||
<img
|
||||
className="loadingIcon"
|
||||
hidden={true}
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
<AnimateHeight
|
||||
animateOpacity={false}
|
||||
animationStateClasses={
|
||||
@@ -535,7 +436,7 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
|
||||
tabIndex={-1}
|
||||
>
|
||||
<img
|
||||
alt="label branch is expanded"
|
||||
alt="Branch is expanded"
|
||||
className="expandCollapseIcon"
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
@@ -549,15 +450,6 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
|
||||
label
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="loadingIconContainer"
|
||||
>
|
||||
<img
|
||||
className="loadingIcon"
|
||||
hidden={true}
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
<AnimateHeight
|
||||
animateOpacity={false}
|
||||
animationStateClasses={
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
}
|
||||
|
||||
&.showingMenu {
|
||||
background-color: #eee;
|
||||
background-color: #EEE;
|
||||
}
|
||||
|
||||
.treeMenuEllipsis {
|
||||
@@ -78,12 +78,3 @@
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.loadingIconContainer {
|
||||
width: 100%;
|
||||
|
||||
.loadingIcon {
|
||||
height: 6px;
|
||||
margin-left: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
jest.mock("../../Common/DocumentClientUtilityBase");
|
||||
jest.mock("../../Common/dataAccess/createCollection");
|
||||
import * as ko from "knockout";
|
||||
import * as sinon from "sinon";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@@ -34,8 +33,8 @@ describe("ContainerSampleGenerator", () => {
|
||||
databaseId: sampleDatabaseId,
|
||||
offerThroughput: 400,
|
||||
databaseLevelThroughput: false,
|
||||
createNewDatabase: true,
|
||||
collectionId: sampleCollectionId,
|
||||
rupmEnabled: false,
|
||||
data: [
|
||||
{
|
||||
firstname: "Eva",
|
||||
@@ -100,8 +99,8 @@ describe("ContainerSampleGenerator", () => {
|
||||
databaseId: sampleDatabaseId,
|
||||
offerThroughput: 400,
|
||||
databaseLevelThroughput: false,
|
||||
createNewDatabase: true,
|
||||
collectionId: sampleCollectionId,
|
||||
rupmEnabled: false,
|
||||
data: [
|
||||
"g.addV('person').property(id, '1').property('_partitionKey','pk').property('name', 'Eva').property('age', 44)"
|
||||
]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import GraphTab from ".././Tabs/GraphTab";
|
||||
@@ -5,11 +6,10 @@ import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsol
|
||||
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, getOrCreateDatabaseAndCollection } from "../../Common/DocumentClientUtilityBase";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
interface SampleDataFile extends DataModels.CreateCollectionParams {
|
||||
interface SampleDataFile extends DataModels.CreateDatabaseAndCollectionRequest {
|
||||
data: any[];
|
||||
}
|
||||
|
||||
@@ -54,11 +54,18 @@ export class ContainerSampleGenerator {
|
||||
}
|
||||
|
||||
private async createContainerAsync(): Promise<ViewModels.Collection> {
|
||||
const createRequest: DataModels.CreateCollectionParams = {
|
||||
const createRequest: DataModels.CreateDatabaseAndCollectionRequest = {
|
||||
...this.sampleDataFile
|
||||
};
|
||||
|
||||
await createCollection(createRequest);
|
||||
const options: any = {};
|
||||
if (this.container.isPreferredApiMongoDB()) {
|
||||
options.initialHeaders = options.initialHeaders || {};
|
||||
options.initialHeaders[Constants.HttpHeaders.supportSpatialLegacyCoordinates] = true;
|
||||
options.initialHeaders[Constants.HttpHeaders.usePolygonsSmallerThanAHemisphere] = true;
|
||||
}
|
||||
|
||||
await getOrCreateDatabaseAndCollection(createRequest, options);
|
||||
await this.container.refreshAllDatabases();
|
||||
const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId);
|
||||
if (!database) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
|
||||
import Database from "./Tree/Database";
|
||||
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
|
||||
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
|
||||
import { refreshCachedResources } from "../Common/DocumentClientUtilityBase";
|
||||
import { readOffers, refreshCachedResources } from "../Common/DocumentClientUtilityBase";
|
||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
||||
@@ -26,7 +26,7 @@ import NewVertexPane from "./Panes/NewVertexPane";
|
||||
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
|
||||
import Q from "q";
|
||||
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
|
||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import TerminalTab from "./Tabs/TerminalTab";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
import { ActionContracts, MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
@@ -87,7 +87,6 @@ import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
|
||||
import TabsBase from "./Tabs/TabsBase";
|
||||
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
|
||||
import { updateUserContext, userContext } from "../UserContext";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
|
||||
BindingHandlersRegisterer.registerBindingHandlers();
|
||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||
@@ -133,7 +132,6 @@ export default class Explorer {
|
||||
public isPreferredApiGraph: ko.Computed<boolean>;
|
||||
public isPreferredApiTable: ko.Computed<boolean>;
|
||||
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
|
||||
public isEnableMongoCapabilityPresent: ko.Computed<boolean>;
|
||||
public isServerlessEnabled: ko.Computed<boolean>;
|
||||
public isEmulator: boolean;
|
||||
public isAccountReady: ko.Observable<boolean>;
|
||||
@@ -213,7 +211,6 @@ export default class Explorer {
|
||||
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||
public isCodeOfConductEnabled: ko.Computed<boolean>;
|
||||
public isLinkInjectionEnabled: ko.Computed<boolean>;
|
||||
public isSettingsV2Enabled: ko.Computed<boolean>;
|
||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
|
||||
@@ -423,7 +420,6 @@ export default class Explorer {
|
||||
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
|
||||
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
||||
);
|
||||
this.isSettingsV2Enabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSettingsV2));
|
||||
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
||||
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||
@@ -525,7 +521,22 @@ export default class Explorer {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.isEnableMongoCapabilityPresent();
|
||||
const capabilities = this.databaseAccount().properties && this.databaseAccount().properties.capabilities;
|
||||
|
||||
if (!capabilities) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < capabilities.length; i++) {
|
||||
if (typeof capabilities[i] === "object") {
|
||||
if (capabilities[i].name === Constants.CapabilityNames.EnableMongo) {
|
||||
// version 3.6
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.isServerlessEnabled = ko.computed(
|
||||
@@ -557,21 +568,6 @@ export default class Explorer {
|
||||
return false;
|
||||
});
|
||||
|
||||
this.isEnableMongoCapabilityPresent = ko.computed(() => {
|
||||
const capabilities = this.databaseAccount && this.databaseAccount()?.properties?.capabilities;
|
||||
if (!capabilities) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < capabilities.length; i++) {
|
||||
if (typeof capabilities[i] === "object" && capabilities[i].name === Constants.CapabilityNames.EnableMongo) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.isHostedDataExplorerEnabled = ko.computed<boolean>(
|
||||
() =>
|
||||
this.getPlatformType() === PlatformType.Portal &&
|
||||
@@ -1427,40 +1423,71 @@ export default class Explorer {
|
||||
|
||||
// TODO: Refactor
|
||||
const deferred: Q.Deferred<any> = Q.defer();
|
||||
this._setLoadingStatusText("Fetching databases...");
|
||||
readDatabases().then(
|
||||
(databases: DataModels.Database[]) => {
|
||||
this._setLoadingStatusText("Successfully fetched databases.");
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.LoadDatabases,
|
||||
{
|
||||
databaseAccountName: this.databaseAccount().name,
|
||||
defaultExperience: this.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ResourceTree
|
||||
},
|
||||
startKey
|
||||
);
|
||||
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
|
||||
const deltaDatabases = this.getDeltaDatabases(databases);
|
||||
this.addDatabasesToList(deltaDatabases.toAdd);
|
||||
this.deleteDatabasesFromList(deltaDatabases.toDelete);
|
||||
this.selectedNode(currentlySelectedNode);
|
||||
this._setLoadingStatusText("Fetching containers...");
|
||||
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd)
|
||||
.then(
|
||||
() => {
|
||||
this._setLoadingStatusText("Successfully fetched containers.");
|
||||
deferred.resolve();
|
||||
|
||||
const refreshDatabases = (offers?: DataModels.Offer[]) => {
|
||||
this._setLoadingStatusText("Fetching databases...");
|
||||
readDatabases().then(
|
||||
(databases: DataModels.Database[]) => {
|
||||
this._setLoadingStatusText("Successfully fetched databases.");
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.LoadDatabases,
|
||||
{
|
||||
databaseAccountName: this.databaseAccount().name,
|
||||
defaultExperience: this.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ResourceTree
|
||||
},
|
||||
reason => {
|
||||
this._setLoadingStatusText("Failed to fetch containers.");
|
||||
deferred.reject(reason);
|
||||
}
|
||||
)
|
||||
.finally(() => this.isRefreshingExplorer(false));
|
||||
startKey
|
||||
);
|
||||
const currentlySelectedNode: ViewModels.TreeNode = this.selectedNode();
|
||||
const deltaDatabases = this.getDeltaDatabases(databases, offers);
|
||||
this.addDatabasesToList(deltaDatabases.toAdd);
|
||||
this.deleteDatabasesFromList(deltaDatabases.toDelete);
|
||||
this.selectedNode(currentlySelectedNode);
|
||||
this._setLoadingStatusText("Fetching containers...");
|
||||
this.refreshAndExpandNewDatabases(deltaDatabases.toAdd)
|
||||
.then(
|
||||
() => {
|
||||
this._setLoadingStatusText("Successfully fetched containers.");
|
||||
deferred.resolve();
|
||||
},
|
||||
reason => {
|
||||
this._setLoadingStatusText("Failed to fetch containers.");
|
||||
deferred.reject(reason);
|
||||
}
|
||||
)
|
||||
.finally(() => this.isRefreshingExplorer(false));
|
||||
},
|
||||
error => {
|
||||
this._setLoadingStatusText("Failed to fetch databases.");
|
||||
this.isRefreshingExplorer(false);
|
||||
deferred.reject(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.LoadDatabases,
|
||||
{
|
||||
databaseAccountName: this.databaseAccount().name,
|
||||
defaultExperience: this.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
error: JSON.stringify(error)
|
||||
},
|
||||
startKey
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while refreshing databases: ${JSON.stringify(error)}`
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers({ isServerless: this.isServerlessEnabled() });
|
||||
this._setLoadingStatusText("Fetching offers...");
|
||||
offerPromise.then(
|
||||
(offers: DataModels.Offer[]) => {
|
||||
this._setLoadingStatusText("Successfully fetched offers.");
|
||||
refreshDatabases(offers);
|
||||
},
|
||||
error => {
|
||||
this._setLoadingStatusText("Failed to fetch databases.");
|
||||
this._setLoadingStatusText("Failed to fetch offers.");
|
||||
this.isRefreshingExplorer(false);
|
||||
deferred.reject(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
@@ -1866,9 +1893,6 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public findSelectedDatabase(): ViewModels.Database {
|
||||
if (!this.selectedNode()) {
|
||||
return null;
|
||||
}
|
||||
if (this.selectedNode().nodeKind === "Database") {
|
||||
return _.find(this.databases(), (database: ViewModels.Database) => database.rid === this.selectedNode().rid);
|
||||
}
|
||||
@@ -2075,13 +2099,16 @@ export default class Explorer {
|
||||
defaultExperience: this.defaultExperience && this.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ResourceTree
|
||||
});
|
||||
databasesToLoad.forEach(async (database: ViewModels.Database) => {
|
||||
await database.loadCollections();
|
||||
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.rid === database.rid);
|
||||
if (isNewDatabase) {
|
||||
database.expandDatabase();
|
||||
}
|
||||
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
|
||||
databasesToLoad.forEach((database: ViewModels.Database) => {
|
||||
loadCollectionPromises.push(
|
||||
database.loadCollections().finally(() => {
|
||||
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.rid === database.rid);
|
||||
if (isNewDatabase) {
|
||||
database.expandDatabase();
|
||||
}
|
||||
this.tabsManager.refreshActiveTab(tab => tab.collection && tab.collection.getDatabase().rid === database.rid);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
Q.all(loadCollectionPromises).done(
|
||||
@@ -2226,7 +2253,8 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
private getDeltaDatabases(
|
||||
updatedDatabaseList: DataModels.Database[]
|
||||
updatedDatabaseList: DataModels.Database[],
|
||||
updatedOffersList: DataModels.Offer[]
|
||||
): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[] } {
|
||||
const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => {
|
||||
const databaseExists = _.some(
|
||||
@@ -2235,9 +2263,10 @@ export default class Explorer {
|
||||
);
|
||||
return !databaseExists;
|
||||
});
|
||||
const databasesToAdd: ViewModels.Database[] = newDatabases.map(
|
||||
(newDatabase: DataModels.Database) => new Database(this, newDatabase)
|
||||
);
|
||||
const databasesToAdd: ViewModels.Database[] = _.map(newDatabases, (newDatabase: DataModels.Database) => {
|
||||
const databaseOffer: DataModels.Offer = this.getOfferForResource(updatedOffersList, newDatabase._self);
|
||||
return new Database(this, newDatabase, databaseOffer);
|
||||
});
|
||||
|
||||
let databasesToDelete: ViewModels.Database[] = [];
|
||||
ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => {
|
||||
@@ -2287,6 +2316,10 @@ export default class Explorer {
|
||||
return null;
|
||||
}
|
||||
|
||||
private getOfferForResource(offers: DataModels.Offer[], resourceId: string): DataModels.Offer {
|
||||
return _.find(offers, (offer: DataModels.Offer) => offer.resource === resourceId);
|
||||
}
|
||||
|
||||
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to upload notebook, but notebook is not enabled";
|
||||
@@ -2585,11 +2618,9 @@ export default class Explorer {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Downloading ${notebookFile.path}`);
|
||||
|
||||
return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then(
|
||||
(content: string) => {
|
||||
const blob = stringToBlob(content, "text/plain");
|
||||
const blob = new Blob([content], { type: "octet/stream" });
|
||||
if (navigator.msSaveBlob) {
|
||||
// for IE and Edge
|
||||
navigator.msSaveBlob(blob, notebookFile.name);
|
||||
@@ -2606,16 +2637,12 @@ export default class Explorer {
|
||||
downloadLink.click();
|
||||
downloadLink.remove();
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
},
|
||||
(error: any) => {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Could not download notebook ${JSON.stringify(error)}`
|
||||
);
|
||||
|
||||
clearMessage();
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -3090,6 +3117,12 @@ export default class Explorer {
|
||||
} else {
|
||||
loadingTitle.innerHTML = title;
|
||||
}
|
||||
|
||||
TelemetryProcessor.trace(
|
||||
Action.LoadingStatus,
|
||||
ActionModifiers.Mark,
|
||||
title !== "Welcome to Azure Cosmos DB" ? `Title: ${title}, Text: ${text}` : text
|
||||
);
|
||||
}
|
||||
|
||||
private _openSetupNotebooksPaneForQuickstart(): void {
|
||||
@@ -3123,15 +3156,4 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async loadSelectedDatabaseOffer(): Promise<void> {
|
||||
const database = this.findSelectedDatabase();
|
||||
await database?.loadOffer();
|
||||
}
|
||||
|
||||
public async loadDatabaseOffers(): Promise<void> {
|
||||
this.databases()?.forEach(async (database: ViewModels.Database) => {
|
||||
await database.loadOffer();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { GraphConfig } from "../../Tabs/GraphTab";
|
||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||
import LoadGraphIcon from "../../../../images/LoadGraph.png";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { InputProperty } from "../../../Contracts/ViewModels";
|
||||
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { PlatformType } from "../../../PlatformType";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { Areas } from "../../../Common/Constants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
|
||||
import AddCollectionIcon from "../../../../images/AddCollection.svg";
|
||||
@@ -391,6 +391,31 @@ export class CommandBarComponentButtonFactory {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private static createScaleAndSettingsButton(container: Explorer): CommandButtonComponentProps {
|
||||
let isShared = false;
|
||||
if (container.isDatabaseNodeSelected()) {
|
||||
isShared = container.findSelectedDatabase().isDatabaseShared();
|
||||
} else if (container.isNodeKindSelected("Collection")) {
|
||||
const database: ViewModels.Database = container.findSelectedCollection().getDatabase();
|
||||
isShared = database && database.isDatabaseShared();
|
||||
}
|
||||
|
||||
const label = isShared ? "Settings" : "Scale & Settings";
|
||||
|
||||
return {
|
||||
iconSrc: ScaleIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||
selectedCollection && (<any>selectedCollection).onSettingsClick();
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected()
|
||||
};
|
||||
}
|
||||
|
||||
private static createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "New Notebook";
|
||||
return {
|
||||
|
||||
@@ -130,14 +130,11 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
<span className="headerStatusEllipsis">{this.state.headerStatus}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="expandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={this.state.isExpanded ? "collapse console" : "expand console"}
|
||||
aria-expanded={this.state.isExpanded}
|
||||
>
|
||||
<img src={this.state.isExpanded ? ChevronDownIcon : ChevronUpIcon} alt="" />
|
||||
<div className="expandCollapseButton" role="button" tabIndex={0}>
|
||||
<img
|
||||
src={this.state.isExpanded ? ChevronDownIcon : ChevronUpIcon}
|
||||
alt={this.state.isExpanded ? "collapse console" : "expand console"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AnimateHeight
|
||||
|
||||
@@ -68,14 +68,12 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-expanded={true}
|
||||
aria-label="collapse console"
|
||||
className="expandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
alt="collapse console"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
464
src/Explorer/Notebook/MonacoEditor/MonacoEditor.tsx
Normal file
464
src/Explorer/Notebook/MonacoEditor/MonacoEditor.tsx
Normal file
@@ -0,0 +1,464 @@
|
||||
import { Channels } from "@nteract/messaging";
|
||||
import * as monaco from "./monaco";
|
||||
import * as React from "react";
|
||||
import { completionProvider } from "./completions/completionItemProvider";
|
||||
import { AppState, ContentRef } from "@nteract/core";
|
||||
import { connect } from "react-redux";
|
||||
import "./styles.css";
|
||||
import { LightThemeName, HCLightThemeName, DarkThemeName } from "./theme";
|
||||
// import { logger } from "src/common/localLogger";
|
||||
import { getCellMonacoLanguage } from "./selectors";
|
||||
// import { DocumentUri } from "./documentUri";
|
||||
|
||||
export type IModelContentChangedEvent = monaco.editor.IModelContentChangedEvent;
|
||||
|
||||
/**
|
||||
* Initial props for Monaco received from agnostic component
|
||||
*/
|
||||
export interface IMonacoProps {
|
||||
id: string;
|
||||
contentRef: ContentRef;
|
||||
modelUri?: monaco.Uri;
|
||||
theme: monaco.editor.IStandaloneThemeData | monaco.editor.BuiltinTheme | string;
|
||||
cellLanguageOverride?: string;
|
||||
notebookLanguageOverride?: string;
|
||||
readOnly?: boolean;
|
||||
channels: Channels | undefined;
|
||||
enableCompletion: boolean;
|
||||
shouldRegisterDefaultCompletion?: boolean;
|
||||
onChange: (value: string, event?: unknown) => void;
|
||||
onFocusChange: (focus: boolean) => void;
|
||||
onCursorPositionChange?: (selection: monaco.ISelection | null) => void;
|
||||
onRegisterCompletionProvider?: (languageId: string) => void;
|
||||
value: string;
|
||||
editorFocused: boolean;
|
||||
lineNumbers: boolean;
|
||||
|
||||
/** set height of editor to fit the specified number of lines in display */
|
||||
numberOfLines?: number;
|
||||
|
||||
options?: monaco.editor.IEditorOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monaco specific props derived from State
|
||||
*/
|
||||
interface IMonacoStateProps {
|
||||
language: string;
|
||||
}
|
||||
|
||||
// Cache the custom theme data to avoid repeatly defining the custom theme
|
||||
let customThemeData: monaco.editor.IStandaloneThemeData;
|
||||
|
||||
function getMonacoTheme(theme: monaco.editor.IStandaloneThemeData | monaco.editor.BuiltinTheme | string) {
|
||||
if (typeof theme === "string") {
|
||||
switch (theme) {
|
||||
case "vs-dark":
|
||||
return DarkThemeName;
|
||||
case "hc-black":
|
||||
return "hc-black";
|
||||
case "vs":
|
||||
return LightThemeName;
|
||||
case "hc-light":
|
||||
return HCLightThemeName;
|
||||
default:
|
||||
return LightThemeName;
|
||||
}
|
||||
} else if (theme === undefined || typeof theme === "undefined") {
|
||||
return LightThemeName;
|
||||
} else {
|
||||
const themeName = "custom-vs";
|
||||
|
||||
// Skip redefining the same custom theme if it is the same theme data.
|
||||
if (customThemeData !== theme) {
|
||||
monaco.editor.defineTheme(themeName, theme);
|
||||
customThemeData = theme;
|
||||
}
|
||||
|
||||
return themeName;
|
||||
}
|
||||
}
|
||||
|
||||
const makeMapStateToProps = (initialState: AppState, initialProps: IMonacoProps) => {
|
||||
const { id, contentRef } = initialProps;
|
||||
const mapStateToProps = (state: AppState, ownProps: IMonacoProps & IMonacoStateProps) => {
|
||||
return {
|
||||
language: getCellMonacoLanguage(
|
||||
state,
|
||||
contentRef,
|
||||
id,
|
||||
ownProps.cellLanguageOverride,
|
||||
ownProps.notebookLanguageOverride
|
||||
)
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a MonacoEditor instance within the MonacoContainer div
|
||||
*/
|
||||
export class MonacoEditor extends React.Component<IMonacoProps & IMonacoStateProps> {
|
||||
editor?: monaco.editor.IStandaloneCodeEditor;
|
||||
editorContainerRef = React.createRef<HTMLDivElement>();
|
||||
contentHeight?: number;
|
||||
private cursorPositionListener?: monaco.IDisposable;
|
||||
|
||||
constructor(props: IMonacoProps & IMonacoStateProps) {
|
||||
super(props);
|
||||
this.onFocus = this.onFocus.bind(this);
|
||||
this.onBlur = this.onBlur.bind(this);
|
||||
this.calculateHeight = this.calculateHeight.bind(this);
|
||||
}
|
||||
|
||||
onDidChangeModelContent(e: monaco.editor.IModelContentChangedEvent): void {
|
||||
if (this.editor) {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(this.editor.getValue(), e);
|
||||
}
|
||||
|
||||
this.calculateHeight();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the height of editor
|
||||
*
|
||||
* @remarks
|
||||
* The way to determine how many lines we should display in editor:
|
||||
* If numberOfLines is not set or set to 0, we adjust the height to fit the content
|
||||
* If numberOfLines is specified we respect that setting
|
||||
*/
|
||||
calculateHeight(): void {
|
||||
// Make sure we have an editor
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we have a model
|
||||
const model = this.editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.editorContainerRef && this.editorContainerRef.current) {
|
||||
const expectedLines = this.props.numberOfLines || model.getLineCount();
|
||||
// The find & replace menu takes up 2 lines, that is why 2 line is set as the minimum number of lines
|
||||
// TODO: we should either disable the find/replace menu or auto expand the editor when find/replace is triggerred.
|
||||
const finalizedLines = Math.max(expectedLines, 1) + 1;
|
||||
const lineHeight = this.editor.getConfiguration().lineHeight;
|
||||
|
||||
const contentHeight = finalizedLines * lineHeight;
|
||||
if (this.contentHeight !== contentHeight) {
|
||||
this.editorContainerRef.current.style.height = contentHeight + "px";
|
||||
this.editor.layout();
|
||||
this.contentHeight = contentHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
if (this.editorContainerRef && this.editorContainerRef.current) {
|
||||
// Register Jupyter completion provider if needed
|
||||
this.registerCompletionProvider();
|
||||
|
||||
// Use Monaco model uri if provided. Otherwise, create a new model uri using editor id.
|
||||
const uri = this.props.modelUri ? this.props.modelUri : monaco.Uri.file(this.props.id);
|
||||
|
||||
// Only create a new model if it does not exist. For example, when we double click on a markdown cell,
|
||||
// an editor model is created for it. Once we go back to markdown preview mode that doesn't use the editor,
|
||||
// double clicking on the markdown cell will again instantiate a monaco editor. In that case, we should
|
||||
// rebind the previously created editor model for the markdown instead of recreating one. Monaco does not
|
||||
// allow models to be recreated with the same uri.
|
||||
let model = monaco.editor.getModel(uri);
|
||||
if (!model) {
|
||||
model = monaco.editor.createModel(this.props.value, this.props.language, uri);
|
||||
}
|
||||
|
||||
// Create Monaco editor backed by a Monaco model.
|
||||
this.editor = monaco.editor.create(this.editorContainerRef.current, {
|
||||
// Following are the default settings
|
||||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
autoIndent: true,
|
||||
overviewRulerLanes: 1,
|
||||
scrollbar: {
|
||||
useShadows: false,
|
||||
verticalHasArrows: false,
|
||||
horizontalHasArrows: false,
|
||||
vertical: "hidden",
|
||||
horizontal: "hidden",
|
||||
verticalScrollbarSize: 0,
|
||||
horizontalScrollbarSize: 0,
|
||||
arrowSize: 30
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
find: {
|
||||
// TODO Need this?
|
||||
// addExtraSpaceOnTop: false, // pops the editor out of alignment if turned on
|
||||
seedSearchStringFromSelection: true, // default is true
|
||||
autoFindInSelection: false // default is false
|
||||
},
|
||||
// Disable highlight current line, too much visual noise with it on.
|
||||
// VS Code also has it disabled for their notebook experience.
|
||||
renderLineHighlight: "none",
|
||||
|
||||
// Allow editor pop up widgets such as context menus, signature help, hover tips to be able to be
|
||||
// displayed outside of the editor. Without this, the pop up widgets can be clipped.
|
||||
fixedOverflowWidgets: true,
|
||||
|
||||
// Apply custom settings from configuration
|
||||
...this.props.options,
|
||||
|
||||
// Apply specific settings passed-in as direct props
|
||||
model,
|
||||
value: this.props.value,
|
||||
language: this.props.language,
|
||||
readOnly: this.props.readOnly,
|
||||
lineNumbers: this.props.lineNumbers ? "on" : "off",
|
||||
theme: getMonacoTheme(this.props.theme)
|
||||
});
|
||||
|
||||
this.addEditorTopMargin();
|
||||
|
||||
// Ignore Ctrl + Enter
|
||||
// tslint:disable-next-line no-bitwise
|
||||
this.editor.addCommand(
|
||||
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
|
||||
() => {
|
||||
// Do nothing. This is handled elsewhere, we just don't want the editor to put the newline.
|
||||
},
|
||||
undefined
|
||||
);
|
||||
// TODO Add right context
|
||||
|
||||
this.toggleEditorOptions(this.props.editorFocused);
|
||||
|
||||
if (this.props.editorFocused) {
|
||||
if (!this.editor.hasTextFocus()) {
|
||||
// Bring browser focus to the editor if text not already in focus
|
||||
this.editor.focus();
|
||||
}
|
||||
this.registerCursorListener();
|
||||
}
|
||||
|
||||
// TODO: Need to remove the event listener when the editor is disposed, or we have a memory leak here.
|
||||
// The same applies to the other event listeners below
|
||||
// Adds listener under the resize window event which calls the resize method
|
||||
window.addEventListener("resize", this.resize.bind(this));
|
||||
|
||||
// Adds listeners for undo and redo actions emitted from the toolbar
|
||||
this.editorContainerRef.current.addEventListener("undo", () => {
|
||||
if (this.editor) {
|
||||
this.editor.trigger("undo-event", "undo", {});
|
||||
}
|
||||
});
|
||||
this.editorContainerRef.current.addEventListener("redo", () => {
|
||||
if (this.editor) {
|
||||
this.editor.trigger("redo-event", "redo", {});
|
||||
}
|
||||
});
|
||||
|
||||
this.editor.onDidChangeModelContent(this.onDidChangeModelContent.bind(this));
|
||||
this.editor.onDidFocusEditorText(this.onFocus);
|
||||
this.editor.onDidBlurEditorText(this.onBlur);
|
||||
this.calculateHeight();
|
||||
|
||||
// FIXME: This might need further investigation as the props value should be respected in construction
|
||||
// The following is a mitigation measure till that time
|
||||
// Ensures that the source contents of the editor (value) is consistent with the state of the editor
|
||||
this.editor.setValue(this.props.value);
|
||||
}
|
||||
}
|
||||
|
||||
addEditorTopMargin(): void {
|
||||
if (this.editor) {
|
||||
// Monaco editor doesn't have margins
|
||||
// https://github.com/notable/notable/issues/551
|
||||
// This is a workaround to add an editor area 12px padding at the top
|
||||
// so that cursors rendered by collab decorators could be visible without being cut.
|
||||
this.editor.changeViewZones(changeAccessor => {
|
||||
const domNode = document.createElement("div");
|
||||
changeAccessor.addZone({
|
||||
afterLineNumber: 0,
|
||||
heightInPx: 12,
|
||||
domNode
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells editor to check the surrounding container size and resize itself appropriately
|
||||
*/
|
||||
resize(): void {
|
||||
if (this.editor && this.props.editorFocused) {
|
||||
this.editor.layout();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { value, channels, /* language, contentRef, id,*/ editorFocused, theme } = this.props;
|
||||
|
||||
// Ensures that the source contents of the editor (value) is consistent with the state of the editor
|
||||
if (this.editor.getValue() !== value) {
|
||||
this.editor.setValue(value);
|
||||
}
|
||||
|
||||
completionProvider.setChannels(channels);
|
||||
|
||||
// Register Jupyter completion provider if needed
|
||||
this.registerCompletionProvider();
|
||||
|
||||
/*
|
||||
// Apply new model to the editor when the language is changed.
|
||||
const model = this.editor.getModel();
|
||||
if (model && language && model.getModeId() !== language) {
|
||||
const newUri = DocumentUri.createCellUri(contentRef, id, language);
|
||||
if (!monaco.editor.getModel(newUri)) {
|
||||
// Save the cursor position before we set new model.
|
||||
const position = this.editor.getPosition();
|
||||
|
||||
// Set new model targeting the changed language.
|
||||
this.editor.setModel(monaco.editor.createModel(value, language, newUri));
|
||||
this.addEditorTopMargin();
|
||||
|
||||
// Restore cursor position to new model.
|
||||
if (position) {
|
||||
this.editor.setPosition(position);
|
||||
}
|
||||
|
||||
// Dispose of the old model in a seperate event. We cannot dispose of the model within the
|
||||
// componentDidUpdate method or else the editor will throw an exception. Zero in the timeout field
|
||||
// means execute immediately but in a seperate next event.
|
||||
setTimeout(() => model.dispose(), 0);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
if (theme) {
|
||||
monaco.editor.setTheme(getMonacoTheme(theme));
|
||||
}
|
||||
|
||||
// In the multi-tabs scenario, when the notebook is hidden by setting "display:none",
|
||||
// Any state update propagated here would cause a UI re-layout, monaco-editor will then recalculate
|
||||
// and set its height to 5px.
|
||||
// To work around that issue, we skip updating the UI when parent element's offsetParent is null (which
|
||||
// indicate an ancient element is hidden by display set to none)
|
||||
// We may revisit this when we get to refactor for multi-notebooks.
|
||||
if (!this.editorContainerRef.current?.offsetParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set focus
|
||||
if (editorFocused && !this.editor.hasTextFocus()) {
|
||||
this.editor.focus();
|
||||
}
|
||||
|
||||
// Tells the editor pane to check if its container has changed size and fill appropriately
|
||||
this.editor.layout();
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
if (this.editor) {
|
||||
try {
|
||||
const model = this.editor.getModel();
|
||||
if (model) {
|
||||
model.dispose();
|
||||
}
|
||||
|
||||
this.editor.dispose();
|
||||
} catch (err) {
|
||||
console.error(`Error occurs in disposing editor: ${JSON.stringify(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<div className="monaco-container">
|
||||
<div ref={this.editorContainerRef} id={`editor-${this.props.id}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register default kernel-based completion provider.
|
||||
* @param language Language
|
||||
*/
|
||||
registerDefaultCompletionProvider(language: string): void {
|
||||
// onLanguage event is emitted only once per language when language is first time needed.
|
||||
monaco.languages.onLanguage(language, () => {
|
||||
monaco.languages.registerCompletionItemProvider(language, completionProvider);
|
||||
});
|
||||
}
|
||||
|
||||
private onFocus() {
|
||||
this.props.onFocusChange(true);
|
||||
this.toggleEditorOptions(true);
|
||||
this.registerCursorListener();
|
||||
}
|
||||
|
||||
private onBlur() {
|
||||
this.props.onFocusChange(false);
|
||||
this.toggleEditorOptions(false);
|
||||
this.unregisterCursorListener();
|
||||
}
|
||||
|
||||
private registerCursorListener() {
|
||||
if (this.editor && this.props.onCursorPositionChange) {
|
||||
const selection = this.editor.getSelection();
|
||||
this.props.onCursorPositionChange(selection);
|
||||
|
||||
if (!this.cursorPositionListener) {
|
||||
this.cursorPositionListener = this.editor.onDidChangeCursorSelection(event =>
|
||||
this.props.onCursorPositionChange!(event.selection)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unregisterCursorListener() {
|
||||
if (this.cursorPositionListener) {
|
||||
this.cursorPositionListener.dispose();
|
||||
this.cursorPositionListener = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle editor options based on if the editor is in active state (i.e. focused).
|
||||
* When the editor is not active, we want to deactivate some of the visual noise.
|
||||
* @param isActive Whether editor is active.
|
||||
*/
|
||||
private toggleEditorOptions(isActive: boolean) {
|
||||
if (this.editor) {
|
||||
this.editor.updateOptions({
|
||||
matchBrackets: isActive,
|
||||
occurrencesHighlight: isActive,
|
||||
renderIndentGuides: isActive
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register language features for target language. Call before setting language type to model.
|
||||
*/
|
||||
private registerCompletionProvider() {
|
||||
const { enableCompletion, language, onRegisterCompletionProvider, shouldRegisterDefaultCompletion } = this.props;
|
||||
|
||||
if (enableCompletion && language) {
|
||||
if (onRegisterCompletionProvider) {
|
||||
onRegisterCompletionProvider(language);
|
||||
} else if (shouldRegisterDefaultCompletion) {
|
||||
this.registerDefaultCompletionProvider(language);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default connect<IMonacoStateProps, void, IMonacoProps, AppState>(makeMapStateToProps)(MonacoEditor);
|
||||
@@ -0,0 +1,239 @@
|
||||
import * as monaco from "monaco-editor/esm/vs/editor/editor.api";
|
||||
// import * as monaco from "../monaco";
|
||||
import { Observable, Observer } from "rxjs";
|
||||
import { first, map } from "rxjs/operators";
|
||||
import { childOf, JupyterMessage, ofMessageType, Channels } from "@nteract/messaging";
|
||||
|
||||
/**
|
||||
* TODO: import from nteract when the changes under editor-base.ts are ported to nteract.
|
||||
*/
|
||||
import { CompletionResults, CompletionMatch, completionRequest, js_idx_to_char_idx } from "../editor-base";
|
||||
|
||||
/**
|
||||
* Jupyter to Monaco completion item kinds.
|
||||
*/
|
||||
const unknownJupyterKind = "<unknown>";
|
||||
const jupyterToMonacoCompletionItemKind = {
|
||||
[unknownJupyterKind]: monaco.languages.CompletionItemKind.Field,
|
||||
class: monaco.languages.CompletionItemKind.Class,
|
||||
function: monaco.languages.CompletionItemKind.Function,
|
||||
keyword: monaco.languages.CompletionItemKind.Keyword,
|
||||
instance: monaco.languages.CompletionItemKind.Variable,
|
||||
statement: monaco.languages.CompletionItemKind.Variable
|
||||
};
|
||||
|
||||
/**
|
||||
* Completion item provider.
|
||||
*/
|
||||
class CompletionItemProvider implements monaco.languages.CompletionItemProvider {
|
||||
private channels: Channels | undefined;
|
||||
|
||||
/**
|
||||
* Set Channels of Jupyter kernel.
|
||||
* @param channels Channels of Jupyter kernel.
|
||||
*/
|
||||
setChannels(channels: Channels | undefined) {
|
||||
this.channels = channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether provider is connected to Jupyter kernel.
|
||||
*/
|
||||
get isConnectedToKernel() {
|
||||
return !!this.channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional characters to trigger completion other than Ctrl+Space.
|
||||
*/
|
||||
get triggerCharacters() {
|
||||
return [" ", "<", "/", ".", "="];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of completion items at position of cursor.
|
||||
* @param model Monaco editor text model.
|
||||
* @param position Position of cursor.
|
||||
*/
|
||||
async provideCompletionItems(model: monaco.editor.ITextModel, position: monaco.Position) {
|
||||
// Convert to zero-based index
|
||||
let cursorPos = model.getOffsetAt(position);
|
||||
const code = model.getValue();
|
||||
cursorPos = js_idx_to_char_idx(cursorPos, code);
|
||||
|
||||
// Get completions from Jupyter kernel if its Channels is connected
|
||||
let items = [];
|
||||
if (this.channels) {
|
||||
try {
|
||||
const message = completionRequest(code, cursorPos);
|
||||
items = await this.codeCompleteObservable(this.channels, message, model).toPromise();
|
||||
} catch (error) {
|
||||
// Temporary log error to console until we settle on how we log in V3
|
||||
// tslint:disable-next-line
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve<monaco.languages.CompletionList>({
|
||||
suggestions: items,
|
||||
incomplete: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of completion items from Jupyter kernel.
|
||||
* @param channels Channels of Jupyter kernel.
|
||||
* @param message Jupyter message for completion request.
|
||||
* @param model Text model.
|
||||
*/
|
||||
private codeCompleteObservable(channels: Channels, message: JupyterMessage, model: monaco.editor.ITextModel) {
|
||||
// Process completion response
|
||||
const completion$ = channels.pipe(
|
||||
childOf(message),
|
||||
ofMessageType("complete_reply"),
|
||||
map(entry => entry.content),
|
||||
first(),
|
||||
map(results => this.adaptToMonacoCompletions(results, model))
|
||||
);
|
||||
|
||||
// Subscribe and send completion request message
|
||||
return Observable.create((observer: Observer<unknown>) => {
|
||||
const subscription = completion$.subscribe(observer);
|
||||
channels.next(message);
|
||||
return subscription;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Jupyter completion result to list of Monaco completion items.
|
||||
*/
|
||||
private adaptToMonacoCompletions(results: CompletionResults, model: monaco.editor.ITextModel) {
|
||||
let range: monaco.IRange;
|
||||
let percentCount = 0;
|
||||
let matches = results ? results.matches : [];
|
||||
if (results.metadata && results.metadata._jupyter_types_experimental) {
|
||||
matches = results.metadata._jupyter_types_experimental as CompletionMatch[];
|
||||
}
|
||||
return matches.map((match: CompletionMatch, index: number) => {
|
||||
if (typeof match === "string") {
|
||||
const text = this.sanitizeText(match);
|
||||
const filtered = this.getFilterText(text);
|
||||
return {
|
||||
kind: this.adaptToMonacoCompletionItemKind(unknownJupyterKind),
|
||||
label: text,
|
||||
insertText: text,
|
||||
filterText: filtered,
|
||||
sortText: this.getSortText(index)
|
||||
} as monaco.languages.CompletionItem;
|
||||
} else {
|
||||
// We only need to get the range once as the range is the same for all completion items in the list.
|
||||
if (!range) {
|
||||
const start = model.getPositionAt(match.start);
|
||||
const end = model.getPositionAt(match.end);
|
||||
range = {
|
||||
startLineNumber: start.lineNumber,
|
||||
startColumn: start.column,
|
||||
endLineNumber: end.lineNumber,
|
||||
endColumn: end.column
|
||||
};
|
||||
|
||||
// Get the range representing the text before the completion action was invoked.
|
||||
// If the text starts with magics % indicator, we need to track how many of these indicators exist
|
||||
// so that we ensure the insertion text only inserts the delta between what the user typed versus
|
||||
// what is recommended by the completion. Without this, there will be extra % insertions.
|
||||
// Example:
|
||||
// User types %%p then suggestion list will recommend %%python, if we now commit the item then the
|
||||
// final text in the editor becomes %%p%%python instead of %%python. This is why the tracking code
|
||||
// below is needed. This behavior is only specific to the magics % indicators as Monaco does not
|
||||
// handle % characters in their completion list well.
|
||||
const rangeText = model.getValueInRange(range);
|
||||
if (rangeText.startsWith("%%")) {
|
||||
percentCount = 2;
|
||||
} else if (rangeText.startsWith("%")) {
|
||||
percentCount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const text = this.sanitizeText(match.text);
|
||||
const filtered = this.getFilterText(text);
|
||||
const insert = this.getInsertText(text, percentCount);
|
||||
return {
|
||||
kind: this.adaptToMonacoCompletionItemKind(match.type as keyof typeof jupyterToMonacoCompletionItemKind),
|
||||
label: text,
|
||||
insertText: percentCount > 0 ? insert : text,
|
||||
filterText: filtered,
|
||||
sortText: this.getSortText(index)
|
||||
} as monaco.languages.CompletionItem;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Jupyter completion item kind to Monaco completion item kind.
|
||||
* @param kind Jupyter completion item kind.
|
||||
*/
|
||||
private adaptToMonacoCompletionItemKind(kind: keyof typeof jupyterToMonacoCompletionItemKind) {
|
||||
const result = jupyterToMonacoCompletionItemKind[kind];
|
||||
return result ? result : jupyterToMonacoCompletionItemKind[unknownJupyterKind];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove everything before a dot. Jupyter completion results like to include all characters before
|
||||
* the trigger character. For example, if user types "myarray.", we expect the completion results to
|
||||
* show "append", "pop", etc. but for the actual case, it will show "myarray.append", "myarray.pop",
|
||||
* etc. so we are going to sanitize the text.
|
||||
* @param text Text of Jupyter completion item
|
||||
*/
|
||||
private sanitizeText(text: string) {
|
||||
const index = text.lastIndexOf(".");
|
||||
return index > -1 && index < text.length - 1 ? text.substring(index + 1) : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove magics all % characters as Monaco doesn't like them for the filtering text.
|
||||
* Without this, completion won't show magics match items.
|
||||
* @param text Text of Jupyter completion item.
|
||||
*/
|
||||
private getFilterText(text: string) {
|
||||
return text.replace(/%/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get insertion text handling what to insert for the magics case depending on what
|
||||
* has already been typed.
|
||||
* @param text Text of Jupyter completion item.
|
||||
* @param percentCount Number of percent characters to remove
|
||||
*/
|
||||
private getInsertText(text: string, percentCount: number) {
|
||||
for (let i = 0; i < percentCount; i++) {
|
||||
text = text.replace("%", "");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps numbers to strings, such that if a>b numerically, f(a)>f(b) lexicograhically.
|
||||
* 1 -> "za", 26 -> "zz", 27 -> "zza", 28 -> "zzb", 52 -> "zzz", 53 ->"zzza"
|
||||
* @param order Number to be converted to a sorting-string. order >= 0.
|
||||
* @returns A string representing the order.
|
||||
*/
|
||||
private getSortText(order: number): string {
|
||||
order++;
|
||||
const numCharacters = 26; // "z" - "a" + 1;
|
||||
const div = Math.floor(order / numCharacters);
|
||||
|
||||
let sortText = "z";
|
||||
for (let i = 0; i < div; i++) {
|
||||
sortText += "z";
|
||||
}
|
||||
|
||||
const remainder = order % numCharacters;
|
||||
if (remainder > 0) {
|
||||
sortText += String.fromCharCode(96 + remainder);
|
||||
}
|
||||
return sortText;
|
||||
}
|
||||
}
|
||||
|
||||
const completionProvider = new CompletionItemProvider();
|
||||
export { completionProvider };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user