Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/aisayas/default-throughput-bucket

This commit is contained in:
Asier Isayas
2026-03-31 07:44:59 -07:00
207 changed files with 29931 additions and 14445 deletions

View File

@@ -201,18 +201,18 @@ jobs:
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken) GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN" echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
# CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-cassandra.documents.azure.com/.default" -o tsv --query accessToken) CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-cassandra.documents.azure.com/.default" -o tsv --query accessToken)
# echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN" echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
# echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV
# MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo.documents.azure.com/.default" -o tsv --query accessToken) MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo.documents.azure.com/.default" -o tsv --query accessToken)
# echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN" echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
# echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV
# MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo32.documents.azure.com/.default" -o tsv --query accessToken) MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo32.documents.azure.com/.default" -o tsv --query accessToken)
# echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN" echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
# echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV
# MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken) MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken)
# echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN" echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
# echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
- name: List test files for shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}} - name: List test files for shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --list run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --list
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}} - name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}

View File

@@ -6,8 +6,8 @@ on:
# Allows you to run this workflow manually from the Actions tab # Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:
schedule: schedule:
# Once every two hours # Once every day at 7 AM PST
- cron: "0 */2 * * *" - cron: "0 13 * * *"
permissions: permissions:
id-token: write id-token: write

1
.gitignore vendored
View File

@@ -17,6 +17,7 @@ Contracts/*
failure.png failure.png
screenshots/* screenshots/*
GettingStarted-ignore*.ipynb GettingStarted-ignore*.ipynb
src/Localization/Keys.generated.ts
/test-results/ /test-results/
/playwright-report/ /playwright-report/
/blob-report/ /blob-report/

3
images/Pin.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.25 1.5C9.25 1.22386 9.47386 1 9.75 1H10.25C10.5261 1 10.75 1.22386 10.75 1.5V5.5L13 7.5V9H8.75V14L8 15L7.25 14V9H3V7.5L5.25 5.5V1.5C5.25 1.22386 5.47386 1 5.75 1H6.25C6.52614 1 6.75 1.22386 6.75 1.5V5.25H9.25V1.5Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 354 B

14319
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"description": "Cosmos Explorer", "description": "Cosmos Explorer",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "16.4.0",
"@azure/cosmos": "4.7.0", "@azure/cosmos": "4.7.0",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "4.5.0", "@azure/identity": "4.5.0",
@@ -14,12 +14,12 @@
"@fluentui/react": "8.119.0", "@fluentui/react": "8.119.0",
"@fluentui/react-components": "9.54.2", "@fluentui/react-components": "9.54.2",
"@jupyterlab/services": "6.0.2", "@jupyterlab/services": "6.0.2",
"@jupyterlab/terminal": "3.0.3", "@jupyterlab/terminal": "3.6.8",
"@microsoft/applicationinsights-web": "2.6.1", "@microsoft/applicationinsights-web": "2.6.1",
"@nteract/commutable": "7.5.1", "@nteract/commutable": "7.5.1",
"@nteract/connected-components": "6.8.2", "@nteract/connected-components": "6.8.2",
"@nteract/core": "15.1.9", "@nteract/core": "15.1.9",
"@nteract/data-explorer": "8.0.3", "@nteract/data-explorer": "8.2.12",
"@nteract/directory-listing": "2.0.6", "@nteract/directory-listing": "2.0.6",
"@nteract/dropdown-menu": "1.0.1", "@nteract/dropdown-menu": "1.0.1",
"@nteract/editor": "10.1.12", "@nteract/editor": "10.1.12",
@@ -31,7 +31,7 @@
"@nteract/monaco-editor": "3.2.2", "@nteract/monaco-editor": "3.2.2",
"@nteract/octicons": "2.0.0", "@nteract/octicons": "2.0.0",
"@nteract/outputs": "3.0.9", "@nteract/outputs": "3.0.9",
"@nteract/presentational-components": "3.0.7", "@nteract/presentational-components": "3.4.12",
"@nteract/stateful-components": "1.7.0", "@nteract/stateful-components": "1.7.0",
"@nteract/styles": "2.0.2", "@nteract/styles": "2.0.2",
"@nteract/transform-geojson": "5.1.8", "@nteract/transform-geojson": "5.1.8",
@@ -39,25 +39,26 @@
"@nteract/transform-plotly": "6.1.6", "@nteract/transform-plotly": "6.1.6",
"@nteract/transform-vdom": "4.0.11", "@nteract/transform-vdom": "4.0.11",
"@nteract/transform-vega": "7.0.6", "@nteract/transform-vega": "7.0.6",
"@octokit/request": "8.4.1",
"@octokit/rest": "17.9.2", "@octokit/rest": "17.9.2",
"@phosphor/widgets": "1.9.3", "@phosphor/widgets": "1.9.3",
"@testing-library/jest-dom": "6.4.6", "@testing-library/jest-dom": "6.4.6",
"@types/lodash": "4.14.171", "@types/lodash": "4.14.171",
"@types/mkdirp": "1.0.1", "@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7", "@types/node-fetch": "2.6.13",
"@xmldom/xmldom": "0.7.13", "@xmldom/xmldom": "0.7.13",
"@xterm/addon-fit": "0.10.0", "@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0", "@xterm/xterm": "5.5.0",
"allotment": "1.20.2", "allotment": "1.20.2",
"applicationinsights": "1.8.0", "applicationinsights": "1.8.0",
"bootstrap": "3.4.1", "bootstrap": "3.4.1",
"canvas": "2.11.2", "canvas": "3.2.1",
"clean-webpack-plugin": "4.0.0", "clean-webpack-plugin": "4.0.0",
"clipboard-copy": "4.0.1", "clipboard-copy": "4.0.1",
"copy-webpack-plugin": "11.0.0", "copy-webpack-plugin": "11.0.0",
"crossroads": "0.12.2", "crossroads": "0.12.2",
"css-element-queries": "1.1.1", "css-element-queries": "1.1.1",
"d3": "7.8.5", "d3": "7.9.0",
"datatables.net-colreorder-dt": "1.7.0", "datatables.net-colreorder-dt": "1.7.0",
"datatables.net-dt": "1.13.8", "datatables.net-dt": "1.13.8",
"date-fns": "1.29.0", "date-fns": "1.29.0",
@@ -70,7 +71,8 @@
"html2canvas": "1.0.0-rc.5", "html2canvas": "1.0.0-rc.5",
"i18next": "23.11.5", "i18next": "23.11.5",
"i18next-browser-languagedetector": "6.0.1", "i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "1.0.23", "i18next-http-backend": "3.0.2",
"i18next-resources-to-backend": "1.2.1",
"iframe-resizer-react": "1.1.0", "iframe-resizer-react": "1.1.0",
"immer": "9.0.6", "immer": "9.0.6",
"immutable": "4.0.0-rc.12", "immutable": "4.0.0-rc.12",
@@ -79,12 +81,15 @@
"jquery-typeahead": "2.11.1", "jquery-typeahead": "2.11.1",
"jquery-ui-dist": "1.13.2", "jquery-ui-dist": "1.13.2",
"knockout": "3.5.1", "knockout": "3.5.1",
"loader-utils": "2.0.3", "lodash": "4.17.23",
"lodash-es": "4.17.23",
"min-document": "2.19.1",
"mkdirp": "1.0.4", "mkdirp": "1.0.4",
"monaco-editor": "0.44.0", "monaco-editor": "0.44.0",
"ms": "2.1.3", "ms": "2.1.3",
"nanoid": "3.3.8",
"p-retry": "6.2.1", "p-retry": "6.2.1",
"patch-package": "8.0.0", "patch-package": "8.0.1",
"plotly.js-cartesian-dist-min": "1.52.3", "plotly.js-cartesian-dist-min": "1.52.3",
"post-robot": "10.0.42", "post-robot": "10.0.42",
"q": "1.5.1", "q": "1.5.1",
@@ -103,7 +108,6 @@
"react-youtube": "9.0.1", "react-youtube": "9.0.1",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12", "rx-jupyter": "5.5.12",
"sanitize-html": "2.3.3",
"shell-quote": "1.7.3", "shell-quote": "1.7.3",
"styled-components": "5.0.1", "styled-components": "5.0.1",
"swr": "0.4.0", "swr": "0.4.0",
@@ -111,16 +115,30 @@
"tinykeys": "2.1.0", "tinykeys": "2.1.0",
"underscore": "1.12.1", "underscore": "1.12.1",
"utility-types": "3.10.0", "utility-types": "3.10.0",
"web-vitals": "4.2.4",
"uuid": "9.0.0", "uuid": "9.0.0",
"web-vitals": "4.2.4",
"ws": "8.17.1",
"zustand": "3.5.0" "zustand": "3.5.0"
}, },
"overrides": {
"d3-color": "3.1.0",
"cross-spawn": "7.0.6",
"less-vars-loader": {
"json5": "1.0.2"
},
"trim": "0.0.3",
"@octokit/plugin-paginate-rest": "9.2.2",
"@octokit/request-error": "5.1.1",
"@octokit/request": "8.4.1",
"prismjs": "1.30.0",
"sanitize-html": "2.17.0"
},
"devDependencies": { "devDependencies": {
"@babel/core": "7.24.7", "@babel/core": "7.29.0",
"@babel/preset-env": "7.24.7", "@babel/preset-env": "7.24.7",
"@babel/preset-react": "7.24.7", "@babel/preset-react": "7.24.7",
"@babel/preset-typescript": "7.24.7", "@babel/preset-typescript": "7.24.7",
"@playwright/test": "1.49.1", "@playwright/test": "1.55.1",
"@testing-library/react": "11.2.3", "@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7", "@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56", "@types/codemirror": "0.0.56",
@@ -134,7 +152,7 @@
"@types/hasher": "0.0.31", "@types/hasher": "0.0.31",
"@types/jest": "29.5.12", "@types/jest": "29.5.12",
"@types/jquery": "3.5.29", "@types/jquery": "3.5.29",
"@types/node": "12.11.1", "@types/node": "18.19.0",
"@types/post-robot": "10.0.1", "@types/post-robot": "10.0.1",
"@types/q": "1.5.1", "@types/q": "1.5.1",
"@types/react": "17.0.44", "@types/react": "17.0.44",
@@ -145,7 +163,7 @@
"@types/react-window": "1.8.8", "@types/react-window": "1.8.8",
"@types/sanitize-html": "1.27.2", "@types/sanitize-html": "1.27.2",
"@types/sinon": "2.3.3", "@types/sinon": "2.3.3",
"@types/styled-components": "5.1.1", "@types/styled-components": "5.1.32",
"@types/underscore": "1.7.36", "@types/underscore": "1.7.36",
"@types/youtube-player": "5.5.6", "@types/youtube-player": "5.5.6",
"@typescript-eslint/eslint-plugin": "6.7.4", "@typescript-eslint/eslint-plugin": "6.7.4",
@@ -153,6 +171,7 @@
"@webpack-cli/serve": "2.0.5", "@webpack-cli/serve": "2.0.5",
"babel-jest": "29.7.0", "babel-jest": "29.7.0",
"babel-loader": "8.1.0", "babel-loader": "8.1.0",
"brace-expansion": "1.1.12",
"buffer": "5.1.0", "buffer": "5.1.0",
"case-sensitive-paths-webpack-plugin": "2.4.0", "case-sensitive-paths-webpack-plugin": "2.4.0",
"create-file-webpack": "1.0.2", "create-file-webpack": "1.0.2",
@@ -170,6 +189,7 @@
"html-inline-css-webpack-plugin": "1.11.2", "html-inline-css-webpack-plugin": "1.11.2",
"html-loader": "5.0.0", "html-loader": "5.0.0",
"html-webpack-plugin": "5.5.3", "html-webpack-plugin": "5.5.3",
"i18next-resources-for-ts": "2.0.0",
"jest": "29.7.0", "jest": "29.7.0",
"jest-canvas-mock": "2.5.2", "jest-canvas-mock": "2.5.2",
"jest-circus": "29.7.0", "jest-circus": "29.7.0",
@@ -177,7 +197,8 @@
"jest-html-loader": "1.0.0", "jest-html-loader": "1.0.0",
"jest-react-hooks-shallow": "1.5.1", "jest-react-hooks-shallow": "1.5.1",
"jest-trx-results-processor": "3.0.2", "jest-trx-results-processor": "3.0.2",
"less": "3.8.1", "js-yaml": "3.14.2",
"less": "4.5.1",
"less-loader": "11.1.3", "less-loader": "11.1.3",
"less-vars-loader": "1.1.0", "less-vars-loader": "1.1.0",
"mini-css-extract-plugin": "2.1.0", "mini-css-extract-plugin": "2.1.0",
@@ -195,14 +216,17 @@
"typedoc": "0.26.2", "typedoc": "0.26.2",
"typescript": "4.9.5", "typescript": "4.9.5",
"url-loader": "4.1.1", "url-loader": "4.1.1",
"wait-on": "4.0.2", "values-to-keys": "1.1.0",
"webpack": "5.88.2", "wait-on": "9.0.3",
"webpack-bundle-analyzer": "4.9.1", "webpack": "5.104.1",
"webpack-bundle-analyzer": "5.2.0",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.2" "webpack-dev-server": "5.2.3",
"ws": "8.17.1"
}, },
"scripts": { "scripts": {
"postinstall": "patch-package", "postinstall": "patch-package && npm run generate:i18n-keys",
"prestart": "npm run generate:i18n-keys",
"start": "webpack serve --mode development", "start": "webpack serve --mode development",
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build", "dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
"build:dataExplorer:ci": "npm run build:ci", "build:dataExplorer:ci": "npm run build:ci",
@@ -229,6 +253,7 @@
"strict:find": "node ./strict-null-checks/find.js", "strict:find": "node ./strict-null-checks/find.js",
"strict:add": "node ./strict-null-checks/auto-add.js", "strict:add": "node ./strict-null-checks/auto-add.js",
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks", "compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks",
"generate:i18n-keys": "node utils/generateI18nKeys.mjs",
"generateARMClients": "npx ts-node utils/armClientGenerator/generator.ts" "generateARMClients": "npx ts-node utils/armClientGenerator/generator.ts"
}, },
"repository": { "repository": {

View File

@@ -26,15 +26,6 @@ export default defineConfig({
}, },
projects: [ projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
launchOptions: {
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
},
},
},
{ {
name: "firefox", name: "firefox",
use: { use: {

View File

@@ -8,9 +8,10 @@
"name": "cosmos-explorer-preview", "name": "cosmos-explorer-preview",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"body-parser": "^1.20.3", "body-parser": "^2.2.2",
"express": "^4.21.2", "express": "^5.2.1",
"http-proxy-middleware": "^3.0.3", "follow-redirects": "^1.15.6",
"http-proxy-middleware": "^3.0.5",
"node": "^20.19.5", "node": "^20.19.5",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"path-to-regexp": "^0.1.12" "path-to-regexp": "^0.1.12"
@@ -18,8 +19,7 @@
}, },
"node_modules/@types/http-proxy": { "node_modules/@types/http-proxy": {
"version": "1.17.16", "version": "1.17.16",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", "license": "MIT",
"integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==",
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
} }
@@ -29,50 +29,40 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "2.0.0",
"license": "MIT", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
"dependencies": { "dependencies": {
"mime-types": "~2.1.34", "mime-types": "^3.0.0",
"negotiator": "0.6.3" "negotiator": "^1.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.3", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "^3.1.2",
"content-type": "~1.0.5", "content-type": "^1.0.5",
"debug": "2.6.9", "debug": "^4.4.3",
"depd": "2.0.0", "http-errors": "^2.0.0",
"destroy": "1.2.0", "iconv-lite": "^0.7.0",
"http-errors": "2.0.0", "on-finished": "^2.4.1",
"iconv-lite": "0.4.24", "qs": "^6.14.1",
"on-finished": "2.4.1", "raw-body": "^3.0.1",
"qs": "6.13.0", "type-is": "^2.0.1"
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.8", "node": ">=18"
"npm": "1.2.8000 || >= 1.4.16" },
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/body-parser/node_modules/debug": {
"version": "2.6.9",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/body-parser/node_modules/ms": {
"version": "2.0.0",
"license": "MIT"
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -83,8 +73,7 @@
}, },
"node_modules/call-bind-apply-helpers": { "node_modules/call-bind-apply-helpers": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "license": "MIT",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@@ -95,8 +84,7 @@
}, },
"node_modules/call-bound": { "node_modules/call-bound": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "license": "MIT",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0" "get-intrinsic": "^1.3.0"
@@ -109,13 +97,15 @@
} }
}, },
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "1.0.1",
"license": "MIT", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
"dependencies": { "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
"safe-buffer": "5.2.1"
},
"engines": { "engines": {
"node": ">= 0.6" "node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/content-type": { "node_modules/content-type": {
@@ -127,20 +117,22 @@
}, },
"node_modules/cookie": { "node_modules/cookie": {
"version": "0.7.1", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "license": "MIT",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/cookie-signature": { "node_modules/cookie-signature": {
"version": "1.0.6", "version": "1.2.2",
"license": "MIT" "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
"engines": {
"node": ">=6.6.0"
}
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "license": "MIT",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
}, },
@@ -160,18 +152,9 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/destroy": {
"version": "1.2.0",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "license": "MIT",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.1", "call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -195,24 +178,21 @@
}, },
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "license": "MIT",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es-errors": { "node_modules/es-errors": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "license": "MIT",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es-object-atoms": { "node_modules/es-object-atoms": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "license": "MIT",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0" "es-errors": "^1.3.0"
}, },
@@ -238,104 +218,77 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.21.2", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "^2.0.0",
"array-flatten": "1.1.1", "body-parser": "^2.2.1",
"body-parser": "1.20.3", "content-disposition": "^1.0.0",
"content-disposition": "0.5.4", "content-type": "^1.0.5",
"content-type": "~1.0.4", "cookie": "^0.7.1",
"cookie": "0.7.1", "cookie-signature": "^1.2.1",
"cookie-signature": "1.0.6", "debug": "^4.4.0",
"debug": "2.6.9", "depd": "^2.0.0",
"depd": "2.0.0", "encodeurl": "^2.0.0",
"encodeurl": "~2.0.0", "escape-html": "^1.0.3",
"escape-html": "~1.0.3", "etag": "^1.8.1",
"etag": "~1.8.1", "finalhandler": "^2.1.0",
"finalhandler": "1.3.1", "fresh": "^2.0.0",
"fresh": "0.5.2", "http-errors": "^2.0.0",
"http-errors": "2.0.0", "merge-descriptors": "^2.0.0",
"merge-descriptors": "1.0.3", "mime-types": "^3.0.0",
"methods": "~1.1.2", "on-finished": "^2.4.1",
"on-finished": "2.4.1", "once": "^1.4.0",
"parseurl": "~1.3.3", "parseurl": "^1.3.3",
"path-to-regexp": "0.1.12", "proxy-addr": "^2.0.7",
"proxy-addr": "~2.0.7", "qs": "^6.14.0",
"qs": "6.13.0", "range-parser": "^1.2.1",
"range-parser": "~1.2.1", "router": "^2.2.0",
"safe-buffer": "5.2.1", "send": "^1.1.0",
"send": "0.19.0", "serve-static": "^2.2.0",
"serve-static": "1.16.2", "statuses": "^2.0.1",
"setprototypeof": "1.2.0", "type-is": "^2.0.1",
"statuses": "2.0.1", "vary": "^1.1.2"
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}, },
"engines": { "engines": {
"node": ">= 0.10.0" "node": ">= 18"
}, },
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/express/node_modules/array-flatten": {
"version": "1.1.1",
"license": "MIT"
},
"node_modules/express/node_modules/debug": {
"version": "2.6.9",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/express/node_modules/ms": {
"version": "2.0.0",
"license": "MIT"
},
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "1.3.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "^4.4.0",
"encodeurl": "~2.0.0", "encodeurl": "^2.0.0",
"escape-html": "~1.0.3", "escape-html": "^1.0.3",
"on-finished": "2.4.1", "on-finished": "^2.4.1",
"parseurl": "~1.3.3", "parseurl": "^1.3.3",
"statuses": "2.0.1", "statuses": "^2.0.1"
"unpipe": "~1.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/finalhandler/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/finalhandler/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.3", "version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh" "url": "https://github.com/sponsors/RubenVerborgh"
} }
], ],
"license": "MIT",
"engines": { "engines": {
"node": ">=4.0" "node": ">=4.0"
}, },
@@ -353,25 +306,23 @@
} }
}, },
"node_modules/fresh": { "node_modules/fresh": {
"version": "0.5.2", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.8"
} }
}, },
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "license": "MIT",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "license": "MIT",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1", "es-define-property": "^1.0.1",
@@ -393,8 +344,7 @@
}, },
"node_modules/get-proto": { "node_modules/get-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "license": "MIT",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dependencies": { "dependencies": {
"dunder-proto": "^1.0.1", "dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0" "es-object-atoms": "^1.0.0"
@@ -405,8 +355,7 @@
}, },
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "license": "MIT",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@@ -416,8 +365,7 @@
}, },
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "license": "MIT",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@@ -427,8 +375,7 @@
}, },
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "license": "MIT",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
}, },
@@ -437,17 +384,22 @@
} }
}, },
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.0", "version": "2.0.1",
"license": "MIT", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"dependencies": { "dependencies": {
"depd": "2.0.0", "depd": "~2.0.0",
"inherits": "2.0.4", "inherits": "~2.0.4",
"setprototypeof": "1.2.0", "setprototypeof": "~1.2.0",
"statuses": "2.0.1", "statuses": "~2.0.2",
"toidentifier": "1.0.1" "toidentifier": "~1.0.1"
}, },
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/http-proxy": { "node_modules/http-proxy": {
@@ -480,8 +432,7 @@
}, },
"node_modules/http-proxy-middleware/node_modules/braces": { "node_modules/http-proxy-middleware/node_modules/braces": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "license": "MIT",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dependencies": { "dependencies": {
"fill-range": "^7.1.1" "fill-range": "^7.1.1"
}, },
@@ -491,8 +442,7 @@
}, },
"node_modules/http-proxy-middleware/node_modules/fill-range": { "node_modules/http-proxy-middleware/node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "license": "MIT",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@@ -502,16 +452,14 @@
}, },
"node_modules/http-proxy-middleware/node_modules/is-number": { "node_modules/http-proxy-middleware/node_modules/is-number": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "license": "MIT",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/http-proxy-middleware/node_modules/micromatch": { "node_modules/http-proxy-middleware/node_modules/micromatch": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "license": "MIT",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dependencies": { "dependencies": {
"braces": "^3.0.3", "braces": "^3.0.3",
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
@@ -522,8 +470,7 @@
}, },
"node_modules/http-proxy-middleware/node_modules/to-regex-range": { "node_modules/http-proxy-middleware/node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "license": "MIT",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
}, },
@@ -532,14 +479,18 @@
} }
}, },
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"dependencies": { "dependencies": {
"safer-buffer": ">= 2.1.2 < 3" "safer-buffer": ">= 2.1.2 < 3.0.0"
}, },
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/inherits": { "node_modules/inherits": {
@@ -570,62 +521,58 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "license": "MIT",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "0.3.0", "version": "1.1.0",
"license": "MIT", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.8"
} }
}, },
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
"version": "1.0.3", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
"engines": {
"node": ">=18"
},
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/methods": {
"version": "1.1.2",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": { "node_modules/mime-db": {
"version": "1.52.0", "version": "1.54.0",
"license": "MIT", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/mime-types": { "node_modules/mime-types": {
"version": "2.1.35", "version": "3.0.2",
"license": "MIT", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
"dependencies": { "dependencies": {
"mime-db": "1.52.0" "mime-db": "^1.54.0"
}, },
"engines": { "engines": {
"node": ">= 0.6" "node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/ms": { "node_modules/ms": {
@@ -633,32 +580,27 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/negotiator": { "node_modules/negotiator": {
"version": "0.6.3", "version": "1.0.0",
"license": "MIT", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/node": { "node_modules/node": {
"version": "20.19.5", "version": "20.19.5",
"resolved": "https://registry.npmjs.org/node/-/node-20.19.5.tgz",
"integrity": "sha512-9fJOHEP8AVrwpbhlUxnbudW8IbkseQVxl4yNQyI/rDfP+gNwKEmfPtBc/Luyf677i5Y0HKIBHiApiB9S9vvxKw==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "ISC",
"dependencies": { "dependencies": {
"node-bin-setup": "^1.0.0" "node-bin-setup": "^1.0.0"
}, },
"bin": { "bin": {
"node": "bin/node" "node": "bin/node.exe"
}, },
"engines": { "engines": {
"npm": ">=5.0.0" "npm": ">=5.0.0"
} }
}, },
"node_modules/node-bin-setup": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/node-bin-setup/-/node-bin-setup-1.1.4.tgz",
"integrity": "sha512-vWNHOne0ZUavArqPP5LJta50+S8R261Fr5SvGul37HbEDcowvLjwdvd0ZeSr0r2lTSrPxl6okq9QUw8BFGiAxA=="
},
"node_modules/node-fetch": { "node_modules/node-fetch": {
"version": "2.6.7", "version": "2.6.7",
"license": "MIT", "license": "MIT",
@@ -677,10 +619,13 @@
} }
} }
}, },
"node_modules/node/node_modules/node-bin-setup": {
"version": "1.1.4",
"license": "ISC"
},
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "license": "MIT",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@@ -698,6 +643,14 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/parseurl": { "node_modules/parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -708,8 +661,7 @@
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.12", "version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "license": "MIT"
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
@@ -740,11 +692,10 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.13.0", "version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "license": "BSD-3-Clause",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"dependencies": { "dependencies": {
"side-channel": "^1.0.6" "side-channel": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@@ -762,40 +713,46 @@
} }
}, },
"node_modules/raw-body": { "node_modules/raw-body": {
"version": "2.5.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "~3.1.2",
"http-errors": "2.0.0", "http-errors": "~2.0.1",
"iconv-lite": "0.4.24", "iconv-lite": "~0.7.0",
"unpipe": "1.0.0" "unpipe": "~1.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.10"
} }
}, },
"node_modules/requires-port": { "node_modules/requires-port": {
"version": "1.0.0", "version": "1.0.0",
"license": "MIT" "license": "MIT"
}, },
"node_modules/safe-buffer": { "node_modules/router": {
"version": "5.2.1", "version": "2.2.0",
"funding": [ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
{ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
"type": "github", "dependencies": {
"url": "https://github.com/sponsors/feross" "debug": "^4.4.0",
}, "depd": "^2.0.0",
{ "is-promise": "^4.0.0",
"type": "patreon", "parseurl": "^1.3.3",
"url": "https://www.patreon.com/feross" "path-to-regexp": "^8.0.0"
}, },
{ "engines": {
"type": "consulting", "node": ">= 18"
"url": "https://feross.org/support" }
} },
], "node_modules/router/node_modules/path-to-regexp": {
"license": "MIT" "version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
}, },
"node_modules/safer-buffer": { "node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@@ -803,61 +760,46 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"node_modules/send": { "node_modules/send": {
"version": "0.19.0", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "^4.4.3",
"depd": "2.0.0", "encodeurl": "^2.0.0",
"destroy": "1.2.0", "escape-html": "^1.0.3",
"encodeurl": "~1.0.2", "etag": "^1.8.1",
"escape-html": "~1.0.3", "fresh": "^2.0.0",
"etag": "~1.8.1", "http-errors": "^2.0.1",
"fresh": "0.5.2", "mime-types": "^3.0.2",
"http-errors": "2.0.0", "ms": "^2.1.3",
"mime": "1.6.0", "on-finished": "^2.4.1",
"ms": "2.1.3", "range-parser": "^1.2.1",
"on-finished": "2.4.1", "statuses": "^2.0.2"
"range-parser": "~1.2.1",
"statuses": "2.0.1"
}, },
"engines": { "engines": {
"node": ">= 0.8.0" "node": ">= 18"
} },
}, "funding": {
"node_modules/send/node_modules/debug": { "type": "opencollective",
"version": "2.6.9", "url": "https://opencollective.com/express"
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/send/node_modules/debug/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
} }
}, },
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "1.16.2", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
"dependencies": { "dependencies": {
"encodeurl": "~2.0.0", "encodeurl": "^2.0.0",
"escape-html": "~1.0.3", "escape-html": "^1.0.3",
"parseurl": "~1.3.3", "parseurl": "^1.3.3",
"send": "0.19.0" "send": "^1.2.0"
}, },
"engines": { "engines": {
"node": ">= 0.8.0" "node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
@@ -866,8 +808,7 @@
}, },
"node_modules/side-channel": { "node_modules/side-channel": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "license": "MIT",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"object-inspect": "^1.13.3", "object-inspect": "^1.13.3",
@@ -884,8 +825,7 @@
}, },
"node_modules/side-channel-list": { "node_modules/side-channel-list": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "license": "MIT",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
"object-inspect": "^1.13.3" "object-inspect": "^1.13.3"
@@ -899,8 +839,7 @@
}, },
"node_modules/side-channel-map": { "node_modules/side-channel-map": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "license": "MIT",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"dependencies": { "dependencies": {
"call-bound": "^1.0.2", "call-bound": "^1.0.2",
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -916,8 +855,7 @@
}, },
"node_modules/side-channel-weakmap": { "node_modules/side-channel-weakmap": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "license": "MIT",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"dependencies": { "dependencies": {
"call-bound": "^1.0.2", "call-bound": "^1.0.2",
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -933,8 +871,9 @@
} }
}, },
"node_modules/statuses": { "node_modules/statuses": {
"version": "2.0.1", "version": "2.0.2",
"license": "MIT", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -951,11 +890,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/type-is": { "node_modules/type-is": {
"version": "1.6.18", "version": "2.0.1",
"license": "MIT", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
"dependencies": { "dependencies": {
"media-typer": "0.3.0", "content-type": "^1.0.5",
"mime-types": "~2.1.24" "media-typer": "^1.1.0",
"mime-types": "^3.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@@ -968,13 +909,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/utils-merge": {
"version": "1.0.1",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"license": "MIT", "license": "MIT",
@@ -993,6 +927,11 @@
"tr46": "~0.0.3", "tr46": "~0.0.3",
"webidl-conversions": "^3.0.0" "webidl-conversions": "^3.0.0"
} }
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
} }
} }
} }

View File

@@ -11,9 +11,10 @@
"keywords": [], "keywords": [],
"author": "Microsoft Corporation", "author": "Microsoft Corporation",
"dependencies": { "dependencies": {
"body-parser": "^1.20.3", "body-parser": "^2.2.2",
"express": "^4.21.2", "express": "^5.2.1",
"http-proxy-middleware": "^3.0.3", "follow-redirects": "^1.15.6",
"http-proxy-middleware": "^3.0.5",
"node": "^20.19.5", "node": "^20.19.5",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"path-to-regexp": "^0.1.12" "path-to-regexp": "^0.1.12"

11
src/@types/i18next.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
import "i18next";
import Resources from "Localization/en/Resources.json";
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "Resources";
resources: {
Resources: typeof Resources;
};
}
}

View File

@@ -1,5 +1,7 @@
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/SubscriptionType"; import { SubscriptionType } from "../Contracts/SubscriptionType";
import { isExpectedError } from "../Metrics/ErrorClassification";
import { scenarioMonitor } from "../Metrics/ScenarioMonitor";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { ARMError } from "../Utils/arm/request"; import { ARMError } from "../Utils/arm/request";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
@@ -7,19 +9,36 @@ import { HttpStatusCodes } from "./Constants";
import { logError } from "./Logger"; import { logError } from "./Logger";
import { sendMessage } from "./MessageHandler"; import { sendMessage } from "./MessageHandler";
export const handleError = (error: string | ARMError | Error, area: string, consoleErrorPrefix?: string): void => { export interface HandleErrorOptions {
/** Optional redacted error to use for telemetry logging instead of the original error */
redactedError?: string | ARMError | Error;
}
export const handleError = (
error: string | ARMError | Error,
area: string,
consoleErrorPrefix?: string,
options?: HandleErrorOptions,
): void => {
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
const errorCode = error instanceof ARMError ? error.code : undefined; const errorCode = error instanceof ARMError ? error.code : undefined;
// logs error to data explorer console // logs error to data explorer console (always shows original, non-redacted message)
const consoleErrorMessage = consoleErrorPrefix ? `${consoleErrorPrefix}:\n ${errorMessage}` : errorMessage; const consoleErrorMessage = consoleErrorPrefix ? `${consoleErrorPrefix}:\n ${errorMessage}` : errorMessage;
logConsoleError(consoleErrorMessage); logConsoleError(consoleErrorMessage);
// logs error to both app insight and kusto // logs error to both app insight and kusto (use redacted message if provided)
logError(errorMessage, area, errorCode); const telemetryErrorMessage = options?.redactedError ? getErrorMessage(options.redactedError) : errorMessage;
logError(telemetryErrorMessage, area, errorCode);
// checks for errors caused by firewall and sends them to portal to handle // checks for errors caused by firewall and sends them to portal to handle
sendNotificationForError(errorMessage, errorCode); sendNotificationForError(errorMessage, errorCode);
// Mark expected failures for health metrics (auth, firewall, permissions, etc.)
// This ensures timeouts with expected failures emit healthy instead of unhealthy
if (isExpectedError(error)) {
scenarioMonitor.markExpectedFailure();
}
}; };
export const getErrorMessage = (error: string | Error = ""): string => { export const getErrorMessage = (error: string | Error = ""): string => {

View File

@@ -38,7 +38,7 @@ export function queryIterator(databaseId: string, collection: Collection, query:
let continuationToken: string; let continuationToken: string;
return { return {
fetchNext: () => { fetchNext: () => {
return queryDocuments(databaseId, collection, false, query).then((response) => { return queryDocuments(databaseId, collection, false, query, continuationToken).then((response) => {
continuationToken = response.continuationToken; continuationToken = response.continuationToken;
const headers: { [key: string]: string | number } = {}; const headers: { [key: string]: string | number } = {};
response.headers.forEach((value, key) => { response.headers.forEach((value, key) => {

View File

@@ -0,0 +1,78 @@
import { IButtonStyles, IStackStyles, ITextStyles } from "@fluentui/react";
import * as React from "react";
export const getDropdownButtonStyles = (disabled: boolean): IButtonStyles => ({
root: {
width: "100%",
height: "32px",
padding: "0 28px 0 8px",
border: "1px solid #8a8886",
background: "#fff",
color: "#323130",
textAlign: "left",
cursor: disabled ? "not-allowed" : "pointer",
position: "relative",
},
flexContainer: {
justifyContent: "flex-start",
},
label: {
fontWeight: "normal",
fontSize: "14px",
textAlign: "left",
},
});
export const buttonLabelStyles: ITextStyles = {
root: {
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
display: "block",
textAlign: "left",
},
};
export const buttonWrapperStyles: React.CSSProperties = {
position: "relative",
width: "100%",
};
export const chevronStyles: React.CSSProperties = {
position: "absolute",
right: "8px",
top: "50%",
transform: "translateY(-50%)",
pointerEvents: "none",
fontSize: "12px",
};
export const calloutContentStyles: IStackStyles = {
root: {
display: "flex",
flexDirection: "column",
},
};
export const listContainerStyles: IStackStyles = {
root: {
maxHeight: "300px",
overflowY: "auto",
},
};
export const getItemStyles = (isSelected: boolean): React.CSSProperties => ({
padding: "8px 12px",
cursor: "pointer",
fontSize: "14px",
backgroundColor: isSelected ? "#e6e6e6" : "transparent",
textAlign: "left",
});
export const emptyMessageStyles: ITextStyles = {
root: {
padding: "8px 12px",
color: "#605e5c",
textAlign: "left",
},
};

View File

@@ -0,0 +1,200 @@
import { fireEvent, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import React from "react";
import { SearchableDropdown } from "./SearchableDropdown";
interface TestItem {
id: string;
name: string;
}
describe("SearchableDropdown", () => {
const mockItems: TestItem[] = [
{ id: "1", name: "Item One" },
{ id: "2", name: "Item Two" },
{ id: "3", name: "Item Three" },
];
const defaultProps = {
label: "Test Label",
items: mockItems,
selectedItem: null as TestItem | null,
onSelect: jest.fn(),
getKey: (item: TestItem) => item.id,
getDisplayText: (item: TestItem) => item.name,
placeholder: "Select an item",
filterPlaceholder: "Filter items",
className: "test-dropdown",
};
beforeEach(() => {
jest.clearAllMocks();
});
it("should render with label and placeholder", () => {
render(<SearchableDropdown {...defaultProps} />);
expect(screen.getByText("Test Label")).toBeInTheDocument();
expect(screen.getByText("Select an item")).toBeInTheDocument();
});
it("should display selected item", () => {
const propsWithSelection = {
...defaultProps,
selectedItem: mockItems[0],
};
render(<SearchableDropdown {...propsWithSelection} />);
expect(screen.getByText("Item One")).toBeInTheDocument();
});
it("should show 'No items found' when items array is empty", () => {
const propsWithEmptyItems = {
...defaultProps,
items: [] as TestItem[],
};
render(<SearchableDropdown {...propsWithEmptyItems} />);
expect(screen.getByText("No Test Labels Found")).toBeInTheDocument();
});
it("should open dropdown when button is clicked", () => {
render(<SearchableDropdown {...defaultProps} />);
const button = screen.getByText("Select an item");
fireEvent.click(button);
expect(screen.getByPlaceholderText("Filter items")).toBeInTheDocument();
});
it("should filter items based on search text", () => {
render(<SearchableDropdown {...defaultProps} />);
const button = screen.getByText("Select an item");
fireEvent.click(button);
const searchBox = screen.getByPlaceholderText("Filter items");
fireEvent.change(searchBox, { target: { value: "Two" } });
expect(screen.getByText("Item Two")).toBeInTheDocument();
expect(screen.queryByText("Item One")).not.toBeInTheDocument();
expect(screen.queryByText("Item Three")).not.toBeInTheDocument();
});
it("should call onSelect when an item is clicked", () => {
const onSelectMock = jest.fn();
const propsWithMock = {
...defaultProps,
onSelect: onSelectMock,
};
render(<SearchableDropdown {...propsWithMock} />);
const button = screen.getByText("Select an item");
fireEvent.click(button);
const item = screen.getByText("Item Two");
fireEvent.click(item);
expect(onSelectMock).toHaveBeenCalledWith(mockItems[1]);
});
it("should close dropdown after selecting an item", () => {
render(<SearchableDropdown {...defaultProps} />);
const button = screen.getByText("Select an item");
fireEvent.click(button);
expect(screen.getByPlaceholderText("Filter items")).toBeInTheDocument();
const item = screen.getByText("Item One");
fireEvent.click(item);
expect(screen.queryByPlaceholderText("Filter items")).not.toBeInTheDocument();
});
it("should disable button when disabled prop is true", () => {
const propsWithDisabled = {
...defaultProps,
disabled: true,
};
render(<SearchableDropdown {...propsWithDisabled} />);
const button = screen.getByRole("button");
expect(button).toBeDisabled();
});
it("should not open dropdown when disabled", () => {
const propsWithDisabled = {
...defaultProps,
disabled: true,
};
render(<SearchableDropdown {...propsWithDisabled} />);
const button = screen.getByRole("button");
fireEvent.click(button);
expect(screen.queryByPlaceholderText("Filter items")).not.toBeInTheDocument();
});
it("should show 'No items found' when search yields no results", () => {
render(<SearchableDropdown {...defaultProps} />);
const button = screen.getByText("Select an item");
fireEvent.click(button);
const searchBox = screen.getByPlaceholderText("Filter items");
fireEvent.change(searchBox, { target: { value: "Nonexistent" } });
expect(screen.getByText("No items found")).toBeInTheDocument();
});
it("should handle case-insensitive filtering", () => {
render(<SearchableDropdown {...defaultProps} />);
const button = screen.getByText("Select an item");
fireEvent.click(button);
const searchBox = screen.getByPlaceholderText("Filter items");
fireEvent.change(searchBox, { target: { value: "two" } });
expect(screen.getByText("Item Two")).toBeInTheDocument();
expect(screen.queryByText("Item One")).not.toBeInTheDocument();
});
it("should clear filter text when dropdown is closed and reopened", () => {
render(<SearchableDropdown {...defaultProps} />);
const button = screen.getByText("Select an item");
fireEvent.click(button);
const searchBox = screen.getByPlaceholderText("Filter items");
fireEvent.change(searchBox, { target: { value: "Two" } });
// Close dropdown by selecting an item
const item = screen.getByText("Item Two");
fireEvent.click(item);
// Reopen dropdown
fireEvent.click(button);
// Filter text should be cleared
const reopenedSearchBox = screen.getByPlaceholderText("Filter items");
expect(reopenedSearchBox).toHaveValue("");
});
it("should use custom placeholder text", () => {
const propsWithCustomPlaceholder = {
...defaultProps,
placeholder: "Choose an option",
};
render(<SearchableDropdown {...propsWithCustomPlaceholder} />);
expect(screen.getByText("Choose an option")).toBeInTheDocument();
});
it("should use custom filter placeholder text", () => {
const propsWithCustomFilterPlaceholder = {
...defaultProps,
filterPlaceholder: "Search here",
};
render(<SearchableDropdown {...propsWithCustomFilterPlaceholder} />);
const button = screen.getByText("Select an item");
fireEvent.click(button);
expect(screen.getByPlaceholderText("Search here")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,155 @@
import {
Callout,
DefaultButton,
DirectionalHint,
Icon,
ISearchBoxStyles,
Label,
SearchBox,
Stack,
Text,
} from "@fluentui/react";
import * as React from "react";
import { useMemo, useRef, useState } from "react";
import {
buttonLabelStyles,
buttonWrapperStyles,
calloutContentStyles,
chevronStyles,
emptyMessageStyles,
getDropdownButtonStyles,
getItemStyles,
listContainerStyles,
} from "./SearchableDropdown.styles";
interface SearchableDropdownProps<T> {
label: string;
items: T[];
selectedItem: T | null;
onSelect: (item: T) => void;
getKey: (item: T) => string;
getDisplayText: (item: T) => string;
placeholder?: string;
filterPlaceholder?: string;
className?: string;
disabled?: boolean;
onDismiss?: () => void;
searchBoxStyles?: Partial<ISearchBoxStyles>;
}
export const SearchableDropdown = <T,>({
label,
items,
selectedItem,
onSelect,
getKey,
getDisplayText,
placeholder = "Select an item",
filterPlaceholder = "Filter items",
className,
disabled = false,
onDismiss,
searchBoxStyles: customSearchBoxStyles,
}: SearchableDropdownProps<T>): React.ReactElement => {
const [isOpen, setIsOpen] = useState(false);
const [filterText, setFilterText] = useState("");
const buttonRef = useRef<HTMLDivElement>(null);
const closeDropdown = () => {
setIsOpen(false);
setFilterText("");
};
const filteredItems = useMemo(
() => items?.filter((item) => getDisplayText(item).toLowerCase().includes(filterText.toLowerCase())),
[items, filterText, getDisplayText],
);
const handleDismiss = () => {
closeDropdown();
onDismiss?.();
};
const handleButtonClick = () => {
if (disabled) {
return;
}
setIsOpen(!isOpen);
};
const handleSelect = (item: T) => {
onSelect(item);
closeDropdown();
};
const buttonLabel = selectedItem
? getDisplayText(selectedItem)
: items?.length === 0
? `No ${label}s Found`
: placeholder;
const buttonId = `${className}-button`;
const buttonStyles = getDropdownButtonStyles(disabled);
return (
<Stack>
<Label htmlFor={buttonId}>{label}</Label>
<div ref={buttonRef} style={buttonWrapperStyles}>
<DefaultButton
id={buttonId}
className={className}
onClick={handleButtonClick}
styles={buttonStyles}
disabled={disabled}
>
<Text styles={buttonLabelStyles}>{buttonLabel}</Text>
</DefaultButton>
<Icon iconName="ChevronDown" style={chevronStyles} />
</div>
{isOpen && (
<Callout
target={buttonRef.current}
onDismiss={handleDismiss}
directionalHint={DirectionalHint.bottomLeftEdge}
isBeakVisible={false}
gapSpace={0}
setInitialFocus
>
<Stack styles={calloutContentStyles} style={{ width: buttonRef.current?.offsetWidth || 300 }}>
<SearchBox
placeholder={filterPlaceholder}
value={filterText}
onChange={(_, newValue) => setFilterText(newValue || "")}
styles={customSearchBoxStyles}
showIcon={true}
/>
<Stack styles={listContainerStyles}>
{filteredItems && filteredItems.length > 0 ? (
filteredItems.map((item) => {
const key = getKey(item);
const isSelected = selectedItem ? getKey(selectedItem) === key : false;
return (
<div
key={key}
onClick={() => handleSelect(item)}
style={getItemStyles(isSelected)}
onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = "#f3f2f1")}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = isSelected ? "#e6e6e6" : "transparent")
}
>
<Text>{getDisplayText(item)}</Text>
</div>
);
})
) : (
<Text styles={emptyMessageStyles}>No items found</Text>
)}
</Stack>
</Stack>
</Callout>
)}
</Stack>
);
};

View File

@@ -44,7 +44,8 @@ export const deleteDocuments = async (
documentIds: DocumentId[], documentIds: DocumentId[],
abortSignal: AbortSignal, abortSignal: AbortSignal,
): Promise<IBulkDeleteResult[]> => { ): Promise<IBulkDeleteResult[]> => {
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`); const totalCount = documentIds.length;
const clearMessage = logConsoleProgress(`Deleting ${totalCount} ${getEntityName(true)}`);
try { try {
const v2Container = await client().database(collection.databaseId).container(collection.id()); const v2Container = await client().database(collection.databaseId).container(collection.id());
@@ -83,11 +84,7 @@ export const deleteDocuments = async (
const flatAllResult = Array.prototype.concat.apply([], allResult); const flatAllResult = Array.prototype.concat.apply([], allResult);
return flatAllResult; return flatAllResult;
} catch (error) { } catch (error) {
handleError( handleError(error, "DeleteDocuments", `Error while deleting ${totalCount} ${getEntityName(totalCount > 1)}`);
error,
"DeleteDocuments",
`Error while deleting ${documentIds.length} ${getEntityName(documentIds.length > 1)}`,
);
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -0,0 +1,171 @@
import { redactSyntaxErrorMessage } from "./queryDocumentsPage";
/* Typical error to redact looks like this (the message property contains a JSON string with nested structure):
{
"message": "{\"code\":\"BadRequest\",\"message\":\"{\\\"errors\\\":[{\\\"severity\\\":\\\"Error\\\",\\\"location\\\":{\\\"start\\\":0,\\\"end\\\":5},\\\"code\\\":\\\"SC1001\\\",\\\"message\\\":\\\"Syntax error, incorrect syntax near 'Crazy'.\\\"}]}\\r\\nActivityId: d5424e10-51bd-46f7-9aec-7b40bed36f17, Windows/10.0.20348 cosmos-netstandard-sdk/3.18.0\"}"
}
*/
// Helper to create the nested error structure that matches what the SDK returns
const createNestedError = (
errors: Array<{ severity?: string; location?: { start: number; end: number }; code: string; message: string }>,
activityId: string = "test-activity-id",
): { message: string } => {
const innerErrorsJson = JSON.stringify({ errors });
const innerMessage = `${innerErrorsJson}\r\n${activityId}`;
const outerJson = JSON.stringify({ code: "BadRequest", message: innerMessage });
return { message: outerJson };
};
// Helper to parse the redacted result
const parseRedactedResult = (result: { message: string }) => {
const outerParsed = JSON.parse(result.message);
const [innerErrorsJson, activityIdPart] = outerParsed.message.split("\r\n");
const innerErrors = JSON.parse(innerErrorsJson);
return { outerParsed, innerErrors, activityIdPart };
};
describe("redactSyntaxErrorMessage", () => {
it("should redact SC1001 error message", () => {
const error = createNestedError(
[
{
severity: "Error",
location: { start: 0, end: 5 },
code: "SC1001",
message: "Syntax error, incorrect syntax near 'Crazy'.",
},
],
"ActivityId: d5424e10-51bd-46f7-9aec-7b40bed36f17",
);
const result = redactSyntaxErrorMessage(error) as { message: string };
const { outerParsed, innerErrors, activityIdPart } = parseRedactedResult(result);
expect(outerParsed.code).toBe("BadRequest");
expect(innerErrors.errors[0].message).toBe("__REDACTED__");
expect(activityIdPart).toContain("ActivityId: d5424e10-51bd-46f7-9aec-7b40bed36f17");
});
it("should redact SC2001 error message", () => {
const error = createNestedError(
[
{
severity: "Error",
location: { start: 0, end: 10 },
code: "SC2001",
message: "Some sensitive syntax error message.",
},
],
"ActivityId: abc123",
);
const result = redactSyntaxErrorMessage(error) as { message: string };
const { outerParsed, innerErrors, activityIdPart } = parseRedactedResult(result);
expect(outerParsed.code).toBe("BadRequest");
expect(innerErrors.errors[0].message).toBe("__REDACTED__");
expect(activityIdPart).toContain("ActivityId: abc123");
});
it("should redact multiple errors with SC1001 and SC2001 codes", () => {
const error = createNestedError(
[
{ severity: "Error", code: "SC1001", message: "First error" },
{ severity: "Error", code: "SC2001", message: "Second error" },
],
"ActivityId: xyz",
);
const result = redactSyntaxErrorMessage(error) as { message: string };
const { innerErrors } = parseRedactedResult(result);
expect(innerErrors.errors[0].message).toBe("__REDACTED__");
expect(innerErrors.errors[1].message).toBe("__REDACTED__");
});
it("should not redact errors with other codes", () => {
const error = createNestedError(
[{ severity: "Error", code: "SC9999", message: "This should not be redacted." }],
"ActivityId: test123",
);
const result = redactSyntaxErrorMessage(error);
expect(result).toBe(error); // Should return original error unchanged
});
it("should not modify non-BadRequest errors", () => {
const innerMessage = JSON.stringify({ errors: [{ code: "SC1001", message: "Should not be redacted" }] });
const error = {
message: JSON.stringify({ code: "NotFound", message: innerMessage }),
};
const result = redactSyntaxErrorMessage(error);
expect(result).toBe(error);
});
it("should handle errors without message property", () => {
const error = { code: "BadRequest" };
const result = redactSyntaxErrorMessage(error);
expect(result).toBe(error);
});
it("should handle non-object errors", () => {
const stringError = "Simple string error";
const nullError: null = null;
const undefinedError: undefined = undefined;
expect(redactSyntaxErrorMessage(stringError)).toBe(stringError);
expect(redactSyntaxErrorMessage(nullError)).toBe(nullError);
expect(redactSyntaxErrorMessage(undefinedError)).toBe(undefinedError);
});
it("should handle malformed JSON in message", () => {
const error = {
message: "not valid json",
};
const result = redactSyntaxErrorMessage(error);
expect(result).toBe(error);
});
it("should handle message without ActivityId suffix", () => {
const innerErrorsJson = JSON.stringify({
errors: [{ severity: "Error", code: "SC1001", message: "Syntax error near something." }],
});
const error = {
message: JSON.stringify({ code: "BadRequest", message: innerErrorsJson + "\r\n" }),
};
const result = redactSyntaxErrorMessage(error) as { message: string };
const { innerErrors } = parseRedactedResult(result);
expect(innerErrors.errors[0].message).toBe("__REDACTED__");
});
it("should preserve other error properties", () => {
const baseError = createNestedError([{ code: "SC1001", message: "Error" }], "ActivityId: test");
const error = {
...baseError,
statusCode: 400,
additionalInfo: "extra data",
};
const result = redactSyntaxErrorMessage(error) as {
message: string;
statusCode: number;
additionalInfo: string;
};
expect(result.statusCode).toBe(400);
expect(result.additionalInfo).toBe("extra data");
const { innerErrors } = parseRedactedResult(result);
expect(innerErrors.errors[0].message).toBe("__REDACTED__");
});
});

View File

@@ -4,6 +4,51 @@ import { getEntityName } from "../DocumentUtility";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
import { MinimalQueryIterator, nextPage } from "../IteratorUtilities"; import { MinimalQueryIterator, nextPage } from "../IteratorUtilities";
// Redact sensitive information from BadRequest errors with specific codes
export const redactSyntaxErrorMessage = (error: unknown): unknown => {
const codesToRedact = ["SC1001", "SC2001", "SC1010"];
try {
// Handle error objects with a message property
if (error && typeof error === "object" && "message" in error) {
const errorObj = error as { code?: string; message?: string };
if (typeof errorObj.message === "string") {
// Parse the inner JSON from the message
const innerJson = JSON.parse(errorObj.message);
if (innerJson.code === "BadRequest" && typeof innerJson.message === "string") {
const [innerErrorsJson, activityIdPart] = innerJson.message.split("\r\n");
const innerErrorsObj = JSON.parse(innerErrorsJson);
if (Array.isArray(innerErrorsObj.errors)) {
let modified = false;
innerErrorsObj.errors = innerErrorsObj.errors.map((err: { code?: string; message?: string }) => {
if (err.code && codesToRedact.includes(err.code)) {
modified = true;
return { ...err, message: "__REDACTED__" };
}
return err;
});
if (modified) {
// Reconstruct the message with the redacted content
const redactedMessage = JSON.stringify(innerErrorsObj) + `\r\n${activityIdPart}`;
const redactedError = {
...error,
message: JSON.stringify({ ...innerJson, message: redactedMessage }),
body: undefined as unknown, // Clear body to avoid sensitive data
};
return redactedError;
}
}
}
}
}
} catch {
// If parsing fails, return the original error
}
return error;
};
export const queryDocumentsPage = async ( export const queryDocumentsPage = async (
resourceName: string, resourceName: string,
documentsIterator: MinimalQueryIterator, documentsIterator: MinimalQueryIterator,
@@ -18,7 +63,12 @@ export const queryDocumentsPage = async (
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`); logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
return result; return result;
} catch (error) { } catch (error) {
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`); // Redact sensitive information for telemetry while showing original in console
const redactedError = redactSyntaxErrorMessage(error);
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`, {
redactedError: redactedError as Error,
});
throw error; throw error;
} finally { } finally {
clearMessage(); clearMessage();

View File

@@ -1,5 +1,6 @@
import { ContainerResponse } from "@azure/cosmos"; import { ContainerResponse } from "@azure/cosmos";
import { Queries } from "Common/Constants"; import { Queries } from "Common/Constants";
import * as Logger from "Common/Logger";
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract"; import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil"; import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
@@ -61,7 +62,14 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
return await readCollectionsWithARM(databaseId); return await readCollectionsWithARM(databaseId);
} }
Logger.logInfo(`readCollections: calling fetchAll for database ${databaseId}`, "readCollections");
const fetchAllStart = Date.now();
const sdkResponse = await client().database(databaseId).containers.readAll().fetchAll(); const sdkResponse = await client().database(databaseId).containers.readAll().fetchAll();
Logger.logInfo(
`readCollections: fetchAll completed for database ${databaseId}, count=${sdkResponse.resources
?.length}, durationMs=${Date.now() - fetchAllStart}`,
"readCollections",
);
return sdkResponse.resources as DataModels.Collection[]; return sdkResponse.resources as DataModels.Collection[];
} catch (error) { } catch (error) {
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`); handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);

View File

@@ -77,6 +77,12 @@ let configContext: Readonly<ConfigContext> = {
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`, `^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
`^https:\\/\\/.*\\.powerbi\\.com$`, `^https:\\/\\/.*\\.powerbi\\.com$`,
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`, `^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
`^https:\\/\\/explorer\\.cosmos\\.sovcloud-api\\.fr$`,
`^https:\\/\\/portal\\.sovcloud-azure\\.fr$`,
`^https:\\/\\/explorer\\.cosmos\\.sovcloud-api\\.de$`,
`^https:\\/\\/portal\\.sovcloud-azure\\.de$`,
`^https:\\/\\/explorer\\.cosmos\\.sovcloud-api\\.sg$`,
`^https:\\/\\/portal\\.sovcloud-azure\\.sg$`,
], // Webpack injects this at build time ], // Webpack injects this at build time
gitSha: process.env.GIT_SHA, gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/", hostedExplorerURL: "https://cosmos.azure.com/",

View File

@@ -46,6 +46,10 @@ export type DataExploreMessageV3 =
params: { params: {
updateType: "created" | "deleted" | "settings"; updateType: "created" | "deleted" | "settings";
}; };
}
| {
type: FabricMessageTypes.RestoreContainer;
params: [];
}; };
export interface GetCosmosTokenMessageOptions { export interface GetCosmosTokenMessageOptions {
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace"; verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";

View File

@@ -275,8 +275,7 @@ export interface DataMaskingPolicy {
startPosition: number; startPosition: number;
length: number; length: number;
}>; }>;
excludedPaths: string[]; excludedPaths?: string[];
isPolicyEnabled: boolean;
} }
export interface MaterializedView { export interface MaterializedView {
@@ -442,7 +441,7 @@ export interface VectorEmbeddingPolicy {
} }
export interface VectorEmbedding { export interface VectorEmbedding {
dataType: "float32" | "uint8" | "int8"; dataType: "float32" | "uint8" | "int8" | "float16";
dimensions: number; dimensions: number;
distanceFunction: "euclidean" | "cosine" | "dotproduct"; distanceFunction: "euclidean" | "cosine" | "dotproduct";
path: string; path: string;

View File

@@ -185,9 +185,10 @@ describe("CommandBar Utils", () => {
it("should respect disabled state when provided", () => { it("should respect disabled state when provided", () => {
const buttons = getCommandBarButtons(mockExplorer, false); const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button) => { // Theme toggle (index 2) is disabled in Portal mode, others are not
expect(button.disabled).toBe(false); const expectedDisabled = buttons.map((_, index) => index === 2);
}); const actualDisabled = buttons.map((button) => button.disabled);
expect(actualDisabled).toEqual(expectedDisabled);
}); });
it("should return CommandButtonComponentProps with all required properties", () => { it("should return CommandButtonComponentProps with all required properties", () => {

View File

@@ -14,6 +14,7 @@ import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommandBarBtnType[] { function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommandBarBtnType[] {
const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref); const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref);
const isPortal = configContext.platform === Platform.Portal;
const buttons: CopyJobCommandBarBtnType[] = [ const buttons: CopyJobCommandBarBtnType[] = [
{ {
key: "createCopyJob", key: "createCopyJob",
@@ -33,8 +34,13 @@ function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommand
key: "themeToggle", key: "themeToggle",
iconSrc: isDarkMode ? SunIcon : MoonIcon, iconSrc: isDarkMode ? SunIcon : MoonIcon,
label: isDarkMode ? "Light Theme" : "Dark Theme", label: isDarkMode ? "Light Theme" : "Dark Theme",
ariaLabel: isDarkMode ? "Switch to Light Theme" : "Switch to Dark Theme", ariaLabel: isPortal
onClick: () => useThemeStore.getState().toggleTheme(), ? "Dark Mode is managed in Azure Portal Settings"
: isDarkMode
? "Switch to Light Theme"
: "Switch to Dark Theme",
disabled: isPortal,
onClick: isPortal ? () => {} : () => useThemeStore.getState().toggleTheme(),
}, },
]; ];

View File

@@ -184,5 +184,10 @@ export default {
Skipped: "Cancelled", Skipped: "Cancelled",
Cancelled: "Cancelled", Cancelled: "Cancelled",
}, },
dialog: {
heading: "",
confirmButtonText: "Confirm",
cancelButtonText: "Cancel",
},
}, },
}; };

View File

@@ -395,6 +395,14 @@ describe("CopyJobUtils", () => {
expect(result).toBe(false); expect(result).toBe(false);
}); });
it("should return false for different completion percentage", () => {
const jobs1 = [createMockJob("job1", "Running")];
const jobs2 = [{ ...createMockJob("job1", "Running"), CompletionPercentage: 75 }];
const result = CopyJobUtils.isEqual(jobs1, jobs2);
expect(result).toBe(false);
});
it("should return true for empty arrays", () => { it("should return true for empty arrays", () => {
const result = CopyJobUtils.isEqual([], []); const result = CopyJobUtils.isEqual([], []);
expect(result).toBe(true); expect(result).toBe(true);

View File

@@ -142,7 +142,7 @@ export function isEqual(prevJobs: CopyJobType[], newJobs: CopyJobType[]): boolea
if (!newJob) { if (!newJob) {
return false; return false;
} }
return prevJob.Status === newJob.Status; return prevJob.Status === newJob.Status && prevJob.CompletionPercentage === newJob.CompletionPercentage;
}); });
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable jest/no-conditional-expect */
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import React from "react"; import React from "react";
@@ -5,6 +6,20 @@ import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../E
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes"; import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
import CopyJobActionMenu from "./CopyJobActionMenu"; import CopyJobActionMenu from "./CopyJobActionMenu";
const mockShowOkCancelModalDialog = jest.fn();
const mockCloseDialog = jest.fn();
const mockOpenDialog = jest.fn();
jest.mock("../../../Controls/Dialog", () => ({
useDialog: {
getState: () => ({
showOkCancelModalDialog: mockShowOkCancelModalDialog,
closeDialog: mockCloseDialog,
openDialog: mockOpenDialog,
}),
},
}));
jest.mock("../../ContainerCopyMessages", () => ({ jest.mock("../../ContainerCopyMessages", () => ({
__esModule: true, __esModule: true,
default: { default: {
@@ -18,6 +33,11 @@ jest.mock("../../ContainerCopyMessages", () => ({
cancel: "Cancel", cancel: "Cancel",
complete: "Complete", complete: "Complete",
}, },
dialog: {
heading: "Confirm Action",
confirmButtonText: "Confirm",
cancelButtonText: "Cancel",
},
}, },
}, },
})); }));
@@ -50,6 +70,9 @@ describe("CopyJobActionMenu", () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
mockShowOkCancelModalDialog.mockClear();
mockCloseDialog.mockClear();
mockOpenDialog.mockClear();
}); });
describe("Component Rendering", () => { describe("Component Rendering", () => {
@@ -266,7 +289,29 @@ describe("CopyJobActionMenu", () => {
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.pause, expect.any(Function)); expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.pause, expect.any(Function));
}); });
it("should call handleClick when cancel action is clicked", () => { it("should show confirmation dialog when cancel action is clicked", () => {
const job = createMockJob({ Name: "Test Job", Status: CopyJobStatusType.InProgress });
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
const actionButton = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionButton);
const cancelButton = screen.getByText("Cancel");
fireEvent.click(cancelButton);
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
"Confirm Action",
null,
"Confirm",
expect.any(Function),
"Cancel",
null,
expect.any(Object), // dialogBody content
);
});
it("should call handleClick when dialog is confirmed for cancel action", () => {
const job = createMockJob({ Status: CopyJobStatusType.InProgress }); const job = createMockJob({ Status: CopyJobStatusType.InProgress });
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />); render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
@@ -277,6 +322,9 @@ describe("CopyJobActionMenu", () => {
const cancelButton = screen.getByText("Cancel"); const cancelButton = screen.getByText("Cancel");
fireEvent.click(cancelButton); fireEvent.click(cancelButton);
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
onOkCallback();
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function)); expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function));
}); });
@@ -294,7 +342,33 @@ describe("CopyJobActionMenu", () => {
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.resume, expect.any(Function)); expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.resume, expect.any(Function));
}); });
it("should call handleClick when complete action is clicked", () => { it("should show confirmation dialog when complete action is clicked", () => {
const job = createMockJob({
Name: "Test Online Job",
Status: CopyJobStatusType.InProgress,
Mode: CopyJobMigrationType.Online,
});
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
const actionButton = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionButton);
const completeButton = screen.getByText("Complete");
fireEvent.click(completeButton);
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
"Confirm Action",
null,
"Confirm",
expect.any(Function),
"Cancel",
null,
expect.any(Object), // dialogBody content
);
});
it("should call handleClick when dialog is confirmed for complete action", () => {
const job = createMockJob({ const job = createMockJob({
Status: CopyJobStatusType.InProgress, Status: CopyJobStatusType.InProgress,
Mode: CopyJobMigrationType.Online, Mode: CopyJobMigrationType.Online,
@@ -308,10 +382,87 @@ describe("CopyJobActionMenu", () => {
const completeButton = screen.getByText("Complete"); const completeButton = screen.getByText("Complete");
fireEvent.click(completeButton); fireEvent.click(completeButton);
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
onOkCallback();
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.complete, expect.any(Function)); expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.complete, expect.any(Function));
}); });
}); });
describe("Dialog Body Content", () => {
it("should pass correct dialog body content for cancel action", () => {
const job = createMockJob({ Name: "MyTestJob", Status: CopyJobStatusType.InProgress });
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
const actionButton = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionButton);
const cancelButton = screen.getByText("Cancel");
fireEvent.click(cancelButton);
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
"Confirm Action",
null,
"Confirm",
expect.any(Function),
"Cancel",
null,
expect.objectContaining({
props: expect.objectContaining({
tokens: expect.any(Object),
children: expect.any(Array),
}),
}),
);
});
it("should pass correct dialog body content for complete action", () => {
const job = createMockJob({
Name: "OnlineTestJob",
Status: CopyJobStatusType.InProgress,
Mode: CopyJobMigrationType.Online,
});
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
const actionButton = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionButton);
const completeButton = screen.getByText("Complete");
fireEvent.click(completeButton);
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
"Confirm Action",
null,
"Confirm",
expect.any(Function),
"Cancel",
null,
expect.objectContaining({
props: expect.objectContaining({
tokens: expect.any(Object),
children: expect.any(Array),
}),
}),
);
});
it("should not show dialog body for actions without confirmation", () => {
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
const actionButton = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionButton);
const pauseButton = screen.getByText("Pause");
fireEvent.click(pauseButton);
expect(mockShowOkCancelModalDialog).not.toHaveBeenCalled();
});
});
describe("Disabled States During Updates", () => { describe("Disabled States During Updates", () => {
const TestComponentWrapper: React.FC<{ const TestComponentWrapper: React.FC<{
job: CopyJobType; job: CopyJobType;
@@ -339,8 +490,13 @@ describe("CopyJobActionMenu", () => {
const pauseButton = screen.getByText("Pause"); const pauseButton = screen.getByText("Pause");
fireEvent.click(pauseButton); fireEvent.click(pauseButton);
fireEvent.click(actionButton); fireEvent.click(actionButton);
const pauseButtonAfterClick = screen.getByText("Pause");
const pauseButtonAfterClick = screen.getByText("Pause").closest("button");
expect(pauseButtonAfterClick).toBeInTheDocument(); expect(pauseButtonAfterClick).toBeInTheDocument();
expect(pauseButtonAfterClick).toHaveAttribute("aria-disabled", "true");
const cancelButtonAfterClick = screen.getByText("Cancel").closest("button");
expect(cancelButtonAfterClick).toHaveAttribute("aria-disabled", "true");
}); });
it("should not disable actions for different jobs when one is updating", () => { it("should not disable actions for different jobs when one is updating", () => {
@@ -360,23 +516,7 @@ describe("CopyJobActionMenu", () => {
expect(screen.getByText("Cancel")).toBeInTheDocument(); expect(screen.getByText("Cancel")).toBeInTheDocument();
}); });
it("should properly handle multiple action types being disabled for the same job", () => { it("should disable complete action when job is being updated", () => {
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
render(<TestComponentWrapper job={job} />);
const actionButton = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionButton);
fireEvent.click(screen.getByText("Pause"));
fireEvent.click(actionButton);
fireEvent.click(screen.getByText("Cancel"));
fireEvent.click(actionButton);
expect(screen.getByText("Pause")).toBeInTheDocument();
expect(screen.getByText("Cancel")).toBeInTheDocument();
});
it("should handle complete action disabled state for online jobs", () => {
const job = createMockJob({ const job = createMockJob({
Status: CopyJobStatusType.InProgress, Status: CopyJobStatusType.InProgress,
Mode: CopyJobMigrationType.Online, Mode: CopyJobMigrationType.Online,
@@ -390,8 +530,34 @@ describe("CopyJobActionMenu", () => {
const completeButton = screen.getByText("Complete"); const completeButton = screen.getByText("Complete");
fireEvent.click(completeButton); fireEvent.click(completeButton);
// Simulate dialog confirmation to trigger state update
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
onOkCallback();
fireEvent.click(actionButton); fireEvent.click(actionButton);
expect(screen.getByText("Complete")).toBeInTheDocument(); const completeButtonAfterClick = screen.getByText("Complete").closest("button");
expect(completeButtonAfterClick).toBeInTheDocument();
expect(completeButtonAfterClick).toHaveAttribute("aria-disabled", "true");
});
it("should disable complete action when any other action is being performed", () => {
const job = createMockJob({
Status: CopyJobStatusType.InProgress,
Mode: CopyJobMigrationType.Online,
});
render(<TestComponentWrapper job={job} />);
const actionButton = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionButton);
const pauseButton = screen.getByText("Pause");
fireEvent.click(pauseButton);
fireEvent.click(actionButton);
const completeButtonAfterClick = screen.getByText("Complete").closest("button");
expect(completeButtonAfterClick).toBeInTheDocument();
expect(completeButtonAfterClick).toHaveAttribute("aria-disabled", "true");
}); });
}); });
@@ -462,6 +628,7 @@ describe("CopyJobActionMenu", () => {
expect(actionButton).toHaveAttribute("aria-label", "Actions"); expect(actionButton).toHaveAttribute("aria-label", "Actions");
expect(actionButton).toHaveAttribute("title", "Actions"); expect(actionButton).toHaveAttribute("title", "Actions");
expect(actionButton).toHaveAttribute("role", "button");
const moreIcon = actionButton.querySelector('[data-icon-name="More"]'); const moreIcon = actionButton.querySelector('[data-icon-name="More"]');
expect(moreIcon || actionButton).toBeInTheDocument(); expect(moreIcon || actionButton).toBeInTheDocument();
@@ -608,4 +775,129 @@ describe("CopyJobActionMenu", () => {
}).not.toThrow(); }).not.toThrow();
}); });
}); });
describe("Complete Coverage Tests", () => {
it("should handle all possible dialog scenarios", () => {
const dialogTests = [
{ action: CopyJobActions.cancel, status: CopyJobStatusType.InProgress, shouldShowDialog: true },
{
action: CopyJobActions.complete,
status: CopyJobStatusType.InProgress,
mode: CopyJobMigrationType.Online,
shouldShowDialog: true,
},
{ action: CopyJobActions.pause, status: CopyJobStatusType.InProgress, shouldShowDialog: false },
{ action: CopyJobActions.resume, status: CopyJobStatusType.Paused, shouldShowDialog: false },
];
dialogTests.forEach(({ action, status, mode = CopyJobMigrationType.Offline, shouldShowDialog }, index) => {
jest.clearAllMocks();
const job = createMockJob({ Status: status, Mode: mode, Name: `DialogTestJob${index}` });
const { unmount } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
const actionButton = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionButton);
const actionText = action.charAt(0).toUpperCase() + action.slice(1);
if (screen.queryByText(actionText)) {
fireEvent.click(screen.getByText(actionText));
if (shouldShowDialog) {
expect(mockShowOkCancelModalDialog).toHaveBeenCalled();
} else {
expect(mockShowOkCancelModalDialog).not.toHaveBeenCalled();
expect(mockHandleClick).toHaveBeenCalled();
}
}
unmount();
});
});
it("should verify component handles state updates correctly", () => {
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
const stateUpdater = jest.fn();
const testHandleClick: HandleJobActionClickType = (job, action, setUpdatingJobAction) => {
setUpdatingJobAction({ jobName: job.Name, action });
stateUpdater(job.Name, action);
};
render(<CopyJobActionMenu job={job} handleClick={testHandleClick} />);
const actionButton = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionButton);
const pauseButton = screen.getByText("Pause");
fireEvent.click(pauseButton);
expect(stateUpdater).toHaveBeenCalledWith(job.Name, CopyJobActions.pause);
});
});
describe("Full Integration Coverage", () => {
it("should test complete workflow for cancel action with dialog", () => {
const job = createMockJob({ Name: "Integration Test Job", Status: CopyJobStatusType.InProgress });
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
const actionButton = screen.getByRole("button", { name: "Actions" });
expect(actionButton).toHaveAttribute("data-test", "CopyJobActionMenu/Button:Integration Test Job");
fireEvent.click(actionButton);
const cancelButton = screen.getByText("Cancel");
fireEvent.click(cancelButton);
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
"Confirm Action", // title
null, // subText
"Confirm", // confirmLabel
expect.any(Function), // onOk
"Cancel", // cancelLabel
null, // onCancel
expect.any(Object), // contentHtml (dialogBody)
);
const onOkCallback = mockShowOkCancelModalDialog.mock.calls[0][3];
onOkCallback();
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function));
});
it("should test complete workflow for complete action with dialog", () => {
const job = createMockJob({
Name: "Online Integration Job",
Status: CopyJobStatusType.Running,
Mode: CopyJobMigrationType.Online,
});
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
const actionButton = screen.getByRole("button", { name: "Actions" });
fireEvent.click(actionButton);
const completeButton = screen.getByText("Complete");
fireEvent.click(completeButton);
expect(mockShowOkCancelModalDialog).toHaveBeenCalled();
const dialogContent = mockShowOkCancelModalDialog.mock.calls[0][6];
expect(dialogContent).toBeTruthy();
const onOkCallback = mockShowOkCancelModalDialog.mock.calls[0][3];
onOkCallback();
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.complete, expect.any(Function));
});
it("should maintain proper component lifecycle", () => {
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
const { rerender, unmount } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
rerender(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
expect(screen.getByRole("button", { name: "Actions" })).toBeInTheDocument();
expect(() => unmount()).not.toThrow();
});
});
}); });

View File

@@ -1,5 +1,6 @@
import { IconButton, IContextualMenuProps } from "@fluentui/react"; import { DirectionalHint, IconButton, IContextualMenuProps, Stack } from "@fluentui/react";
import React from "react"; import React from "react";
import { useDialog } from "../../../Controls/Dialog";
import ContainerCopyMessages from "../../ContainerCopyMessages"; import ContainerCopyMessages from "../../ContainerCopyMessages";
import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums"; import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums";
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes"; import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
@@ -9,6 +10,28 @@ interface CopyJobActionMenuProps {
handleClick: HandleJobActionClickType; handleClick: HandleJobActionClickType;
} }
const dialogBody = {
[CopyJobActions.cancel]: (jobName: string) => (
<Stack tokens={{ childrenGap: 10 }}>
<Stack.Item>
You are about to cancel <b>{jobName}</b> copy job.
</Stack.Item>
<Stack.Item>Cancelling will stop the job immediately.</Stack.Item>
</Stack>
),
[CopyJobActions.complete]: (jobName: string) => (
<Stack tokens={{ childrenGap: 10 }}>
<Stack.Item>
You are about to complete <b>{jobName}</b> copy job.
</Stack.Item>
<Stack.Item>
Once completed, continuous data copy will stop after any pending documents are processed. To maintain data
integrity, we recommend stopping updates to the source container before completing the job.
</Stack.Item>
</Stack>
),
};
const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick }) => { const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick }) => {
const [updatingJobAction, setUpdatingJobAction] = React.useState<{ jobName: string; action: string } | null>(null); const [updatingJobAction, setUpdatingJobAction] = React.useState<{ jobName: string; action: string } | null>(null);
if ( if (
@@ -22,9 +45,22 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
return null; return null;
} }
const showActionConfirmationDialog = (job: CopyJobType, action: CopyJobActions): void => {
useDialog
.getState()
.showOkCancelModalDialog(
ContainerCopyMessages.MonitorJobs.dialog.heading,
null,
ContainerCopyMessages.MonitorJobs.dialog.confirmButtonText,
() => handleClick(job, action, setUpdatingJobAction),
ContainerCopyMessages.MonitorJobs.dialog.cancelButtonText,
null,
action in dialogBody ? dialogBody[action as keyof typeof dialogBody](job.Name) : null,
);
};
const getMenuItems = (): IContextualMenuProps["items"] => { const getMenuItems = (): IContextualMenuProps["items"] => {
const isThisJobUpdating = updatingJobAction?.jobName === job.Name; const isThisJobUpdating = updatingJobAction?.jobName === job.Name;
const updatingAction = updatingJobAction?.action;
const baseItems = [ const baseItems = [
{ {
@@ -32,21 +68,21 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
text: ContainerCopyMessages.MonitorJobs.Actions.pause, text: ContainerCopyMessages.MonitorJobs.Actions.pause,
iconProps: { iconName: "Pause" }, iconProps: { iconName: "Pause" },
onClick: () => handleClick(job, CopyJobActions.pause, setUpdatingJobAction), onClick: () => handleClick(job, CopyJobActions.pause, setUpdatingJobAction),
disabled: isThisJobUpdating && updatingAction === CopyJobActions.pause, disabled: isThisJobUpdating,
}, },
{ {
key: CopyJobActions.cancel, key: CopyJobActions.cancel,
text: ContainerCopyMessages.MonitorJobs.Actions.cancel, text: ContainerCopyMessages.MonitorJobs.Actions.cancel,
iconProps: { iconName: "Cancel" }, iconProps: { iconName: "Cancel" },
onClick: () => handleClick(job, CopyJobActions.cancel, setUpdatingJobAction), onClick: () => showActionConfirmationDialog(job, CopyJobActions.cancel),
disabled: isThisJobUpdating && updatingAction === CopyJobActions.cancel, disabled: isThisJobUpdating,
}, },
{ {
key: CopyJobActions.resume, key: CopyJobActions.resume,
text: ContainerCopyMessages.MonitorJobs.Actions.resume, text: ContainerCopyMessages.MonitorJobs.Actions.resume,
iconProps: { iconName: "Play" }, iconProps: { iconName: "Play" },
onClick: () => handleClick(job, CopyJobActions.resume, setUpdatingJobAction), onClick: () => handleClick(job, CopyJobActions.resume, setUpdatingJobAction),
disabled: isThisJobUpdating && updatingAction === CopyJobActions.resume, disabled: isThisJobUpdating,
}, },
]; ];
@@ -67,8 +103,8 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
key: CopyJobActions.complete, key: CopyJobActions.complete,
text: ContainerCopyMessages.MonitorJobs.Actions.complete, text: ContainerCopyMessages.MonitorJobs.Actions.complete,
iconProps: { iconName: "CheckMark" }, iconProps: { iconName: "CheckMark" },
onClick: () => handleClick(job, CopyJobActions.complete, setUpdatingJobAction), onClick: () => showActionConfirmationDialog(job, CopyJobActions.complete),
disabled: isThisJobUpdating && updatingAction === CopyJobActions.complete, disabled: isThisJobUpdating,
}); });
} }
return filteredItems; return filteredItems;
@@ -86,8 +122,8 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
data-test={`CopyJobActionMenu/Button:${job.Name}`} data-test={`CopyJobActionMenu/Button:${job.Name}`}
role="button" role="button"
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }} iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
menuProps={{ items: getMenuItems() }} menuProps={{ items: getMenuItems(), directionalHint: DirectionalHint.leftTopEdge, directionalHintFixed: false }}
menuIconProps={{ iconName: "" }} menuIconProps={{ iconName: "", className: "hidden" }}
ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions} ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions}
title={ContainerCopyMessages.MonitorJobs.Columns.actions} title={ContainerCopyMessages.MonitorJobs.Columns.actions}
/> />

View File

@@ -11,9 +11,17 @@ jest.mock("../../Actions/CopyJobActions", () => ({
jest.mock("./CopyJobColumns", () => ({ jest.mock("./CopyJobColumns", () => ({
getColumns: jest.fn(() => [ getColumns: jest.fn(() => [
{
key: "LastUpdatedTime",
name: "Date & time",
fieldName: "LastUpdatedTime",
minWidth: 140,
maxWidth: 300,
isResizable: true,
},
{ {
key: "Name", key: "Name",
name: "Name", name: "Job name",
fieldName: "Name", fieldName: "Name",
minWidth: 140, minWidth: 140,
maxWidth: 300, maxWidth: 300,
@@ -165,6 +173,165 @@ describe("CopyJobsList", () => {
expect(screen.getByTestId("action-menu-job-2")).toBeInTheDocument(); expect(screen.getByTestId("action-menu-job-2")).toBeInTheDocument();
expect(screen.getByTestId("action-menu-job-3")).toBeInTheDocument(); expect(screen.getByTestId("action-menu-job-3")).toBeInTheDocument();
}); });
it("renders filter TextField with data-test attribute", () => {
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
const filterTextField = document.querySelector('[data-test="CopyJobsList/FilterTextField"]');
expect(filterTextField).toBeInTheDocument();
});
it("renders search TextField with correct placeholder", () => {
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
const searchInput = screen.getByPlaceholderText("Search jobs...");
expect(searchInput).toBeInTheDocument();
});
});
describe("Filtering", () => {
it("filters jobs by Name when text is entered", async () => {
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
const filterInput = screen.getByPlaceholderText("Search jobs...");
fireEvent.change(filterInput, { target: { value: "Job 1" } });
await waitFor(() => {
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
expect(screen.queryByText("Test Job 3")).not.toBeInTheDocument();
});
});
it("filters jobs case-insensitively", async () => {
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
const filterInput = screen.getByPlaceholderText("Search jobs...");
fireEvent.change(filterInput, { target: { value: "test job 1" } });
await waitFor(() => {
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
});
});
it("shows all jobs when filter text is empty", async () => {
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
const filterInput = screen.getByPlaceholderText("Search jobs...");
fireEvent.change(filterInput, { target: { value: "Job 1" } });
await waitFor(() => {
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
});
fireEvent.change(filterInput, { target: { value: "" } });
await waitFor(() => {
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
expect(screen.getByText("Test Job 2")).toBeInTheDocument();
expect(screen.getByText("Test Job 3")).toBeInTheDocument();
});
});
it("filters jobs by Status across all columns", async () => {
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
const filterInput = screen.getByPlaceholderText("Search jobs...");
fireEvent.change(filterInput, { target: { value: CopyJobStatusType.Running } });
await waitFor(() => {
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
expect(screen.queryByText("Test Job 3")).not.toBeInTheDocument();
});
});
it("filters jobs by Mode across all columns", async () => {
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
const filterInput = screen.getByPlaceholderText("Search jobs...");
fireEvent.change(filterInput, { target: { value: "Offline" } });
await waitFor(() => {
expect(screen.queryByText("Test Job 1")).not.toBeInTheDocument();
expect(screen.getByText("Test Job 2")).toBeInTheDocument();
expect(screen.queryByText("Test Job 3")).not.toBeInTheDocument();
});
});
it("shows no results when filter matches no jobs", async () => {
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
const filterInput = screen.getByPlaceholderText("Search jobs...");
fireEvent.change(filterInput, { target: { value: "NonExistentJob" } });
await waitFor(() => {
expect(screen.queryByText("Test Job 1")).not.toBeInTheDocument();
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
expect(screen.queryByText("Test Job 3")).not.toBeInTheDocument();
});
});
it("filters by partial text match", async () => {
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
const filterInput = screen.getByPlaceholderText("Search jobs...");
fireEvent.change(filterInput, { target: { value: "Test" } });
await waitFor(() => {
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
expect(screen.getByText("Test Job 2")).toBeInTheDocument();
expect(screen.getByText("Test Job 3")).toBeInTheDocument();
});
});
it("resets pagination when filter changes", async () => {
const manyJobs: CopyJobType[] = Array.from({ length: 25 }, (_, i) => ({
...mockJobs[0],
ID: `job-${i + 1}`,
Name: `Test Job ${i + 1}`,
}));
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
// Navigate to page 2
fireEvent.click(screen.getByLabelText("Go to next page"));
await waitFor(() => {
expect(screen.getByText("Showing 11 - 20 of 25 items")).toBeInTheDocument();
});
// Apply filter - should reset to page 1
const filterInput = screen.getByPlaceholderText("Search jobs...");
fireEvent.change(filterInput, { target: { value: "Job 1" } });
await waitFor(() => {
// Filtered results show from the beginning
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
});
});
it("updates filtered count in pager", async () => {
const manyJobs: CopyJobType[] = Array.from({ length: 25 }, (_, i) => ({
...mockJobs[0],
ID: `job-${i + 1}`,
Name: i < 5 ? `Alpha Job ${i + 1}` : `Beta Job ${i + 1}`,
}));
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
expect(screen.getByText("Showing 1 - 10 of 25 items")).toBeInTheDocument();
const filterInput = screen.getByPlaceholderText("Search jobs...");
fireEvent.change(filterInput, { target: { value: "Alpha" } });
await waitFor(() => {
expect(screen.queryByText("Showing 1 - 10 of 25 items")).not.toBeInTheDocument();
// Pager should not be visible since filtered results (5) are less than page size (10)
expect(screen.queryByLabelText("Go to next page")).not.toBeInTheDocument();
});
});
}); });
describe("Pagination", () => { describe("Pagination", () => {
@@ -342,7 +509,7 @@ describe("CopyJobsList", () => {
describe("Component Props", () => { describe("Component Props", () => {
it("uses default page size when not provided", () => { it("uses default page size when not provided", () => {
const manyJobs: CopyJobType[] = Array.from({ length: 12 }, (_, i) => ({ const manyJobs: CopyJobType[] = Array.from({ length: 20 }, (_, i) => ({
...mockJobs[0], ...mockJobs[0],
ID: `job-${i + 1}`, ID: `job-${i + 1}`,
Name: `Test Job ${i + 1}`, Name: `Test Job ${i + 1}`,
@@ -351,7 +518,7 @@ describe("CopyJobsList", () => {
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} />); render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} />);
expect(screen.getByLabelText("Go to next page")).toBeInTheDocument(); expect(screen.getByLabelText("Go to next page")).toBeInTheDocument();
expect(screen.getByText("Showing 1 - 10 of 12 items")).toBeInTheDocument(); expect(screen.getByText("Showing 1 - 15 of 20 items")).toBeInTheDocument();
}); });
it("passes correct props to getColumns function", async () => { it("passes correct props to getColumns function", async () => {
@@ -440,7 +607,33 @@ describe("CopyJobsList", () => {
render(<CopyJobsList jobs={largeJobsList} handleActionClick={mockHandleActionClick} />); render(<CopyJobsList jobs={largeJobsList} handleActionClick={mockHandleActionClick} />);
}).not.toThrow(); }).not.toThrow();
expect(screen.getByText("Showing 1 - 10 of 1000 items")).toBeInTheDocument(); expect(screen.getByText("Showing 1 - 15 of 1000 items")).toBeInTheDocument();
});
it("handles filtering with null or undefined values gracefully", async () => {
const jobsWithNullValues: CopyJobType[] = [
{
...mockJobs[0],
ID: "job-with-values",
Name: "Valid Job",
},
{
...mockJobs[1],
ID: "job-null-name",
Name: undefined as unknown as string,
},
];
expect(() => {
render(<CopyJobsList jobs={jobsWithNullValues} handleActionClick={mockHandleActionClick} />);
}).not.toThrow();
const filterInput = screen.getByPlaceholderText("Search jobs...");
fireEvent.change(filterInput, { target: { value: "Valid" } });
await waitFor(() => {
expect(screen.getByText("Valid Job")).toBeInTheDocument();
});
}); });
}); });
}); });

View File

@@ -12,8 +12,9 @@ import {
Stack, Stack,
Sticky, Sticky,
StickyPositionType, StickyPositionType,
TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import React, { useEffect } from "react"; import React, { useEffect, useMemo } from "react";
import Pager from "../../../../Common/Pager"; import Pager from "../../../../Common/Pager";
import { useThemeStore } from "../../../../hooks/useTheme"; import { useThemeStore } from "../../../../hooks/useTheme";
import { getThemeTokens } from "../../../Theme/ThemeUtil"; import { getThemeTokens } from "../../../Theme/ThemeUtil";
@@ -30,9 +31,15 @@ interface CopyJobsListProps {
const styles = { const styles = {
container: { height: "100%" } as React.CSSProperties, container: { height: "100%" } as React.CSSProperties,
stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties, stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties,
filterContainer: {
margin: "15px 5px",
},
}; };
const PAGE_SIZE = 10; const PAGE_SIZE = 15;
// Columns to search across
const searchableFields = ["Name", "Status", "LastUpdatedTime", "Mode"];
const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => { const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => {
const isDarkMode = useThemeStore((state) => state.isDarkMode); const isDarkMode = useThemeStore((state) => state.isDarkMode);
@@ -41,6 +48,23 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
const [sortedJobs, setSortedJobs] = React.useState<CopyJobType[]>(jobs); const [sortedJobs, setSortedJobs] = React.useState<CopyJobType[]>(jobs);
const [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined); const [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined);
const [isSortedDescending, setIsSortedDescending] = React.useState<boolean>(false); const [isSortedDescending, setIsSortedDescending] = React.useState<boolean>(false);
const [filterText, setFilterText] = React.useState<string>("");
const filteredJobs = useMemo(() => {
if (!filterText) {
return sortedJobs;
}
const lowerFilterText = filterText.toLowerCase();
return sortedJobs.filter((job: any) => {
return searchableFields.some((field) => {
const value = job[field];
if (value === undefined || value === null) {
return false;
}
return String(value).toLowerCase().includes(lowerFilterText);
});
});
}, [sortedJobs, filterText]);
useEffect(() => { useEffect(() => {
setSortedJobs(jobs); setSortedJobs(jobs);
@@ -64,7 +88,15 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
setStartIndex(0); setStartIndex(0);
}; };
const columns: IColumn[] = getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending); const sortableColumns: IColumn[] = getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending);
const handleFilterTextChange = (
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string,
) => {
setFilterText(newValue || "");
setStartIndex(0);
};
const _handleRowClick = (job: CopyJobType) => { const _handleRowClick = (job: CopyJobType) => {
openCopyJobDetailsPanel(job); openCopyJobDetailsPanel(job);
@@ -81,14 +113,25 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
return ( return (
<div style={styles.container}> <div style={styles.container}>
<Stack verticalFill={true}> <Stack verticalFill={true}>
<Stack.Item>
<div style={styles.filterContainer}>
<TextField
data-test="CopyJobsList/FilterTextField"
placeholder="Search jobs..."
ariaLabel="Search jobs"
value={filterText}
onChange={handleFilterTextChange}
/>
</div>
</Stack.Item>
<Stack.Item verticalFill={true} grow={1} shrink={1} style={styles.stackItem}> <Stack.Item verticalFill={true} grow={1} shrink={1} style={styles.stackItem}>
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}> <ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
<ShimmeredDetailsList <ShimmeredDetailsList
className="CopyJobListContainer" className="CopyJobListContainer"
onRenderRow={_onRenderRow} onRenderRow={_onRenderRow}
checkboxVisibility={2} checkboxVisibility={2}
columns={columns} columns={sortableColumns}
items={sortedJobs.slice(startIndex, startIndex + pageSize)} items={filteredJobs.slice(startIndex, startIndex + pageSize)}
enableShimmer={false} enableShimmer={false}
constrainMode={ConstrainMode.unconstrained} constrainMode={ConstrainMode.unconstrained}
layoutMode={DetailsListLayoutMode.justified} layoutMode={DetailsListLayoutMode.justified}
@@ -117,12 +160,12 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
/> />
</ScrollablePane> </ScrollablePane>
</Stack.Item> </Stack.Item>
{sortedJobs.length > pageSize && ( {filteredJobs.length > pageSize && (
<Stack.Item> <Stack.Item>
<Pager <Pager
disabled={false} disabled={false}
startIndex={startIndex} startIndex={startIndex}
totalCount={sortedJobs.length} totalCount={filteredJobs.length}
pageSize={pageSize} pageSize={pageSize}
onLoadPage={(startIdx /* pageSize */) => { onLoadPage={(startIdx /* pageSize */) => {
setStartIndex(startIdx); setStartIndex(startIdx);

View File

@@ -1,5 +1,27 @@
@import "../../../less/Common/Constants.less"; @import "../../../less/Common/Constants.less";
.themedTextFieldStyles() {
.ms-TextField {
.ms-TextField-fieldGroup {
background-color: var(--colorNeutralBackground1);
border-color: var(--colorNeutralStroke1);
}
.ms-TextField-field {
color: var(--colorNeutralForeground1);
background-color: var(--colorNeutralBackground1);
&::placeholder {
color: var(--colorNeutralForeground4);
}
}
.ms-Label {
color: var(--colorNeutralForeground1);
}
}
}
// Common theme-aware classes // Common theme-aware classes
.themeText { .themeText {
color: var(--colorNeutralForeground1); color: var(--colorNeutralForeground1);
@@ -119,25 +141,8 @@
filter: invert(1); filter: invert(1);
} }
.ms-TextField { .themedTextFieldStyles();
.ms-TextField-fieldGroup {
background-color: var(--colorNeutralBackground1);
border-color: var(--colorNeutralStroke1);
}
.ms-TextField-field {
color: var(--colorNeutralForeground1);
background-color: var(--colorNeutralBackground1);
&::placeholder {
color: var(--colorNeutralForeground4);
}
}
.ms-Label {
color: var(--colorNeutralForeground1);
}
}
.migrationTypeDescription { .migrationTypeDescription {
p { p {
color: var(--colorNeutralForeground1); color: var(--colorNeutralForeground1);
@@ -173,6 +178,11 @@
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
margin: 0 auto; margin: 0 auto;
body.isDarkMode & {
.themedTextFieldStyles();
}
.ms-DetailsList { .ms-DetailsList {
width: 100%; width: 100%;

View File

@@ -7,7 +7,8 @@ import {
AddGlobalSecondaryIndexPanelProps, AddGlobalSecondaryIndexPanelProps,
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel"; } from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil"; import { Keys, t } from "Localization";
import { isFabric, isFabricNative, openRestoreContainerDialog } from "Platform/Fabric/FabricUtil";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor"; import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
@@ -23,7 +24,9 @@ import DeleteSprocIcon from "../../images/DeleteSproc.svg";
import DeleteTriggerIcon from "../../images/DeleteTrigger.svg"; import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
import DeleteUDFIcon from "../../images/DeleteUDF.svg"; import DeleteUDFIcon from "../../images/DeleteUDF.svg";
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg"; import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
import PinIcon from "../../images/Pin.svg";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils"; import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { useSidePanel } from "../hooks/useSidePanel"; import { useSidePanel } from "../hooks/useSidePanel";
@@ -51,28 +54,45 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
if (isFabric() && userContext.fabricContext?.isReadOnly) { if (isFabric() && userContext.fabricContext?.isReadOnly) {
return undefined; return undefined;
} }
const isPinned = useDatabases.getState().isPinned(databaseId);
const items: TreeNodeMenuItem[] = [ const items: TreeNodeMenuItem[] = [
{
iconSrc: PinIcon,
onClick: () => useDatabases.getState().togglePinDatabase(databaseId),
label: isPinned ? "Unpin from top" : "Pin to top",
},
{ {
iconSrc: AddCollectionIcon, iconSrc: AddCollectionIcon,
onClick: () => container.onNewCollectionClicked({ databaseId }), onClick: () => container.onNewCollectionClicked({ databaseId }),
label: `New ${getCollectionName()}`, label: t(Keys.contextMenu.newContainer, { containerName: getCollectionName() }),
}, },
]; ];
if (isFabricNative() && !userContext.fabricContext?.isReadOnly) {
const features = extractFeatures();
if (features?.enableRestoreContainer) {
items.push({
iconSrc: AddCollectionIcon,
onClick: () => openRestoreContainerDialog(),
label: t(Keys.contextMenu.restoreContainer, { containerName: getCollectionName() }),
});
}
}
if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) { if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) {
items.push({ items.push({
iconSrc: DeleteDatabaseIcon, iconSrc: DeleteDatabaseIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => { onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
(useSidePanel.getState().getRef = lastFocusedElement), useSidePanel.getState().getRef = lastFocusedElement;
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel( .openSidePanel(
"Delete " + getDatabaseName(), "Delete " + getDatabaseName(),
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />, <DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
); );
}, },
label: `Delete ${getDatabaseName()}`, label: t(Keys.contextMenu.deleteDatabase, { databaseName: getDatabaseName() }),
styleClass: "deleteDatabaseMenuItem", styleClass: "deleteDatabaseMenuItem",
}); });
} }
@@ -88,7 +108,7 @@ export const createCollectionContextMenuButton = (
items.push({ items.push({
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, undefined), onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, undefined),
label: "New SQL Query", label: t(Keys.contextMenu.newSqlQuery),
}); });
} }
@@ -96,7 +116,7 @@ export const createCollectionContextMenuButton = (
items.push({ items.push({
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, undefined), onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, undefined),
label: "New Query", label: t(Keys.contextMenu.newQuery),
}); });
items.push({ items.push({
@@ -111,8 +131,8 @@ export const createCollectionContextMenuButton = (
}, },
label: label:
useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell
? "Open Mongo Shell" ? t(Keys.contextMenu.openMongoShell)
: "New Shell", : t(Keys.contextMenu.newShell),
}); });
} }
@@ -125,7 +145,7 @@ export const createCollectionContextMenuButton = (
onClick: () => { onClick: () => {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra); container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
}, },
label: "Open Cassandra Shell", label: t(Keys.contextMenu.openCassandraShell),
}); });
} }
@@ -138,7 +158,7 @@ export const createCollectionContextMenuButton = (
onClick: () => { onClick: () => {
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined); selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
}, },
label: "New Stored Procedure", label: t(Keys.contextMenu.newStoredProcedure),
}); });
items.push({ items.push({
@@ -146,7 +166,7 @@ export const createCollectionContextMenuButton = (
onClick: () => { onClick: () => {
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection); selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
}, },
label: "New UDF", label: t(Keys.contextMenu.newUdf),
}); });
items.push({ items.push({
@@ -154,7 +174,7 @@ export const createCollectionContextMenuButton = (
onClick: () => { onClick: () => {
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined); selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
}, },
label: "New Trigger", label: t(Keys.contextMenu.newTrigger),
}); });
} }
@@ -163,15 +183,15 @@ export const createCollectionContextMenuButton = (
iconSrc: DeleteCollectionIcon, iconSrc: DeleteCollectionIcon,
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => { onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
useSelectedNode.getState().setSelectedNode(selectedCollection); useSelectedNode.getState().setSelectedNode(selectedCollection);
(useSidePanel.getState().getRef = lastFocusedElement), useSidePanel.getState().getRef = lastFocusedElement;
useSidePanel useSidePanel
.getState() .getState()
.openSidePanel( .openSidePanel(
"Delete " + getCollectionName(), "Delete " + getCollectionName(),
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />, <DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
); );
}, },
label: `Delete ${getCollectionName()}`, label: t(Keys.contextMenu.deleteContainer, { containerName: getCollectionName() }),
styleClass: "deleteCollectionMenuItem", styleClass: "deleteCollectionMenuItem",
}); });
} }
@@ -208,14 +228,14 @@ export const createSampleCollectionContextMenuButton = (): TreeNodeMenuItem[] =>
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot); useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType }); traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
}, },
label: "New SQL Query", label: t(Keys.contextMenu.newSqlQuery),
}); });
} else if (copilotVersion === "v2.0") { } else if (copilotVersion === "v2.0") {
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection; const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
items.push({ items.push({
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
onClick: () => sampleCollection && sampleCollection.onNewQueryClick(sampleCollection, undefined), onClick: () => sampleCollection && sampleCollection.onNewQueryClick(sampleCollection, undefined),
label: "New SQL Query", label: t(Keys.contextMenu.newSqlQuery),
}); });
} }
} }
@@ -235,7 +255,7 @@ export const createStoreProcedureContextMenuItems = (
{ {
iconSrc: DeleteSprocIcon, iconSrc: DeleteSprocIcon,
onClick: () => storedProcedure.delete(), onClick: () => storedProcedure.delete(),
label: "Delete Stored Procedure", label: t(Keys.contextMenu.deleteStoredProcedure),
}, },
]; ];
}; };
@@ -249,7 +269,7 @@ export const createTriggerContextMenuItems = (container: Explorer, trigger: Trig
{ {
iconSrc: DeleteTriggerIcon, iconSrc: DeleteTriggerIcon,
onClick: () => trigger.delete(), onClick: () => trigger.delete(),
label: "Delete Trigger", label: t(Keys.contextMenu.deleteTrigger),
}, },
]; ];
}; };
@@ -266,7 +286,7 @@ export const createUserDefinedFunctionContextMenuItems = (
{ {
iconSrc: DeleteUDFIcon, iconSrc: DeleteUDFIcon,
onClick: () => userDefinedFunction.delete(), onClick: () => userDefinedFunction.delete(),
label: "Delete User Defined Function", label: t(Keys.contextMenu.deleteUdf),
}, },
]; ];
}; };

View File

@@ -12,6 +12,7 @@ export interface CollapsibleSectionProps {
showDelete?: boolean; showDelete?: boolean;
onDelete?: () => void; onDelete?: () => void;
disabled?: boolean; disabled?: boolean;
disableDelete?: boolean;
} }
export interface CollapsibleSectionState { export interface CollapsibleSectionState {
@@ -75,7 +76,7 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
{this.props.showDelete && ( {this.props.showDelete && (
<Stack.Item style={{ marginLeft: "auto" }}> <Stack.Item style={{ marginLeft: "auto" }}>
<IconButton <IconButton
disabled={this.props.disabled} disabled={this.props.disableDelete ?? this.props.disabled}
id={`delete-${this.props.title.split(" ").join("-")}`} id={`delete-${this.props.title.split(" ").join("-")}`}
iconProps={{ iconName: "Delete" }} iconProps={{ iconName: "Delete" }}
style={{ height: 27, marginRight: "20px" }} style={{ height: 27, marginRight: "20px" }}

View File

@@ -58,6 +58,11 @@ export interface CommandButtonComponentProps {
*/ */
tooltipText?: string; tooltipText?: string;
/**
* Rich JSX content for tooltip (used instead of tooltipText when provided)
*/
tooltipContent?: React.ReactNode;
/** /**
* Custom styles to apply to the button using Fluent UI theme tokens * Custom styles to apply to the button using Fluent UI theme tokens
*/ */

View File

@@ -17,6 +17,7 @@ import {
} from "@fluentui/react"; } from "@fluentui/react";
import React, { FC, useEffect } from "react"; import React, { FC, useEffect } from "react";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import { Keys, t } from "Localization";
export interface DialogState { export interface DialogState {
visible: boolean; visible: boolean;
@@ -88,7 +89,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
isModal: true, isModal: true,
title, title,
subText, subText,
primaryButtonText: "Close", primaryButtonText: t(Keys.common.close),
secondaryButtonText: undefined, secondaryButtonText: undefined,
onPrimaryButtonClick: () => { onPrimaryButtonClick: () => {
get().closeDialog(); get().closeDialog();

View File

@@ -30,7 +30,6 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
dataMaskingPolicy: { dataMaskingPolicy: {
includedPaths: [], includedPaths: [],
excludedPaths: ["/excludedPath"], excludedPaths: ["/excludedPath"],
isPolicyEnabled: true,
}, },
indexes: [], indexes: [],
}), }),
@@ -307,12 +306,10 @@ describe("SettingsComponent", () => {
dataMaskingContent: { dataMaskingContent: {
includedPaths: [], includedPaths: [],
excludedPaths: ["/excludedPath"], excludedPaths: ["/excludedPath"],
isPolicyEnabled: true,
}, },
dataMaskingContentBaseline: { dataMaskingContentBaseline: {
includedPaths: [], includedPaths: [],
excludedPaths: [], excludedPaths: [],
isPolicyEnabled: false,
}, },
isDataMaskingDirty: true, isDataMaskingDirty: true,
}); });
@@ -326,7 +323,6 @@ describe("SettingsComponent", () => {
expect(wrapper.state("dataMaskingContentBaseline")).toEqual({ expect(wrapper.state("dataMaskingContentBaseline")).toEqual({
includedPaths: [], includedPaths: [],
excludedPaths: ["/excludedPath"], excludedPaths: ["/excludedPath"],
isPolicyEnabled: true,
}); });
}); });
@@ -340,7 +336,6 @@ describe("SettingsComponent", () => {
const invalidPolicy: InvalidPolicy = { const invalidPolicy: InvalidPolicy = {
includedPaths: "invalid", includedPaths: "invalid",
excludedPaths: [], excludedPaths: [],
isPolicyEnabled: false,
}; };
// Use type assertion since we're deliberately testing with invalid data // Use type assertion since we're deliberately testing with invalid data
settingsComponentInstance["onDataMaskingContentChange"](invalidPolicy as unknown as DataModels.DataMaskingPolicy); settingsComponentInstance["onDataMaskingContentChange"](invalidPolicy as unknown as DataModels.DataMaskingPolicy);
@@ -349,7 +344,6 @@ describe("SettingsComponent", () => {
expect(wrapper.state("dataMaskingContent")).toEqual({ expect(wrapper.state("dataMaskingContent")).toEqual({
includedPaths: "invalid", includedPaths: "invalid",
excludedPaths: [], excludedPaths: [],
isPolicyEnabled: false,
}); });
expect(wrapper.state("dataMaskingValidationErrors")).toEqual(["includedPaths must be an array"]); expect(wrapper.state("dataMaskingValidationErrors")).toEqual(["includedPaths must be an array"]);
@@ -364,7 +358,6 @@ describe("SettingsComponent", () => {
}, },
], ],
excludedPaths: ["/excludedPath"], excludedPaths: ["/excludedPath"],
isPolicyEnabled: true,
}; };
settingsComponentInstance["onDataMaskingContentChange"](validPolicy); settingsComponentInstance["onDataMaskingContentChange"](validPolicy);
@@ -388,7 +381,6 @@ describe("SettingsComponent", () => {
}, },
], ],
excludedPaths: ["/excludedPath1"], excludedPaths: ["/excludedPath1"],
isPolicyEnabled: false,
}; };
const modifiedPolicy = { const modifiedPolicy = {
@@ -401,7 +393,6 @@ describe("SettingsComponent", () => {
}, },
], ],
excludedPaths: ["/excludedPath2"], excludedPaths: ["/excludedPath2"],
isPolicyEnabled: true,
}; };
// Set initial state // Set initial state

View File

@@ -16,7 +16,7 @@ import {
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView"; import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { isFabricNative } from "Platform/Fabric/FabricUtil"; import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { isCapabilityEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isRunningOnPublicCloud } from "Utils/CloudUtils"; import { isRunningOnPublicCloud } from "Utils/CloudUtils";
import * as React from "react"; import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg"; import DiscardIcon from "../../../../images/discard.svg";
@@ -44,6 +44,7 @@ import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2"; import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
import "./SettingsComponent.less"; import "./SettingsComponent.less";
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils"; import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
import { Keys, t } from "Localization";
import { import {
ConflictResolutionComponent, ConflictResolutionComponent,
ConflictResolutionComponentProps, ConflictResolutionComponentProps,
@@ -70,6 +71,7 @@ import {
getMongoNotification, getMongoNotification,
getTabTitle, getTabTitle,
hasDatabaseSharedThroughput, hasDatabaseSharedThroughput,
isDataMaskingEnabled,
isDirty, isDirty,
parseConflictResolutionMode, parseConflictResolutionMode,
parseConflictResolutionProcedure, parseConflictResolutionProcedure,
@@ -122,6 +124,7 @@ export interface SettingsComponentState {
vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy; vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy; vectorEmbeddingPolicyBaseline: DataModels.VectorEmbeddingPolicy;
isVectorEmbeddingPolicyValid: boolean;
fullTextPolicy: DataModels.FullTextPolicy; fullTextPolicy: DataModels.FullTextPolicy;
fullTextPolicyBaseline: DataModels.FullTextPolicy; fullTextPolicyBaseline: DataModels.FullTextPolicy;
shouldDiscardContainerPolicies: boolean; shouldDiscardContainerPolicies: boolean;
@@ -243,6 +246,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
vectorEmbeddingPolicy: undefined, vectorEmbeddingPolicy: undefined,
vectorEmbeddingPolicyBaseline: undefined, vectorEmbeddingPolicyBaseline: undefined,
isVectorEmbeddingPolicyValid: true,
fullTextPolicy: undefined, fullTextPolicy: undefined,
fullTextPolicyBaseline: undefined, fullTextPolicyBaseline: undefined,
shouldDiscardContainerPolicies: false, shouldDiscardContainerPolicies: false,
@@ -370,6 +374,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
return false; return false;
} }
if (!this.state.isVectorEmbeddingPolicyValid) {
return false;
}
return ( return (
this.state.isScaleSaveable || this.state.isScaleSaveable ||
this.state.isSubSettingsSaveable || this.state.isSubSettingsSaveable ||
@@ -504,6 +512,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
changeFeedPolicy: this.state.changeFeedPolicyBaseline, changeFeedPolicy: this.state.changeFeedPolicyBaseline,
autoPilotThroughput: this.state.autoPilotThroughputBaseline, autoPilotThroughput: this.state.autoPilotThroughputBaseline,
isAutoPilotSelected: this.state.wasAutopilotOriginallySet, isAutoPilotSelected: this.state.wasAutopilotOriginallySet,
isVectorEmbeddingPolicyValid: true,
shouldDiscardContainerPolicies: true, shouldDiscardContainerPolicies: true,
shouldDiscardIndexingPolicy: true, shouldDiscardIndexingPolicy: true,
isScaleSaveable: false, isScaleSaveable: false,
@@ -648,6 +657,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private onVectorEmbeddingPolicyDirtyChange = (isVectorEmbeddingPolicyDirty: boolean): void => private onVectorEmbeddingPolicyDirtyChange = (isVectorEmbeddingPolicyDirty: boolean): void =>
this.setState({ isContainerPolicyDirty: isVectorEmbeddingPolicyDirty }); this.setState({ isContainerPolicyDirty: isVectorEmbeddingPolicyDirty });
private onVectorEmbeddingPolicyValidationChange = (isVectorEmbeddingPolicyValid: boolean): void =>
this.setState({ isVectorEmbeddingPolicyValid });
private onFullTextPolicyDirtyChange = (isFullTextPolicyDirty: boolean): void => private onFullTextPolicyDirtyChange = (isFullTextPolicyDirty: boolean): void =>
this.setState({ isContainerPolicyDirty: isFullTextPolicyDirty }); this.setState({ isContainerPolicyDirty: isFullTextPolicyDirty });
@@ -686,22 +698,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.setState({ isComputedPropertiesDirty: isComputedPropertiesDirty }); this.setState({ isComputedPropertiesDirty: isComputedPropertiesDirty });
private onDataMaskingContentChange = (newDataMasking: DataModels.DataMaskingPolicy): void => { private onDataMaskingContentChange = (newDataMasking: DataModels.DataMaskingPolicy): void => {
if (!newDataMasking.excludedPaths) {
newDataMasking.excludedPaths = [];
}
if (!newDataMasking.includedPaths) {
newDataMasking.includedPaths = [];
}
const validationErrors = []; const validationErrors = [];
if (!Array.isArray(newDataMasking.includedPaths)) { if (newDataMasking.includedPaths === undefined || newDataMasking.includedPaths === null) {
validationErrors.push("includedPaths must be an array"); validationErrors.push(t(Keys.controls.settings.dataMasking.includedPathsRequired));
} else if (!Array.isArray(newDataMasking.includedPaths)) {
validationErrors.push(t(Keys.controls.settings.dataMasking.includedPathsMustBeArray));
} }
if (!Array.isArray(newDataMasking.excludedPaths)) { if (newDataMasking.excludedPaths !== undefined && !Array.isArray(newDataMasking.excludedPaths)) {
validationErrors.push("excludedPaths must be an array"); validationErrors.push(t(Keys.controls.settings.dataMasking.excludedPathsMustBeArray));
}
if (typeof newDataMasking.isPolicyEnabled !== "boolean") {
validationErrors.push("isPolicyEnabled must be a boolean");
} }
this.setState({ this.setState({
@@ -842,7 +846,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const dataMaskingContent: DataModels.DataMaskingPolicy = { const dataMaskingContent: DataModels.DataMaskingPolicy = {
includedPaths: this.collection.dataMaskingPolicy?.()?.includedPaths || [], includedPaths: this.collection.dataMaskingPolicy?.()?.includedPaths || [],
excludedPaths: this.collection.dataMaskingPolicy?.()?.excludedPaths || [], excludedPaths: this.collection.dataMaskingPolicy?.()?.excludedPaths || [],
isPolicyEnabled: this.collection.dataMaskingPolicy?.()?.isPolicyEnabled ?? true,
}; };
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy = const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy(); this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
@@ -904,7 +907,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const buttons: CommandButtonComponentProps[] = []; const buttons: CommandButtonComponentProps[] = [];
const isExecuting = this.props.settingsTab.isExecuting(); const isExecuting = this.props.settingsTab.isExecuting();
if (this.saveSettingsButton.isVisible()) { if (this.saveSettingsButton.isVisible()) {
const label = "Save"; const label = t(Keys.common.save);
buttons.push({ buttons.push({
iconSrc: SaveIcon, iconSrc: SaveIcon,
iconAlt: label, iconAlt: label,
@@ -917,7 +920,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
} }
if (this.discardSettingsChangesButton.isVisible()) { if (this.discardSettingsChangesButton.isVisible()) {
const label = "Discard"; const label = t(Keys.common.discard);
buttons.push({ buttons.push({
iconSrc: DiscardIcon, iconSrc: DiscardIcon,
iconAlt: label, iconAlt: label,
@@ -942,9 +945,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1; const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
const throughputDelta = (newThroughput - this.offer.autoscaleMaxThroughput) * numberOfRegions; const throughputDelta = (newThroughput - this.offer.autoscaleMaxThroughput) * numberOfRegions;
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) { if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${ throughputError = t(Keys.controls.settings.throughput.throughputCapError, {
this.totalThroughputUsed + throughputDelta throughputCap: String(throughputCap),
} RU/s. Change total throughput limit in cost management.`; newTotalThroughput: String(this.totalThroughputUsed + throughputDelta),
});
} }
this.setState({ autoPilotThroughput: newThroughput, throughputError }); this.setState({ autoPilotThroughput: newThroughput, throughputError });
}; };
@@ -955,9 +959,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1; const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
const throughputDelta = (newThroughput - this.offer.manualThroughput) * numberOfRegions; const throughputDelta = (newThroughput - this.offer.manualThroughput) * numberOfRegions;
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) { if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${ throughputError = t(Keys.controls.settings.throughput.throughputCapError, {
this.totalThroughputUsed + throughputDelta throughputCap: String(throughputCap),
} RU/s. Change total throughput limit in cost management.`; newTotalThroughput: String(this.totalThroughputUsed + throughputDelta),
});
} }
this.setState({ throughput: newThroughput, throughputError }); this.setState({ throughput: newThroughput, throughputError });
}; };
@@ -1073,8 +1078,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
newCollection.fullTextPolicy = this.state.fullTextPolicy; newCollection.fullTextPolicy = this.state.fullTextPolicy;
// Only send data masking policy if it was modified (dirty) // Only send data masking policy if it was modified (dirty) and data masking is enabled
if (this.state.isDataMaskingDirty && isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking)) { if (this.state.isDataMaskingDirty && isDataMaskingEnabled(this.collection.dataMaskingPolicy?.())) {
newCollection.dataMaskingPolicy = this.state.dataMaskingContent; newCollection.dataMaskingPolicy = this.state.dataMaskingContent;
} }
@@ -1326,6 +1331,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
vectorEmbeddingPolicyBaseline: this.state.vectorEmbeddingPolicyBaseline, vectorEmbeddingPolicyBaseline: this.state.vectorEmbeddingPolicyBaseline,
onVectorEmbeddingPolicyChange: this.onVectorEmbeddingPolicyChange, onVectorEmbeddingPolicyChange: this.onVectorEmbeddingPolicyChange,
onVectorEmbeddingPolicyDirtyChange: this.onVectorEmbeddingPolicyDirtyChange, onVectorEmbeddingPolicyDirtyChange: this.onVectorEmbeddingPolicyDirtyChange,
onVectorEmbeddingPolicyValidationChange: this.onVectorEmbeddingPolicyValidationChange,
isVectorSearchEnabled: this.isVectorSearchEnabled, isVectorSearchEnabled: this.isVectorSearchEnabled,
fullTextPolicy: this.state.fullTextPolicy, fullTextPolicy: this.state.fullTextPolicy,
fullTextPolicyBaseline: this.state.fullTextPolicyBaseline, fullTextPolicyBaseline: this.state.fullTextPolicyBaseline,
@@ -1463,15 +1469,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}); });
} }
// Check if DDM should be enabled if (isDataMaskingEnabled(this.collection.dataMaskingPolicy?.())) {
const shouldEnableDDM = (): boolean => {
const hasDataMaskingCapability = isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking);
const isSqlAccount = userContext.apiType === "SQL";
return isSqlAccount && hasDataMaskingCapability; // Only show for SQL accounts with DDM capability
};
if (shouldEnableDDM()) {
const dataMaskingComponentProps: DataMaskingComponentProps = { const dataMaskingComponentProps: DataMaskingComponentProps = {
shouldDiscardDataMasking: this.state.shouldDiscardDataMasking, shouldDiscardDataMasking: this.state.shouldDiscardDataMasking,
resetShouldDiscardDataMasking: this.resetShouldDiscardDataMasking, resetShouldDiscardDataMasking: this.resetShouldDiscardDataMasking,
@@ -1576,7 +1574,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
} }
> >
{this.shouldShowKeyspaceSharedThroughputMessage() && ( {this.shouldShowKeyspaceSharedThroughputMessage() && (
<div>This table shared throughput is configured at the keyspace</div> <div>{t(Keys.controls.settings.scale.keyspaceSharedThroughput)}</div>
)} )}
<div <div

View File

@@ -22,6 +22,7 @@ import {
Stack, Stack,
Text, Text,
} from "@fluentui/react"; } from "@fluentui/react";
import { Keys, t } from "Localization";
import * as React from "react"; import * as React from "react";
import { Urls } from "../../../Common/Constants"; import { Urls } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants"; import { StyleConstants } from "../../../Common/StyleConstants";
@@ -338,10 +339,12 @@ export const getEstimatedSpendingElement = (
const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : ""; const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : "";
return ( return (
<Stack> <Stack>
<Text style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>Cost estimate*</Text> <Text style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.costEstimate.title)}
</Text>
{costElement} {costElement}
<Text style={{ fontWeight: 600, marginTop: 15, color: "var(--colorNeutralForeground1)" }}> <Text style={{ fontWeight: 600, marginTop: 15, color: "var(--colorNeutralForeground1)" }}>
How we calculate this {t(Keys.controls.settings.costEstimate.howWeCalculate)}
</Text> </Text>
<Stack id="throughputSpendElement" style={{ marginTop: 5 }}> <Stack id="throughputSpendElement" style={{ marginTop: 5 }}>
<span> <span>
@@ -353,7 +356,8 @@ export const getEstimatedSpendingElement = (
</span> </span>
<span> <span>
{priceBreakdown.currencySign} {priceBreakdown.currencySign}
{priceBreakdown.pricePerRu}/RU {priceBreakdown.pricePerRu}
{t(Keys.controls.settings.costEstimate.perRu)}
</span> </span>
</Stack> </Stack>
<Text style={{ marginTop: 15, color: "var(--colorNeutralForeground1)" }}> <Text style={{ marginTop: 15, color: "var(--colorNeutralForeground1)" }}>
@@ -365,18 +369,16 @@ export const getEstimatedSpendingElement = (
export const manualToAutoscaleDisclaimerElement: JSX.Element = ( export const manualToAutoscaleDisclaimerElement: JSX.Element = (
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement"> <Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings {t(Keys.controls.settings.throughput.manualToAutoscaleDisclaimer)}{" "}
and storage of your resource. After autoscale has been enabled, you can change the max RU/s.{" "} <Link href={Urls.autoscaleMigration}>{t(Keys.common.learnMore)}</Link>
<Link href={Urls.autoscaleMigration}>Learn more</Link>
</Text> </Text>
); );
export const ttlWarning: JSX.Element = ( export const ttlWarning: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}> <Text styles={infoAndToolTipTextStyle}>
The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete {t(Keys.controls.settings.throughput.ttlWarningText)}{" "}
operation explicitly issued by a client application. For more information see,{" "}
<Link target="_blank" href="https://aka.ms/cosmos-db-ttl"> <Link target="_blank" href="https://aka.ms/cosmos-db-ttl">
Time to Live (TTL) in Azure Cosmos DB {t(Keys.controls.settings.throughput.ttlWarningLinkText)}
</Link> </Link>
. .
</Text> </Text>
@@ -384,29 +386,28 @@ export const ttlWarning: JSX.Element = (
export const unsavedEditorWarningMessage = (editor: editorType): JSX.Element => ( export const unsavedEditorWarningMessage = (editor: editorType): JSX.Element => (
<Text styles={infoAndToolTipTextStyle}> <Text styles={infoAndToolTipTextStyle}>
You have not saved the latest changes made to your{" "} {t(Keys.controls.settings.throughput.unsavedEditorWarningPrefix)}{" "}
{editor === "indexPolicy" {editor === "indexPolicy"
? "indexing policy" ? t(Keys.controls.settings.throughput.unsavedIndexingPolicy)
: editor === "dataMasking" : editor === "dataMasking"
? "data masking policy" ? t(Keys.controls.settings.throughput.unsavedDataMaskingPolicy)
: "computed properties"} : t(Keys.controls.settings.throughput.unsavedComputedProperties)}
. Please click save to confirm the changes. {t(Keys.controls.settings.throughput.unsavedEditorWarningSuffix)}
</Text> </Text>
); );
export const updateThroughputDelayedApplyWarningMessage: JSX.Element = ( export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage"> <Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
You are about to request an increase in throughput beyond the pre-allocated capacity. This operation will take some {t(Keys.controls.settings.throughput.updateDelayedApplyWarning)}
time to complete.
</Text> </Text>
); );
export const getUpdateThroughputBeyondInstantLimitMessage = (instantMaximumThroughput: number): JSX.Element => { export const getUpdateThroughputBeyondInstantLimitMessage = (instantMaximumThroughput: number): JSX.Element => {
return ( return (
<Text id="updateThroughputDelayedApplyWarningMessage"> <Text id="updateThroughputDelayedApplyWarningMessage">
Scaling up will take 4-6 hours as it exceeds what Azure Cosmos DB can instantly support currently based on your {t(Keys.controls.settings.throughput.scalingUpDelayMessage, {
number of physical partitions. You can increase your throughput to {instantMaximumThroughput} instantly or proceed instantMaximumThroughput: String(instantMaximumThroughput),
with this value and wait until the scale-up is completed. })}
</Text> </Text>
); );
}; };
@@ -418,22 +419,26 @@ export const getUpdateThroughputBeyondSupportLimitMessage = (
return ( return (
<> <>
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage"> <Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
Your request to increase throughput exceeds the pre-allocated capacity which may take longer than expected. {t(Keys.controls.settings.throughput.exceedPreAllocatedMessage)}
There are three options you can choose from to proceed:
</Text> </Text>
<ol style={{ fontSize: 14, color: "var(--colorNeutralForeground1)", marginTop: "5px" }}> <ol style={{ fontSize: 14, color: "var(--colorNeutralForeground1)", marginTop: "5px" }}>
<li>You can instantly scale up to {instantMaximumThroughput} RU/s.</li> <li>
{t(Keys.controls.settings.throughput.instantScaleOption, {
instantMaximumThroughput: String(instantMaximumThroughput),
})}
</li>
{instantMaximumThroughput < maximumThroughput && ( {instantMaximumThroughput < maximumThroughput && (
<li>You can asynchronously scale up to any value under {maximumThroughput} RU/s in 4-6 hours.</li> <li>
{t(Keys.controls.settings.throughput.asyncScaleOption, { maximumThroughput: String(maximumThroughput) })}
</li>
)} )}
<li> <li>
Your current quota max is {maximumThroughput} RU/s. To go over this limit, you must request a quota increase {t(Keys.controls.settings.throughput.quotaMaxOption, { maximumThroughput: String(maximumThroughput) })}
and the Azure Cosmos DB team will review.
<Link <Link
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase" href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase"
target="_blank" target="_blank"
> >
Learn more {t(Keys.common.learnMore)}
</Link> </Link>
</li> </li>
</ol> </ol>
@@ -444,23 +449,19 @@ export const getUpdateThroughputBeyondSupportLimitMessage = (
export const getUpdateThroughputBelowMinimumMessage = (minimum: number): JSX.Element => { export const getUpdateThroughputBelowMinimumMessage = (minimum: number): JSX.Element => {
return ( return (
<Text styles={infoAndToolTipTextStyle}> <Text styles={infoAndToolTipTextStyle}>
You are not able to lower throughput below your current minimum of {minimum} RU/s. For more information on this {t(Keys.controls.settings.throughput.belowMinimumMessage, { minimum: String(minimum) })}
limit, please refer to our service quote documentation.
<Link <Link
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits" href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank" target="_blank"
> >
Learn more {t(Keys.common.learnMore)}
</Link> </Link>
</Text> </Text>
); );
}; };
export const saveThroughputWarningMessage: JSX.Element = ( export const saveThroughputWarningMessage: JSX.Element = (
<Text> <Text>{t(Keys.controls.settings.throughput.saveThroughputWarning)}</Text>
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below
before saving your changes
</Text>
); );
const getCurrentThroughput = ( const getCurrentThroughput = (
@@ -472,23 +473,29 @@ const getCurrentThroughput = (
if (targetThroughput) { if (targetThroughput) {
if (throughput) { if (throughput) {
return isAutoscale return isAutoscale
? `, Current autoscale throughput: ${Math.round( ? `, ${t(Keys.controls.settings.throughput.currentAutoscaleThroughput)} ${Math.round(
throughput / 10, throughput / 10,
)} - ${throughput} ${throughputUnit}, Target autoscale throughput: ${Math.round( )} - ${throughput} ${throughputUnit}, ${t(
targetThroughput / 10, Keys.controls.settings.throughput.targetAutoscaleThroughput,
)} - ${targetThroughput} ${throughputUnit}` )} ${Math.round(targetThroughput / 10)} - ${targetThroughput} ${throughputUnit}`
: `, Current manual throughput: ${throughput} ${throughputUnit}, Target manual throughput: ${targetThroughput}`; : `, ${t(Keys.controls.settings.throughput.currentManualThroughput)} ${throughput} ${throughputUnit}, ${t(
Keys.controls.settings.throughput.targetManualThroughput,
)} ${targetThroughput}`;
} else { } else {
return isAutoscale return isAutoscale
? `, Target autoscale throughput: ${Math.round(targetThroughput / 10)} - ${targetThroughput} ${throughputUnit}` ? `, ${t(Keys.controls.settings.throughput.targetAutoscaleThroughput)} ${Math.round(
: `, Target manual throughput: ${targetThroughput} ${throughputUnit}`; targetThroughput / 10,
)} - ${targetThroughput} ${throughputUnit}`
: `, ${t(Keys.controls.settings.throughput.targetManualThroughput)} ${targetThroughput} ${throughputUnit}`;
} }
} }
if (!targetThroughput && throughput) { if (!targetThroughput && throughput) {
return isAutoscale return isAutoscale
? `, Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} ${throughputUnit}` ? `, ${t(Keys.controls.settings.throughput.currentAutoscaleThroughput)} ${Math.round(
: `, Current manual throughput: ${throughput} ${throughputUnit}`; throughput / 10,
)} - ${throughput} ${throughputUnit}`
: `, ${t(Keys.controls.settings.throughput.currentManualThroughput)} ${throughput} ${throughputUnit}`;
} }
return ""; return "";
@@ -503,10 +510,10 @@ export const getThroughputApplyDelayedMessage = (
requestedThroughput: number, requestedThroughput: number,
): JSX.Element => ( ): JSX.Element => (
<Text styles={infoAndToolTipTextStyle}> <Text styles={infoAndToolTipTextStyle}>
The request to increase the throughput has successfully been submitted. This operation will take 1-3 business days {t(Keys.controls.settings.throughput.applyDelayedMessage)}
to complete. View the latest status in Notifications.
<br /> <br />
Database: {databaseName}, Container: {collectionName}{" "} {t(Keys.controls.settings.throughput.databaseLabel)} {databaseName},{" "}
{t(Keys.controls.settings.throughput.containerLabel)} {collectionName}{" "}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)} {getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
</Text> </Text>
); );
@@ -519,9 +526,13 @@ export const getThroughputApplyShortDelayMessage = (
collectionName: string, collectionName: string,
): JSX.Element => ( ): JSX.Element => (
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage"> <Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
A request to increase the throughput is currently in progress. This operation will take some time to complete. {t(Keys.controls.settings.throughput.applyShortDelayMessage)}
<br /> <br />
{collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `} {collectionName
? `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName}, ${t(
Keys.controls.settings.throughput.containerLabel,
)} ${collectionName} `
: `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName} `}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)} {getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
</Text> </Text>
); );
@@ -535,10 +546,13 @@ export const getThroughputApplyLongDelayMessage = (
requestedThroughput: number, requestedThroughput: number,
): JSX.Element => ( ): JSX.Element => (
<Text styles={infoAndToolTipTextStyle} id="throughputApplyLongDelayMessage"> <Text styles={infoAndToolTipTextStyle} id="throughputApplyLongDelayMessage">
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to {t(Keys.controls.settings.throughput.applyLongDelayMessage)}
complete. View the latest status in Notifications.
<br /> <br />
{collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `} {collectionName
? `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName}, ${t(
Keys.controls.settings.throughput.containerLabel,
)} ${collectionName} `
: `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName} `}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)} {getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
</Text> </Text>
); );
@@ -547,63 +561,49 @@ export const getToolTipContainer = (content: string | JSX.Element): JSX.Element
content ? <Text styles={infoAndToolTipTextStyle}>{content}</Text> : undefined; content ? <Text styles={infoAndToolTipTextStyle}>{content}</Text> : undefined;
export const conflictResolutionLwwTooltip: JSX.Element = ( export const conflictResolutionLwwTooltip: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}> <Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.conflictResolution.lwwTooltip)}</Text>
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 = ( export const conflictResolutionCustomToolTip: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}> <Text styles={infoAndToolTipTextStyle}>
Gets or sets the name of a stored procedure (aka merge procedure) for resolving the conflicts. You can write {t(Keys.controls.settings.conflictResolution.customTooltip)}
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"> <Link className="linkDarkBackground" href="https://aka.ms/dataexplorerconflics" target="_blank">
{` conflicts feed`} {t(Keys.controls.settings.conflictResolution.customTooltipConflictsFeed)}
</Link> </Link>
. You can update/re-register the stored procedure at any time. {t(Keys.controls.settings.conflictResolution.customTooltipSuffix)}
</Text> </Text>
); );
export const changeFeedPolicyToolTip: JSX.Element = ( export const changeFeedPolicyToolTip: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}> <Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.changeFeed.tooltip)}</Text>
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 mongoIndexingPolicyDisclaimer: JSX.Element = ( export const mongoIndexingPolicyDisclaimer: JSX.Element = (
<Text style={{ color: "var(--colorNeutralForeground1)" }}> <Text style={{ color: "var(--colorNeutralForeground1)" }}>
For queries that filter on multiple properties, create multiple single field indexes instead of a compound index. {t(Keys.controls.settings.mongoIndexing.disclaimer)}
<Link <Link
href="https://docs.microsoft.com/azure/cosmos-db/mongodb-indexing#index-types" href="https://docs.microsoft.com/azure/cosmos-db/mongodb-indexing#index-types"
target="_blank" target="_blank"
style={{ color: "var(--colorBrandForeground1)" }} style={{ color: "var(--colorBrandForeground1)" }}
> >
{` Compound indexes `} {t(Keys.controls.settings.mongoIndexing.disclaimerCompoundIndexesLink)}
</Link> </Link>
are only used for sorting query results. If you need to add a compound index, you can create one using the Mongo {t(Keys.controls.settings.mongoIndexing.disclaimerSuffix)}
shell.
</Text> </Text>
); );
export const mongoCompoundIndexNotSupportedMessage: JSX.Element = ( export const mongoCompoundIndexNotSupportedMessage: JSX.Element = (
<Text style={{ color: "var(--colorNeutralForeground1)" }}> <Text style={{ color: "var(--colorNeutralForeground1)" }}>
Collections with compound indexes are not yet supported in the indexing editor. To modify indexing policy for this {t(Keys.controls.settings.mongoIndexing.compoundNotSupported)}
collection, use the Mongo Shell.
</Text> </Text>
); );
export const mongoIndexingPolicyAADError: JSX.Element = ( export const mongoIndexingPolicyAADError: JSX.Element = (
<MessageBar messageBarType={MessageBarType.error}> <MessageBar messageBarType={MessageBarType.error}>
<Text> <Text>
To use the indexing policy editor, please login to the {t(Keys.controls.settings.mongoIndexing.aadError)}
<Link target="_blank" href="https://portal.azure.com"> <Link target="_blank" href="https://portal.azure.com">
{"azure portal."} {t(Keys.controls.settings.mongoIndexing.aadErrorLink)}
</Link> </Link>
</Text> </Text>
</MessageBar> </MessageBar>
@@ -611,7 +611,7 @@ export const mongoIndexingPolicyAADError: JSX.Element = (
export const mongoIndexTransformationRefreshingMessage: JSX.Element = ( export const mongoIndexTransformationRefreshingMessage: JSX.Element = (
<Stack horizontal {...mongoWarningStackProps}> <Stack horizontal {...mongoWarningStackProps}>
<Text styles={infoAndToolTipTextStyle}>Refreshing index transformation progress</Text> <Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.mongoIndexing.refreshingProgress)}</Text>
<Spinner size={SpinnerSize.small} /> <Spinner size={SpinnerSize.small} />
</Stack> </Stack>
); );
@@ -623,15 +623,18 @@ export const renderMongoIndexTransformationRefreshMessage = (
if (progress === 0) { if (progress === 0) {
return ( return (
<Text styles={infoAndToolTipTextStyle}> <Text styles={infoAndToolTipTextStyle}>
{"You can make more indexing changes once the current index transformation is complete. "} {t(Keys.controls.settings.mongoIndexing.canMakeMoreChangesZero)}
<Link onClick={performRefresh}>{"Refresh to check if it has completed."}</Link> <Link onClick={performRefresh}>{t(Keys.controls.settings.mongoIndexing.refreshToCheck)}</Link>
</Text> </Text>
); );
} else { } else {
return ( return (
<Text styles={infoAndToolTipTextStyle}> <Text styles={infoAndToolTipTextStyle}>
{`You can make more indexing changes once the current index transformation has completed. It is ${progress}% complete. `} {`${t(Keys.controls.settings.mongoIndexing.canMakeMoreChangesProgress).replace(
<Link onClick={performRefresh}>{"Refresh to check the progress."}</Link> "{{progress}}",
String(progress),
)} `}
<Link onClick={performRefresh}>{t(Keys.controls.settings.mongoIndexing.refreshToCheckProgress)}</Link>
</Text> </Text>
); );
} }

View File

@@ -4,6 +4,7 @@ import { titleAndInputStackProps, unsavedEditorWarningMessage } from "Explorer/C
import { isDirty } from "Explorer/Controls/Settings/SettingsUtils"; import { isDirty } from "Explorer/Controls/Settings/SettingsUtils";
import { loadMonaco } from "Explorer/LazyMonaco"; import { loadMonaco } from "Explorer/LazyMonaco";
import { monacoTheme, useThemeStore } from "hooks/useTheme"; import { monacoTheme, useThemeStore } from "hooks/useTheme";
import { Keys, t } from "Localization";
import * as monaco from "monaco-editor"; import * as monaco from "monaco-editor";
import * as React from "react"; import * as React from "react";
export interface ComputedPropertiesComponentProps { export interface ComputedPropertiesComponentProps {
@@ -107,7 +108,7 @@ export class ComputedPropertiesComponent extends React.Component<
this.computedPropertiesEditor = monaco.editor.create(this.computedPropertiesDiv.current, { this.computedPropertiesEditor = monaco.editor.create(this.computedPropertiesDiv.current, {
value: value, value: value,
language: "json", language: "json",
ariaLabel: "Computed properties", ariaLabel: t(Keys.controls.settings.computedProperties.ariaLabel),
theme: monacoTheme(), theme: monacoTheme(),
}); });
if (this.computedPropertiesEditor) { if (this.computedPropertiesEditor) {
@@ -151,9 +152,9 @@ export class ComputedPropertiesComponent extends React.Component<
)} )}
<Text style={{ marginLeft: "30px", marginBottom: "10px", color: "var(--colorNeutralForeground1)" }}> <Text style={{ marginLeft: "30px", marginBottom: "10px", color: "var(--colorNeutralForeground1)" }}>
<Link target="_blank" href="https://aka.ms/computed-properties-preview/"> <Link target="_blank" href="https://aka.ms/computed-properties-preview/">
{"Learn more"} <FontIcon iconName="NavigateExternalInline" /> {t(Keys.common.learnMore)} <FontIcon iconName="NavigateExternalInline" />
</Link> </Link>
&#160; about how to define computed properties and how to use them. &#160; {t(Keys.controls.settings.computedProperties.learnMorePrefix)}
</Text> </Text>
<div <div
className="settingsV2Editor" className="settingsV2Editor"

View File

@@ -2,6 +2,7 @@ import { ChoiceGroup, IChoiceGroupOption, ITextFieldProps, Stack, TextField } fr
import * as React from "react"; import * as React from "react";
import * as DataModels from "../../../../Contracts/DataModels"; import * as DataModels from "../../../../Contracts/DataModels";
import * as ViewModels from "../../../../Contracts/ViewModels"; import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys, t } from "Localization";
import { import {
conflictResolutionCustomToolTip, conflictResolutionCustomToolTip,
conflictResolutionLwwTooltip, conflictResolutionLwwTooltip,
@@ -32,9 +33,12 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
private conflictResolutionChoiceGroupOptions: IChoiceGroupOption[] = [ private conflictResolutionChoiceGroupOptions: IChoiceGroupOption[] = [
{ {
key: DataModels.ConflictResolutionMode.LastWriterWins, key: DataModels.ConflictResolutionMode.LastWriterWins,
text: "Last Write Wins (default)", text: t(Keys.controls.settings.conflictResolution.lwwDefault),
},
{
key: DataModels.ConflictResolutionMode.Custom,
text: t(Keys.controls.settings.conflictResolution.customMergeProcedure),
}, },
{ key: DataModels.ConflictResolutionMode.Custom, text: "Merge Procedure (custom)" },
]; ];
componentDidMount(): void { componentDidMount(): void {
@@ -85,7 +89,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
private getConflictResolutionModeComponent = (): JSX.Element => ( private getConflictResolutionModeComponent = (): JSX.Element => (
<ChoiceGroup <ChoiceGroup
label="Mode" label={t(Keys.controls.settings.conflictResolution.mode)}
selectedKey={this.props.conflictResolutionPolicyMode} selectedKey={this.props.conflictResolutionPolicyMode}
options={this.conflictResolutionChoiceGroupOptions} options={this.conflictResolutionChoiceGroupOptions}
onChange={this.onConflictResolutionPolicyModeChange} onChange={this.onConflictResolutionPolicyModeChange}
@@ -103,7 +107,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
private getConflictResolutionLWWComponent = (): JSX.Element => ( private getConflictResolutionLWWComponent = (): JSX.Element => (
<TextField <TextField
id="conflictResolutionLwwTextField" id="conflictResolutionLwwTextField"
label={"Conflict Resolver Property"} label={t(Keys.controls.settings.conflictResolution.conflictResolverProperty)}
onRenderLabel={this.onRenderLwwComponentTextField} onRenderLabel={this.onRenderLwwComponentTextField}
styles={{ styles={{
fieldGroup: { fieldGroup: {
@@ -158,7 +162,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
return ( return (
<TextField <TextField
id="conflictResolutionCustomTextField" id="conflictResolutionCustomTextField"
label="Stored procedure" label={t(Keys.controls.settings.conflictResolution.storedProcedure)}
onRenderLabel={this.onRenderCustomComponentTextField} onRenderLabel={this.onRenderCustomComponentTextField}
styles={{ styles={{
fieldGroup: { fieldGroup: {

View File

@@ -7,6 +7,7 @@ import {
import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils"; import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils";
import { ContainerPolicyTabTypes, isDirty } from "Explorer/Controls/Settings/SettingsUtils"; import { ContainerPolicyTabTypes, isDirty } from "Explorer/Controls/Settings/SettingsUtils";
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent"; import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
export interface ContainerPolicyComponentProps { export interface ContainerPolicyComponentProps {
@@ -14,6 +15,7 @@ export interface ContainerPolicyComponentProps {
vectorEmbeddingPolicyBaseline: VectorEmbeddingPolicy; vectorEmbeddingPolicyBaseline: VectorEmbeddingPolicy;
onVectorEmbeddingPolicyChange: (newVectorEmbeddingPolicy: VectorEmbeddingPolicy) => void; onVectorEmbeddingPolicyChange: (newVectorEmbeddingPolicy: VectorEmbeddingPolicy) => void;
onVectorEmbeddingPolicyDirtyChange: (isVectorEmbeddingPolicyDirty: boolean) => void; onVectorEmbeddingPolicyDirtyChange: (isVectorEmbeddingPolicyDirty: boolean) => void;
onVectorEmbeddingPolicyValidationChange: (isValid: boolean) => void;
isVectorSearchEnabled: boolean; isVectorSearchEnabled: boolean;
fullTextPolicy: FullTextPolicy; fullTextPolicy: FullTextPolicy;
fullTextPolicyBaseline: FullTextPolicy; fullTextPolicyBaseline: FullTextPolicy;
@@ -30,6 +32,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
vectorEmbeddingPolicyBaseline, vectorEmbeddingPolicyBaseline,
onVectorEmbeddingPolicyChange, onVectorEmbeddingPolicyChange,
onVectorEmbeddingPolicyDirtyChange, onVectorEmbeddingPolicyDirtyChange,
onVectorEmbeddingPolicyValidationChange,
isVectorSearchEnabled, isVectorSearchEnabled,
fullTextPolicy, fullTextPolicy,
fullTextPolicyBaseline, fullTextPolicyBaseline,
@@ -42,8 +45,12 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
const [selectedTab, setSelectedTab] = React.useState<ContainerPolicyTabTypes>( const [selectedTab, setSelectedTab] = React.useState<ContainerPolicyTabTypes>(
ContainerPolicyTabTypes.VectorPolicyTab, ContainerPolicyTabTypes.VectorPolicyTab,
); );
const [vectorEmbeddings, setVectorEmbeddings] = React.useState<VectorEmbedding[]>(); const [vectorEmbeddings, setVectorEmbeddings] = React.useState<VectorEmbedding[]>(
const [vectorEmbeddingsBaseline, setVectorEmbeddingsBaseline] = React.useState<VectorEmbedding[]>(); vectorEmbeddingPolicy?.vectorEmbeddings ?? [],
);
const [vectorEmbeddingsBaseline, setVectorEmbeddingsBaseline] = React.useState<VectorEmbedding[]>(
vectorEmbeddingPolicyBaseline?.vectorEmbeddings ?? [],
);
const [discardVectorChanges, setDiscardVectorChanges] = React.useState<boolean>(false); const [discardVectorChanges, setDiscardVectorChanges] = React.useState<boolean>(false);
const [fullTextSearchPolicy, setFullTextSearchPolicy] = React.useState<FullTextPolicy>(); const [fullTextSearchPolicy, setFullTextSearchPolicy] = React.useState<FullTextPolicy>();
const [fullTextSearchPolicyBaseline, setFullTextSearchPolicyBaseline] = React.useState<FullTextPolicy>(); const [fullTextSearchPolicyBaseline, setFullTextSearchPolicyBaseline] = React.useState<FullTextPolicy>();
@@ -52,7 +59,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
React.useEffect(() => { React.useEffect(() => {
setVectorEmbeddings(vectorEmbeddingPolicy?.vectorEmbeddings); setVectorEmbeddings(vectorEmbeddingPolicy?.vectorEmbeddings);
setVectorEmbeddingsBaseline(vectorEmbeddingPolicyBaseline?.vectorEmbeddings); setVectorEmbeddingsBaseline(vectorEmbeddingPolicyBaseline?.vectorEmbeddings);
}, [vectorEmbeddingPolicy]); }, [vectorEmbeddingPolicy, vectorEmbeddingPolicyBaseline]);
React.useEffect(() => { React.useEffect(() => {
setFullTextSearchPolicy(fullTextPolicy); setFullTextSearchPolicy(fullTextPolicy);
@@ -69,12 +76,15 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
} }
}); });
const checkAndSendVectorEmbeddingPoliciesToSettings = (newVectorEmbeddings: VectorEmbedding[]): void => { const checkAndSendVectorEmbeddingPoliciesToSettings = (
if (isDirty(newVectorEmbeddings, vectorEmbeddingsBaseline)) { newVectorEmbeddings: VectorEmbedding[],
onVectorEmbeddingPolicyDirtyChange(true); validationPassed: boolean,
): void => {
onVectorEmbeddingPolicyValidationChange(validationPassed);
const isVectorDirty: boolean = isDirty(newVectorEmbeddings, vectorEmbeddingsBaseline);
onVectorEmbeddingPolicyDirtyChange(isVectorDirty);
if (isVectorDirty) {
onVectorEmbeddingPolicyChange({ vectorEmbeddings: newVectorEmbeddings }); onVectorEmbeddingPolicyChange({ vectorEmbeddings: newVectorEmbeddings });
} else {
resetShouldDiscardContainerPolicyChange();
} }
}; };
@@ -153,21 +163,21 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
<PivotItem <PivotItem
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.VectorPolicyTab]} itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.VectorPolicyTab]}
style={{ marginTop: 20, color: "var(--colorNeutralForeground1)" }} style={{ marginTop: 20, color: "var(--colorNeutralForeground1)" }}
headerText="Vector Policy" headerText={t(Keys.controls.settings.containerPolicy.vectorPolicy)}
> >
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}> <Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
{vectorEmbeddings && ( <VectorEmbeddingPoliciesComponent
<VectorEmbeddingPoliciesComponent vectorEmbeddingsBaseline={vectorEmbeddingsBaseline}
disabled={true} vectorEmbeddings={vectorEmbeddings}
vectorEmbeddings={vectorEmbeddings} vectorIndexes={undefined}
vectorIndexes={undefined} onVectorEmbeddingChange={(
onVectorEmbeddingChange={(vectorEmbeddings: VectorEmbedding[]) => vectorEmbeddings: VectorEmbedding[],
checkAndSendVectorEmbeddingPoliciesToSettings(vectorEmbeddings) _vectorIndexingPolicies,
} validationPassed: boolean,
discardChanges={discardVectorChanges} ) => checkAndSendVectorEmbeddingPoliciesToSettings(vectorEmbeddings, validationPassed)}
onChangesDiscarded={onVectorChangesDiscarded} discardChanges={discardVectorChanges}
/> onChangesDiscarded={onVectorChangesDiscarded}
)} />
</Stack> </Stack>
</PivotItem> </PivotItem>
)} )}
@@ -175,7 +185,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
<PivotItem <PivotItem
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.FullTextPolicyTab]} itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.FullTextPolicyTab]}
style={{ marginTop: 20, color: "var(--colorNeutralForeground1)" }} style={{ marginTop: 20, color: "var(--colorNeutralForeground1)" }}
headerText="Full Text Policy" headerText={t(Keys.controls.settings.containerPolicy.fullTextPolicy)}
> >
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}> <Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
{fullTextSearchPolicy ? ( {fullTextSearchPolicy ? (
@@ -218,7 +228,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
}); });
}} }}
> >
Create new full text search policy {t(Keys.controls.settings.containerPolicy.createFullTextPolicy)}
</DefaultButton> </DefaultButton>
)} )}
</Stack> </Stack>

View File

@@ -53,7 +53,6 @@ describe("DataMaskingComponent", () => {
}, },
], ],
excludedPaths: [], excludedPaths: [],
isPolicyEnabled: false,
}; };
let changeContentCallback: () => void; let changeContentCallback: () => void;
@@ -78,7 +77,7 @@ describe("DataMaskingComponent", () => {
<DataMaskingComponent <DataMaskingComponent
{...mockProps} {...mockProps}
dataMaskingContent={samplePolicy} dataMaskingContent={samplePolicy}
dataMaskingContentBaseline={{ ...samplePolicy, isPolicyEnabled: true }} dataMaskingContentBaseline={{ ...samplePolicy, excludedPaths: ["/excluded"] }}
/>, />,
); );
@@ -123,7 +122,7 @@ describe("DataMaskingComponent", () => {
}); });
it("resets content when shouldDiscardDataMasking is true", async () => { it("resets content when shouldDiscardDataMasking is true", async () => {
const baselinePolicy = { ...samplePolicy, isPolicyEnabled: true }; const baselinePolicy = { ...samplePolicy, excludedPaths: ["/excluded"] };
const wrapper = mount( const wrapper = mount(
<DataMaskingComponent <DataMaskingComponent
@@ -159,7 +158,7 @@ describe("DataMaskingComponent", () => {
wrapper.update(); wrapper.update();
// Update baseline to trigger componentDidUpdate // Update baseline to trigger componentDidUpdate
const newBaseline = { ...samplePolicy, isPolicyEnabled: true }; const newBaseline = { ...samplePolicy, excludedPaths: ["/excluded"] };
wrapper.setProps({ dataMaskingContentBaseline: newBaseline }); wrapper.setProps({ dataMaskingContentBaseline: newBaseline });
expect(mockProps.onDataMaskingDirtyChange).toHaveBeenCalledWith(true); expect(mockProps.onDataMaskingDirtyChange).toHaveBeenCalledWith(true);
@@ -174,7 +173,6 @@ describe("DataMaskingComponent", () => {
const invalidPolicy: Record<string, unknown> = { const invalidPolicy: Record<string, unknown> = {
includedPaths: "not an array", includedPaths: "not an array",
excludedPaths: [] as string[], excludedPaths: [] as string[],
isPolicyEnabled: "not a boolean",
}; };
mockGetValue.mockReturnValue(JSON.stringify(invalidPolicy)); mockGetValue.mockReturnValue(JSON.stringify(invalidPolicy));
@@ -197,7 +195,7 @@ describe("DataMaskingComponent", () => {
wrapper.update(); wrapper.update();
// First change // First change
const modifiedPolicy1 = { ...samplePolicy, isPolicyEnabled: true }; const modifiedPolicy1 = { ...samplePolicy, excludedPaths: ["/path1"] };
mockGetValue.mockReturnValue(JSON.stringify(modifiedPolicy1)); mockGetValue.mockReturnValue(JSON.stringify(modifiedPolicy1));
changeContentCallback(); changeContentCallback();
expect(mockProps.onDataMaskingDirtyChange).toHaveBeenCalledWith(true); expect(mockProps.onDataMaskingDirtyChange).toHaveBeenCalledWith(true);

View File

@@ -1,12 +1,11 @@
import { MessageBar, MessageBarType, Stack } from "@fluentui/react"; import { MessageBar, MessageBarType, Stack } from "@fluentui/react";
import * as monaco from "monaco-editor"; import * as monaco from "monaco-editor";
import * as React from "react"; import * as React from "react";
import * as Constants from "../../../../Common/Constants";
import * as DataModels from "../../../../Contracts/DataModels"; import * as DataModels from "../../../../Contracts/DataModels";
import { isCapabilityEnabled } from "../../../../Utils/CapabilityUtils"; import { Keys, t } from "Localization";
import { loadMonaco } from "../../../LazyMonaco"; import { loadMonaco } from "../../../LazyMonaco";
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils"; import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
import { isDirty as isContentDirty } from "../SettingsUtils"; import { isDirty as isContentDirty, isDataMaskingEnabled } from "../SettingsUtils";
export interface DataMaskingComponentProps { export interface DataMaskingComponentProps {
shouldDiscardDataMasking: boolean; shouldDiscardDataMasking: boolean;
@@ -24,16 +23,8 @@ interface DataMaskingComponentState {
} }
const emptyDataMaskingPolicy: DataModels.DataMaskingPolicy = { const emptyDataMaskingPolicy: DataModels.DataMaskingPolicy = {
includedPaths: [ includedPaths: [],
{
path: "/",
strategy: "Default",
startPosition: 0,
length: -1,
},
],
excludedPaths: [], excludedPaths: [],
isPolicyEnabled: true,
}; };
export class DataMaskingComponent extends React.Component<DataMaskingComponentProps, DataMaskingComponentState> { export class DataMaskingComponent extends React.Component<DataMaskingComponentProps, DataMaskingComponentState> {
@@ -99,7 +90,7 @@ export class DataMaskingComponent extends React.Component<DataMaskingComponentPr
value: value, value: value,
language: "json", language: "json",
automaticLayout: true, automaticLayout: true,
ariaLabel: "Data Masking Policy", ariaLabel: t(Keys.controls.settings.dataMasking.ariaLabel),
fontSize: 13, fontSize: 13,
minimap: { enabled: false }, minimap: { enabled: false },
wordWrap: "off", wordWrap: "off",
@@ -140,7 +131,7 @@ export class DataMaskingComponent extends React.Component<DataMaskingComponentPr
}; };
public render(): JSX.Element { public render(): JSX.Element {
if (!isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking)) { if (!isDataMaskingEnabled(this.props.dataMaskingContent)) {
return null; return null;
} }
@@ -152,7 +143,7 @@ export class DataMaskingComponent extends React.Component<DataMaskingComponentPr
)} )}
{this.props.validationErrors.length > 0 && ( {this.props.validationErrors.length > 0 && (
<MessageBar messageBarType={MessageBarType.error}> <MessageBar messageBarType={MessageBarType.error}>
Validation failed: {this.props.validationErrors.join(", ")} {t(Keys.controls.settings.dataMasking.validationFailed)} {this.props.validationErrors.join(", ")}
</MessageBar> </MessageBar>
)} )}
<div className="settingsV2Editor" tabIndex={0} ref={this.dataMaskingDiv}></div> <div className="settingsV2Editor" tabIndex={0} ref={this.dataMaskingDiv}></div>

View File

@@ -2,6 +2,7 @@ import { FontIcon, Link, Stack, Text } from "@fluentui/react";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import React from "react"; import React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels"; import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys, t } from "Localization";
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent"; import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent"; import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
@@ -21,7 +22,9 @@ export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexCompone
<Stack tokens={{ childrenGap: 8 }} styles={{ root: { maxWidth: 600 } }}> <Stack tokens={{ childrenGap: 8 }} styles={{ root: { maxWidth: 600 } }}>
<Stack horizontal verticalAlign="center" wrap tokens={{ childrenGap: 8 }}> <Stack horizontal verticalAlign="center" wrap tokens={{ childrenGap: 8 }}>
{isSourceContainer && ( {isSourceContainer && (
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following indexes defined for it.</Text> <Text styles={{ root: { fontWeight: 600 } }}>
{t(Keys.controls.settings.globalSecondaryIndex.indexesDefined)}
</Text>
)} )}
<Text> <Text>
<Link <Link
@@ -31,7 +34,7 @@ export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexCompone
Learn more Learn more
<FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} /> <FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} />
</Link>{" "} </Link>{" "}
about how to define global secondary indexes and how to use them. {t(Keys.controls.settings.globalSecondaryIndex.learnMoreSuffix)}
</Text> </Text>
</Stack> </Stack>
{isSourceContainer && <GlobalSecondaryIndexSourceComponent collection={collection} explorer={explorer} />} {isSourceContainer && <GlobalSecondaryIndexSourceComponent collection={collection} explorer={explorer} />}

View File

@@ -9,6 +9,7 @@ import { useSidePanel } from "hooks/useSidePanel";
import * as monaco from "monaco-editor"; import * as monaco from "monaco-editor";
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import * as ViewModels from "../../../../Contracts/ViewModels"; import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys, t } from "Localization";
export interface GlobalSecondaryIndexSourceComponentProps { export interface GlobalSecondaryIndexSourceComponentProps {
collection: ViewModels.Collection; collection: ViewModels.Collection;
@@ -67,7 +68,7 @@ export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexS
editorRef.current = monacoInstance.editor.create(editorContainerRef.current, { editorRef.current = monacoInstance.editor.create(editorContainerRef.current, {
value: jsonValue, value: jsonValue,
language: "json", language: "json",
ariaLabel: "Global Secondary Index JSON", ariaLabel: t(Keys.controls.settings.globalSecondaryIndex.jsonAriaLabel),
readOnly: true, readOnly: true,
}); });
}; };
@@ -98,7 +99,7 @@ export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexS
}} }}
/> />
<PrimaryButton <PrimaryButton
text="Add index" text={t(Keys.controls.settings.globalSecondaryIndex.addIndex)}
styles={{ root: { width: "fit-content", marginTop: 12 } }} styles={{ root: { width: "fit-content", marginTop: 12 } }}
onClick={() => onClick={() =>
useSidePanel useSidePanel

View File

@@ -1,6 +1,7 @@
import { Stack, Text } from "@fluentui/react"; import { Stack, Text } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels"; import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys, t } from "Localization";
export interface GlobalSecondaryIndexTargetComponentProps { export interface GlobalSecondaryIndexTargetComponentProps {
collection: ViewModels.Collection; collection: ViewModels.Collection;
@@ -25,17 +26,21 @@ export const GlobalSecondaryIndexTargetComponent: React.FC<GlobalSecondaryIndexT
return ( return (
<Stack tokens={{ childrenGap: 15 }} styles={{ root: { maxWidth: 600 } }}> <Stack tokens={{ childrenGap: 15 }} styles={{ root: { maxWidth: 600 } }}>
<Text styles={textHeadingStyle}>Global Secondary Index Settings</Text> <Text styles={textHeadingStyle}>{t(Keys.controls.settings.globalSecondaryIndex.settingsTitle)}</Text>
<Stack tokens={{ childrenGap: 5 }}> <Stack tokens={{ childrenGap: 5 }}>
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text> <Text styles={{ root: { fontWeight: "600" } }}>
{t(Keys.controls.settings.globalSecondaryIndex.sourceContainer)}
</Text>
<Stack styles={valueBoxStyle}> <Stack styles={valueBoxStyle}>
<Text>{globalSecondaryIndexDefinition?.sourceCollectionId}</Text> <Text>{globalSecondaryIndexDefinition?.sourceCollectionId}</Text>
</Stack> </Stack>
</Stack> </Stack>
<Stack tokens={{ childrenGap: 5 }}> <Stack tokens={{ childrenGap: 5 }}>
<Text styles={{ root: { fontWeight: "600" } }}>Global secondary index definition</Text> <Text styles={{ root: { fontWeight: "600" } }}>
{t(Keys.controls.settings.globalSecondaryIndex.indexDefinition)}
</Text>
<Stack styles={valueBoxStyle}> <Stack styles={valueBoxStyle}>
<Text>{globalSecondaryIndexDefinition?.definition}</Text> <Text>{globalSecondaryIndexDefinition?.definition}</Text>
</Stack> </Stack>

View File

@@ -3,6 +3,7 @@ import { monacoTheme, useThemeStore } from "hooks/useTheme";
import * as monaco from "monaco-editor"; import * as monaco from "monaco-editor";
import * as React from "react"; import * as React from "react";
import * as DataModels from "../../../../Contracts/DataModels"; import * as DataModels from "../../../../Contracts/DataModels";
import { Keys, t } from "Localization";
import { loadMonaco } from "../../../LazyMonaco"; import { loadMonaco } from "../../../LazyMonaco";
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils"; import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
import { isDirty, isIndexTransforming } from "../SettingsUtils"; import { isDirty, isIndexTransforming } from "../SettingsUtils";
@@ -119,7 +120,7 @@ export class IndexingPolicyComponent extends React.Component<
value: value, value: value,
language: "json", language: "json",
readOnly: isIndexTransforming(this.props.indexTransformationProgress), readOnly: isIndexTransforming(this.props.indexTransformationProgress),
ariaLabel: "Indexing Policy", ariaLabel: t(Keys.controls.settings.indexingPolicy.ariaLabel),
theme: monacoTheme(), theme: monacoTheme(),
}); });
if (this.indexingPolicyEditor) { if (this.indexingPolicyEditor) {

View File

@@ -1,6 +1,7 @@
import { MessageBar, MessageBarType } from "@fluentui/react"; import { MessageBar, MessageBarType } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { handleError } from "../../../../../Common/ErrorHandlingUtils"; import { handleError } from "../../../../../Common/ErrorHandlingUtils";
import { Keys, t } from "Localization";
import { import {
mongoIndexTransformationRefreshingMessage, mongoIndexTransformationRefreshingMessage,
renderMongoIndexTransformationRefreshMessage, renderMongoIndexTransformationRefreshMessage,
@@ -46,7 +47,11 @@ export class IndexingPolicyRefreshComponent extends React.Component<
try { try {
await this.props.refreshIndexTransformationProgress(); await this.props.refreshIndexTransformationProgress();
} catch (error) { } catch (error) {
handleError(error, "RefreshIndexTransformationProgress", "Refreshing index transformation progress failed"); handleError(
error,
"RefreshIndexTransformationProgress",
t(Keys.controls.settings.indexingPolicyRefresh.refreshFailed),
);
} finally { } finally {
this.setState({ isRefreshing: false }); this.setState({ isRefreshing: false });
} }

View File

@@ -9,6 +9,7 @@ import {
IDropdownOption, IDropdownOption,
ITextField, ITextField,
} from "@fluentui/react"; } from "@fluentui/react";
import { Keys, t } from "Localization";
import { import {
addMongoIndexSubElementsTokens, addMongoIndexSubElementsTokens,
mongoErrorMessageStyles, mongoErrorMessageStyles,
@@ -66,7 +67,7 @@ export class AddMongoIndexComponent extends React.Component<AddMongoIndexCompone
<Stack {...mongoWarningStackProps}> <Stack {...mongoWarningStackProps}>
<Stack horizontal tokens={addMongoIndexSubElementsTokens}> <Stack horizontal tokens={addMongoIndexSubElementsTokens}>
<TextField <TextField
ariaLabel={"Index Field Name " + this.props.position} ariaLabel={t(Keys.controls.settings.mongoIndexing.indexFieldName) + " " + this.props.position}
disabled={this.props.disabled} disabled={this.props.disabled}
styles={shortWidthTextFieldStyles} styles={shortWidthTextFieldStyles}
componentRef={this.setRef} componentRef={this.setRef}
@@ -76,17 +77,17 @@ export class AddMongoIndexComponent extends React.Component<AddMongoIndexCompone
/> />
<Dropdown <Dropdown
ariaLabel={"Index Type " + this.props.position} ariaLabel={t(Keys.controls.settings.mongoIndexing.indexType) + " " + this.props.position}
disabled={this.props.disabled} disabled={this.props.disabled}
styles={shortWidthDropDownStyles} styles={shortWidthDropDownStyles}
placeholder="Select an index type" placeholder={t(Keys.controls.settings.mongoIndexing.selectIndexType)}
selectedKey={this.props.type} selectedKey={this.props.type}
options={this.indexTypes} options={this.indexTypes}
onChange={this.onTypeChange} onChange={this.onTypeChange}
/> />
<IconButton <IconButton
ariaLabel={"Undo Button " + this.props.position} ariaLabel={t(Keys.controls.settings.mongoIndexing.undoButton) + " " + this.props.position}
iconProps={{ iconName: "Undo" }} iconProps={{ iconName: "Undo" }}
disabled={!this.props.description && !this.props.type} disabled={!this.props.description && !this.props.type}
onClick={() => this.props.onDiscard()} onClick={() => this.props.onDiscard()}

View File

@@ -15,6 +15,7 @@ import {
} from "@fluentui/react"; } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { MongoIndex } from "../../../../../Utils/arm/generatedClients/cosmos/types"; import { MongoIndex } from "../../../../../Utils/arm/generatedClients/cosmos/types";
import { Keys, t } from "Localization";
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent"; import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
import { import {
addMongoIndexStackProps, addMongoIndexStackProps,
@@ -83,11 +84,25 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
}; };
private initialIndexesColumns: IColumn[] = [ private initialIndexesColumns: IColumn[] = [
{ key: "definition", name: "Definition", fieldName: "definition", minWidth: 100, maxWidth: 200, isResizable: true }, {
{ key: "type", name: "Type", fieldName: "type", minWidth: 100, maxWidth: 200, isResizable: true }, key: "definition",
name: t(Keys.controls.settings.mongoIndexing.definitionColumn),
fieldName: "definition",
minWidth: 100,
maxWidth: 200,
isResizable: true,
},
{
key: "type",
name: t(Keys.controls.settings.mongoIndexing.typeColumn),
fieldName: "type",
minWidth: 100,
maxWidth: 200,
isResizable: true,
},
{ {
key: "actionButton", key: "actionButton",
name: "Drop Index", name: t(Keys.controls.settings.mongoIndexing.dropIndexColumn),
fieldName: "actionButton", fieldName: "actionButton",
minWidth: 100, minWidth: 100,
maxWidth: 200, maxWidth: 200,
@@ -96,11 +111,25 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
]; ];
private indexesToBeDroppedColumns: IColumn[] = [ private indexesToBeDroppedColumns: IColumn[] = [
{ key: "definition", name: "Definition", fieldName: "definition", minWidth: 100, maxWidth: 200, isResizable: true }, {
{ key: "type", name: "Type", fieldName: "type", minWidth: 100, maxWidth: 200, isResizable: true }, key: "definition",
name: t(Keys.controls.settings.mongoIndexing.definitionColumn),
fieldName: "definition",
minWidth: 100,
maxWidth: 200,
isResizable: true,
},
{
key: "type",
name: t(Keys.controls.settings.mongoIndexing.typeColumn),
fieldName: "type",
minWidth: 100,
maxWidth: 200,
isResizable: true,
},
{ {
key: "actionButton", key: "actionButton",
name: "Add index back", name: t(Keys.controls.settings.mongoIndexing.addIndexBackColumn),
fieldName: "actionButton", fieldName: "actionButton",
minWidth: 100, minWidth: 100,
maxWidth: 200, maxWidth: 200,
@@ -161,7 +190,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
private getActionButton = (arrayPosition: number, isCurrentIndex: boolean): JSX.Element => { private getActionButton = (arrayPosition: number, isCurrentIndex: boolean): JSX.Element => {
return isCurrentIndex ? ( return isCurrentIndex ? (
<IconButton <IconButton
ariaLabel="Delete index Button" ariaLabel={t(Keys.controls.settings.mongoIndexing.deleteIndexButton)}
iconProps={{ iconName: "Delete" }} iconProps={{ iconName: "Delete" }}
disabled={isIndexTransforming(this.props.indexTransformationProgress)} disabled={isIndexTransforming(this.props.indexTransformationProgress)}
onClick={() => { onClick={() => {
@@ -170,7 +199,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
/> />
) : ( ) : (
<IconButton <IconButton
ariaLabel="Add back Index Button" ariaLabel={t(Keys.controls.settings.mongoIndexing.addBackIndexButton)}
iconProps={{ iconName: "Add" }} iconProps={{ iconName: "Add" }}
onClick={() => { onClick={() => {
this.props.onRevertIndexDrop(arrayPosition); this.props.onRevertIndexDrop(arrayPosition);
@@ -258,7 +287,10 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
return ( return (
<Stack {...createAndAddMongoIndexStackProps} styles={mediumWidthStackStyles}> <Stack {...createAndAddMongoIndexStackProps} styles={mediumWidthStackStyles}>
<CollapsibleSectionComponent title="Current index(es)" isExpandedByDefault={true}> <CollapsibleSectionComponent
title={t(Keys.controls.settings.mongoIndexing.currentIndexes)}
isExpandedByDefault={true}
>
{ {
<> <>
<DetailsList <DetailsList
@@ -285,7 +317,10 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
return ( return (
<Stack styles={mediumWidthStackStyles}> <Stack styles={mediumWidthStackStyles}>
<CollapsibleSectionComponent title="Index(es) to be dropped" isExpandedByDefault={true}> <CollapsibleSectionComponent
title={t(Keys.controls.settings.mongoIndexing.indexesToBeDropped)}
isExpandedByDefault={true}
>
{indexesToBeDropped.length > 0 && ( {indexesToBeDropped.length > 0 && (
<DetailsList <DetailsList
styles={customDetailsListStyles} styles={customDetailsListStyles}

View File

@@ -18,6 +18,7 @@ import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/da
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane"; import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
import { Keys, t } from "Localization";
import { import {
CosmosSqlDataTransferDataSourceSink, CosmosSqlDataTransferDataSourceSink,
DataTransferJobGetResults, DataTransferJobGetResults,
@@ -80,7 +81,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
return (collection.partitionKeyProperties || []).map((property) => "/" + property).join(", "); return (collection.partitionKeyProperties || []).map((property) => "/" + property).join(", ");
}; };
const partitionKeyName = "Partition key"; const partitionKeyName = t(Keys.controls.settings.partitionKey.partitionKey);
const partitionKeyValue = getPartitionKeyValue(); const partitionKeyValue = getPartitionKeyValue();
const textHeadingStyle = { const textHeadingStyle = {
@@ -148,22 +149,28 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
const getProgressDescription = (): string => { const getProgressDescription = (): string => {
const processedCount = portalDataTransferJob?.properties?.processedCount; const processedCount = portalDataTransferJob?.properties?.processedCount;
const totalCount = portalDataTransferJob?.properties?.totalCount; const totalCount = portalDataTransferJob?.properties?.totalCount;
const processedCountString = totalCount > 0 ? `(${processedCount} of ${totalCount} documents processed)` : ""; const processedCountString =
totalCount > 0
? t(Keys.controls.settings.partitionKeyEditor.documentsProcessed, {
processedCount: String(processedCount),
totalCount: String(totalCount),
})
: "";
return `${portalDataTransferJob?.properties?.status} ${processedCountString}`; return `${portalDataTransferJob?.properties?.status} ${processedCountString}`;
}; };
const startPartitionkeyChangeWorkflow = () => { const startPartitionkeyChangeWorkflow = () => {
useSidePanel useSidePanel.getState().openSidePanel(
.getState() t(Keys.controls.settings.partitionKeyEditor.changePartitionKey, {
.openSidePanel( partitionKeyName: t(Keys.controls.settings.partitionKey.partitionKey).toLowerCase(),
"Change partition key", }),
<ChangePartitionKeyPane <ChangePartitionKeyPane
sourceDatabase={database} sourceDatabase={database}
sourceCollection={collection} sourceCollection={collection}
explorer={explorer} explorer={explorer}
onClose={refreshDataTransferOperations} onClose={refreshDataTransferOperations}
/>, />,
); );
}; };
const getPercentageComplete = () => { const getPercentageComplete = () => {
@@ -181,16 +188,28 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
return ( return (
<Stack tokens={{ childrenGap: 20 }} styles={{ root: { maxWidth: 600 } }}> <Stack tokens={{ childrenGap: 20 }} styles={{ root: { maxWidth: 600 } }}>
<Stack tokens={{ childrenGap: 10 }}> <Stack tokens={{ childrenGap: 10 }}>
{!isReadOnly && <Text styles={textHeadingStyle}>Change {partitionKeyName.toLowerCase()}</Text>} {!isReadOnly && (
<Text styles={textHeadingStyle}>
{t(Keys.controls.settings.partitionKeyEditor.changePartitionKey, {
partitionKeyName: partitionKeyName.toLowerCase(),
})}
</Text>
)}
<Stack horizontal tokens={{ childrenGap: 20 }}> <Stack horizontal tokens={{ childrenGap: 20 }}>
<Stack tokens={{ childrenGap: 5 }}> <Stack tokens={{ childrenGap: 5 }}>
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text> <Text styles={textSubHeadingStyle}>
<Text styles={textSubHeadingStyle}>Partitioning</Text> {t(Keys.controls.settings.partitionKeyEditor.currentPartitionKey, {
partitionKeyName: partitionKeyName.toLowerCase(),
})}
</Text>
<Text styles={textSubHeadingStyle}>{t(Keys.controls.settings.partitionKeyEditor.partitioning)}</Text>
</Stack> </Stack>
<Stack tokens={{ childrenGap: 5 }} data-test="partition-key-values"> <Stack tokens={{ childrenGap: 5 }} data-test="partition-key-values">
<Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text> <Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text>
<Text styles={textSubHeadingStyle1}> <Text styles={textSubHeadingStyle1}>
{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"} {isHierarchicalPartitionedContainer()
? t(Keys.controls.settings.partitionKeyEditor.hierarchical)
: t(Keys.controls.settings.partitionKeyEditor.nonHierarchical)}
</Text> </Text>
</Stack> </Stack>
</Stack> </Stack>
@@ -204,33 +223,33 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }} messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
styles={darkThemeMessageBarStyles} styles={darkThemeMessageBarStyles}
> >
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to {t(Keys.controls.settings.partitionKeyEditor.safeguardWarning)}
the source container for the entire duration of the partition key change process.
<Link <Link
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work" href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
target="_blank" target="_blank"
underline underline
style={{ color: "var(--colorBrandForeground1)" }} style={{ color: "var(--colorBrandForeground1)" }}
> >
Learn more {t(Keys.common.learnMore)}
</Link> </Link>
</MessageBar> </MessageBar>
<Text styles={{ root: { color: "var(--colorNeutralForeground1)" } }}> <Text styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>
To change the partition key, a new destination container must be created or an existing destination {t(Keys.controls.settings.partitionKeyEditor.changeDescription)}
container selected. Data will then be copied to the destination container.
</Text> </Text>
{configContext.platform !== Platform.Emulator && ( {configContext.platform !== Platform.Emulator && (
<PrimaryButton <PrimaryButton
data-test="change-partition-key-button" data-test="change-partition-key-button"
styles={{ root: { width: "fit-content" } }} styles={{ root: { width: "fit-content" } }}
text="Change" text={t(Keys.controls.settings.partitionKeyEditor.changeButton)}
onClick={startPartitionkeyChangeWorkflow} onClick={startPartitionkeyChangeWorkflow}
disabled={isCurrentJobInProgress(portalDataTransferJob)} disabled={isCurrentJobInProgress(portalDataTransferJob)}
/> />
)} )}
{portalDataTransferJob && ( {portalDataTransferJob && (
<Stack> <Stack>
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text> <Text styles={textHeadingStyle}>
{t(Keys.controls.settings.partitionKeyEditor.changeJob, { partitionKeyName })}
</Text>
<Stack <Stack
horizontal horizontal
tokens={{ childrenGap: 20 }} tokens={{ childrenGap: 20 }}
@@ -251,7 +270,10 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
}} }}
></ProgressIndicator> ></ProgressIndicator>
{isCurrentJobInProgress(portalDataTransferJob) && ( {isCurrentJobInProgress(portalDataTransferJob) && (
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} /> <DefaultButton
text={t(Keys.controls.settings.partitionKeyEditor.cancelButton)}
onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)}
/>
)} )}
</Stack> </Stack>
</Stack> </Stack>

View File

@@ -1,4 +1,5 @@
import { Link, MessageBar, MessageBarType, Stack, Text, TextField } from "@fluentui/react"; import { Link, MessageBar, MessageBarType, Stack, Text, TextField } from "@fluentui/react";
import { Keys, t } from "Localization";
import * as React from "react"; import * as React from "react";
import * as Constants from "../../../../Common/Constants"; import * as Constants from "../../../../Common/Constants";
import { Platform, configContext } from "../../../../ConfigContext"; import { Platform, configContext } from "../../../../ConfigContext";
@@ -92,8 +93,10 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
} }
const minThroughput: string = this.getMinRUs().toLocaleString(); const minThroughput: string = this.getMinRUs().toLocaleString();
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : this.getMaxRUs().toLocaleString(); const maxThroughput: string = !this.props.isFixedContainer
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`; ? t(Keys.controls.settings.scale.unlimited)
: this.getMaxRUs().toLocaleString();
return t(Keys.controls.settings.scale.throughputRangeLabel, { min: minThroughput, max: maxThroughput });
}; };
public canThroughputExceedMaximumValue = (): boolean => { public canThroughputExceedMaximumValue = (): boolean => {
@@ -156,14 +159,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
const freeTierLimits = SharedConstants.FreeTierLimits; const freeTierLimits = SharedConstants.FreeTierLimits;
return ( return (
<Text> <Text>
With free tier, you will get the first {freeTierLimits.RU} RU/s and {freeTierLimits.Storage} GB of storage in {t(Keys.controls.settings.scale.freeTierInfo, { ru: freeTierLimits.RU, storage: freeTierLimits.Storage })}
this account for free. To keep your account free, keep the total RU/s across all resources in the account to{" "}
{freeTierLimits.RU} RU/s.
<Link <Link
href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts" href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts"
target="_blank" target="_blank"
> >
Learn more. {t(Keys.controls.settings.scale.freeTierLearnMore)}
</Link> </Link>
</Text> </Text>
); );
@@ -188,12 +189,9 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
{/* TODO: Replace link with call to the Azure Support blade */} {/* TODO: Replace link with call to the Azure Support blade */}
{this.isAutoScaleEnabled() && ( {this.isAutoScaleEnabled() && (
<Stack {...titleAndInputStackProps}> <Stack {...titleAndInputStackProps}>
<Text>Throughput (RU/s)</Text> <Text>{t(Keys.controls.settings.scale.throughputRuS)}</Text>
<TextField disabled styles={getTextFieldStyles(undefined, undefined)} /> <TextField disabled styles={getTextFieldStyles(undefined, undefined)} />
<Text> <Text>{t(Keys.controls.settings.scale.autoScaleCustomSettings)}</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>
)} )}
</Stack> </Stack>

View File

@@ -12,6 +12,7 @@ import {
} from "@fluentui/react"; } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import * as ViewModels from "../../../../Contracts/ViewModels"; import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys, t } from "Localization";
import { userContext } from "../../../../UserContext"; import { userContext } from "../../../../UserContext";
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon"; import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
import { import {
@@ -85,9 +86,12 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
constructor(props: SubSettingsComponentProps) { constructor(props: SubSettingsComponentProps) {
super(props); super(props);
this.geospatialVisible = userContext.apiType === "SQL"; this.geospatialVisible = userContext.apiType === "SQL";
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key"; this.partitionKeyName =
userContext.apiType === "Mongo"
? t(Keys.controls.settings.partitionKey.shardKey)
: t(Keys.controls.settings.partitionKey.partitionKey);
this.partitionKeyValue = this.getPartitionKeyValue(); this.partitionKeyValue = this.getPartitionKeyValue();
this.uniqueKeyName = "Unique keys"; this.uniqueKeyName = t(Keys.controls.settings.subSettings.uniqueKeys);
this.uniqueKeyValue = this.getUniqueKeyValue(); this.uniqueKeyValue = this.getUniqueKeyValue();
} }
@@ -143,9 +147,13 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
}; };
private ttlChoiceGroupOptions: IChoiceGroupOption[] = [ private ttlChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: TtlType.Off, text: "Off", ariaLabel: "ttl-off-option" }, { key: TtlType.Off, text: t(Keys.controls.settings.subSettings.ttlOff), ariaLabel: "ttl-off-option" },
{ key: TtlType.OnNoDefault, text: "On (no default)", ariaLabel: "ttl-on-no-default-option" }, {
{ key: TtlType.On, text: "On", ariaLabel: "ttl-on-option" }, key: TtlType.OnNoDefault,
text: t(Keys.controls.settings.subSettings.ttlOnNoDefault),
ariaLabel: "ttl-on-no-default-option",
},
{ key: TtlType.On, text: t(Keys.controls.settings.subSettings.ttlOn), ariaLabel: "ttl-on-option" },
]; ];
public getTtlValue = (value: string): TtlType => { public getTtlValue = (value: string): TtlType => {
@@ -216,13 +224,13 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
}} }}
> >
<Text style={{ color: "var(--colorNeutralForeground1)" }}> <Text style={{ color: "var(--colorNeutralForeground1)" }}>
To enable time-to-live (TTL) for your collection/documents,{" "} {t(Keys.controls.settings.subSettings.mongoTtlMessage)}{" "}
<Link <Link
href="https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-time-to-live" href="https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-time-to-live"
target="_blank" target="_blank"
style={{ color: "var(--colorBrandForeground1)" }} style={{ color: "var(--colorBrandForeground1)" }}
> >
create a TTL index {t(Keys.controls.settings.subSettings.mongoTtlLinkText)}
</Link> </Link>
. .
</Text> </Text>
@@ -231,7 +239,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
<Stack {...titleAndInputStackProps}> <Stack {...titleAndInputStackProps}>
<ChoiceGroup <ChoiceGroup
id="timeToLive" id="timeToLive"
label="Time to Live" label={t(Keys.controls.settings.subSettings.timeToLive)}
selectedKey={this.props.timeToLive} selectedKey={this.props.timeToLive}
options={this.ttlChoiceGroupOptions} options={this.ttlChoiceGroupOptions}
onChange={this.onTtlChange} onChange={this.onTtlChange}
@@ -255,8 +263,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
max={Int32.Max} max={Int32.Max}
value={this.props.displayedTtlSeconds} value={this.props.displayedTtlSeconds}
onChange={this.onTimeToLiveSecondsChange} onChange={this.onTimeToLiveSecondsChange}
suffix="second(s)" suffix={t(Keys.controls.settings.subSettings.seconds)}
ariaLabel={`Time to live in seconds`} ariaLabel={t(Keys.controls.settings.subSettings.timeToLiveInSeconds)}
data-test="ttl-input" data-test="ttl-input"
/> />
)} )}
@@ -264,16 +272,16 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
); );
private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [ private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: TtlType.Off, text: "Off", disabled: true }, { key: TtlType.Off, text: t(Keys.controls.settings.subSettings.ttlOff), disabled: true },
{ key: TtlType.OnNoDefault, text: "On (no default)" }, { key: TtlType.OnNoDefault, text: t(Keys.controls.settings.subSettings.ttlOnNoDefault) },
{ key: TtlType.On, text: "On" }, { key: TtlType.On, text: t(Keys.controls.settings.subSettings.ttlOn) },
]; ];
private getAnalyticalStorageTtlComponent = (): JSX.Element => ( private getAnalyticalStorageTtlComponent = (): JSX.Element => (
<Stack {...titleAndInputStackProps}> <Stack {...titleAndInputStackProps}>
<ChoiceGroup <ChoiceGroup
id="analyticalStorageTimeToLive" id="analyticalStorageTimeToLive"
label="Analytical Storage Time to Live" label={t(Keys.controls.settings.subSettings.analyticalStorageTtl)}
selectedKey={this.props.analyticalStorageTtlSelection} selectedKey={this.props.analyticalStorageTtlSelection}
options={this.analyticalTtlChoiceGroupOptions} options={this.analyticalTtlChoiceGroupOptions}
onChange={this.onAnalyticalStorageTtlSelectionChange} onChange={this.onAnalyticalStorageTtlSelectionChange}
@@ -294,7 +302,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
min={1} min={1}
max={Int32.Max} max={Int32.Max}
value={this.props.analyticalStorageTtlSeconds?.toString()} value={this.props.analyticalStorageTtlSeconds?.toString()}
suffix="second(s)" suffix={t(Keys.controls.settings.subSettings.seconds)}
onChange={this.onAnalyticalStorageTtlSecondsChange} onChange={this.onAnalyticalStorageTtlSecondsChange}
/> />
)} )}
@@ -302,14 +310,22 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
); );
private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [ private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: GeospatialConfigType.Geography, text: "Geography", ariaLabel: "geography-option" }, {
{ key: GeospatialConfigType.Geometry, text: "Geometry", ariaLabel: "geometry-option" }, key: GeospatialConfigType.Geography,
text: t(Keys.controls.settings.subSettings.geography),
ariaLabel: "geography-option",
},
{
key: GeospatialConfigType.Geometry,
text: t(Keys.controls.settings.subSettings.geometry),
ariaLabel: "geometry-option",
},
]; ];
private getGeoSpatialComponent = (): JSX.Element => ( private getGeoSpatialComponent = (): JSX.Element => (
<ChoiceGroup <ChoiceGroup
id="geoSpatialConfig" id="geoSpatialConfig"
label="Geospatial Configuration" label={t(Keys.controls.settings.subSettings.geospatialConfiguration)}
selectedKey={this.props.geospatialConfigType} selectedKey={this.props.geospatialConfigType}
options={this.geoSpatialConfigTypeChoiceGroupOptions} options={this.geoSpatialConfigTypeChoiceGroupOptions}
onChange={this.onGeoSpatialConfigTypeChange} onChange={this.onGeoSpatialConfigTypeChange}
@@ -318,8 +334,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
); );
private changeFeedChoiceGroupOptions: IChoiceGroupOption[] = [ private changeFeedChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: ChangeFeedPolicyState.Off, text: "Off" }, { key: ChangeFeedPolicyState.Off, text: t(Keys.controls.settings.subSettings.ttlOff) },
{ key: ChangeFeedPolicyState.On, text: "On" }, { key: ChangeFeedPolicyState.On, text: t(Keys.controls.settings.subSettings.ttlOn) },
]; ];
private getChangeFeedComponent = (): JSX.Element => { private getChangeFeedComponent = (): JSX.Element => {
@@ -328,7 +344,10 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
return ( return (
<Stack> <Stack>
<Label id={labelId}> <Label id={labelId}>
<ToolTipLabelComponent label="Change feed log retention policy" toolTipElement={changeFeedPolicyToolTip} /> <ToolTipLabelComponent
label={t(Keys.controls.settings.changeFeed.label)}
toolTipElement={changeFeedPolicyToolTip}
/>
</Label> </Label>
<ChoiceGroup <ChoiceGroup
id="changeFeedPolicy" id="changeFeedPolicy"
@@ -354,9 +373,10 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
<Stack {...titleAndInputStackProps}> <Stack {...titleAndInputStackProps}>
{this.getPartitionKeyVisible() && ( {this.getPartitionKeyVisible() && (
<TooltipHost <TooltipHost
content={`This ${this.partitionKeyName.toLowerCase()} is used to distribute data across multiple partitions for scalability. The value "${ content={t(Keys.controls.settings.subSettings.partitionKeyTooltipTemplate, {
this.partitionKeyValue partitionKeyName: this.partitionKeyName.toLowerCase(),
}" determines how documents are partitioned.`} partitionKeyValue: this.partitionKeyValue,
})}
styles={{ styles={{
root: { root: {
display: "block", display: "block",
@@ -373,14 +393,20 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
)} )}
{userContext.apiType === "SQL" && this.isLargePartitionKeyEnabled() && ( {userContext.apiType === "SQL" && this.isLargePartitionKeyEnabled() && (
<Text className={classNames.hintText}>Large {this.partitionKeyName.toLowerCase()} has been enabled.</Text> <Text className={classNames.hintText}>
{t(Keys.controls.settings.subSettings.largePartitionKeyEnabled, {
partitionKeyName: this.partitionKeyName.toLowerCase(),
})}
</Text>
)} )}
{userContext.apiType === "SQL" && {userContext.apiType === "SQL" &&
(this.isHierarchicalPartitionedContainer() ? ( (this.isHierarchicalPartitionedContainer() ? (
<Text className={classNames.hintText}>Hierarchically partitioned container.</Text> <Text className={classNames.hintText}>{t(Keys.controls.settings.subSettings.hierarchicalPartitioned)}</Text>
) : ( ) : (
<Text className={classNames.hintText}>Non-hierarchically partitioned container.</Text> <Text className={classNames.hintText}>
{t(Keys.controls.settings.subSettings.nonHierarchicalPartitioned)}
</Text>
))} ))}
</Stack> </Stack>
); );

View File

@@ -12,6 +12,7 @@ import {
TooltipHost, TooltipHost,
} from "@fluentui/react"; } from "@fluentui/react";
import { ThroughputBucket } from "Contracts/DataModels"; import { ThroughputBucket } from "Contracts/DataModels";
import { Keys, t } from "Localization";
import React, { FC, useEffect, useState } from "react"; import React, { FC, useEffect, useState } from "react";
import { isDirty } from "../../SettingsUtils"; import { isDirty } from "../../SettingsUtils";
@@ -145,7 +146,9 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
return ( return (
<Stack tokens={{ childrenGap: "m" }} styles={{ root: { width: "70%", maxWidth: 700 } }}> <Stack tokens={{ childrenGap: "m" }} styles={{ root: { width: "70%", maxWidth: 700 } }}>
<Label styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>Throughput Buckets</Label> <Label styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>
{t(Keys.controls.settings.throughputBuckets.label)}
</Label>
<Stack> <Stack>
{throughputBuckets?.map((bucket) => ( {throughputBuckets?.map((bucket) => (
<Stack key={bucket.id} horizontal tokens={{ childrenGap: 8 }} verticalAlign="center"> <Stack key={bucket.id} horizontal tokens={{ childrenGap: 8 }} verticalAlign="center">
@@ -156,7 +159,9 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
value={bucket.maxThroughputPercentage} value={bucket.maxThroughputPercentage}
onChange={(newValue) => handleBucketChange(bucket.id, newValue)} onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
showValue={false} showValue={false}
label={`Bucket ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`} label={`${t(Keys.controls.settings.throughputBuckets.bucketLabel, { id: String(bucket.id) })}${
bucket.id === 1 ? t(Keys.controls.settings.throughputBuckets.dataExplorerQueryBucket) : ""
}`}
styles={{ styles={{
root: { flex: 2, maxWidth: 400 }, root: { flex: 2, maxWidth: 400 },
titleLabel: { titleLabel: {
@@ -180,8 +185,8 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
data-test={`bucket-${bucket.id}-percentage-input`} data-test={`bucket-${bucket.id}-percentage-input`}
/> />
<Toggle <Toggle
onText="Active" onText={t(Keys.controls.settings.throughputBuckets.active)}
offText="Inactive" offText={t(Keys.controls.settings.throughputBuckets.inactive)}
checked={bucket.maxThroughputPercentage !== 100} checked={bucket.maxThroughputPercentage !== 100}
onChange={(event, checked) => onToggle(bucket.id, checked)} onChange={(event, checked) => onToggle(bucket.id, checked)}
styles={{ styles={{

View File

@@ -16,6 +16,7 @@ import {
Text, Text,
TextField, TextField,
} from "@fluentui/react"; } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import * as DataModels from "../../../../../Contracts/DataModels"; import * as DataModels from "../../../../../Contracts/DataModels";
import * as SharedConstants from "../../../../../Shared/Constants"; import * as SharedConstants from "../../../../../Shared/Constants";
@@ -97,8 +98,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
private throughputInputMaxValue: number; private throughputInputMaxValue: number;
private autoPilotInputMaxValue: number; private autoPilotInputMaxValue: number;
private options: IChoiceGroupOption[] = [ private options: IChoiceGroupOption[] = [
{ key: "true", text: "Autoscale" }, { key: "true", text: t(Keys.controls.settings.throughputInput.autoscale) },
{ key: "false", text: "Manual" }, { key: "false", text: t(Keys.controls.settings.throughputInput.manual) },
]; ];
// Style constants for theme-aware colors and layout // Style constants for theme-aware colors and layout
@@ -244,7 +245,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
return ( return (
<div> <div>
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}> <Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
Updated cost per month {t(Keys.controls.settings.costEstimate.updatedCostPerMonth)}
</Text> </Text>
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}> <Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
<Text <Text
@@ -253,7 +254,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
}} }}
> >
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)} min {newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)}{" "}
{t(Keys.controls.settings.throughputInput.min)}
</Text> </Text>
<Text <Text
style={{ style={{
@@ -261,7 +263,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
}} }}
> >
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} max {newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}{" "}
{t(Keys.controls.settings.throughputInput.max)}
</Text> </Text>
</Stack> </Stack>
</div> </div>
@@ -274,7 +277,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}> <Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
{newThroughput && newThroughputCostElement()} {newThroughput && newThroughputCostElement()}
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}> <Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
Current cost per month {t(Keys.controls.settings.costEstimate.currentCostPerMonth)}
</Text> </Text>
<Stack horizontal style={{ marginTop: 5, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}> <Stack horizontal style={{ marginTop: 5, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
<Text <Text
@@ -283,7 +286,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
}} }}
> >
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)} min {prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)}{" "}
{t(Keys.controls.settings.throughputInput.min)}
</Text> </Text>
<Text <Text
style={{ style={{
@@ -291,7 +295,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
}} }}
> >
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} max {prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}{" "}
{t(Keys.controls.settings.throughputInput.max)}
</Text> </Text>
</Stack> </Stack>
</Stack> </Stack>
@@ -326,17 +331,20 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
return ( return (
<div> <div>
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}> <Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
Updated cost per month {t(Keys.controls.settings.costEstimate.updatedCostPerMonth)}
</Text> </Text>
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}> <Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
<Text style={this.settingsAndScaleStyle.root}> <Text style={this.settingsAndScaleStyle.root}>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}/hr {newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}
{t(Keys.controls.settings.costEstimate.perHour)}
</Text> </Text>
<Text style={this.settingsAndScaleStyle.root}> <Text style={this.settingsAndScaleStyle.root}>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}/day {newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}
{t(Keys.controls.settings.costEstimate.perDay)}
</Text> </Text>
<Text style={this.settingsAndScaleStyle.root}> <Text style={this.settingsAndScaleStyle.root}>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}/mo {newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
{t(Keys.controls.settings.costEstimate.perMonth)}
</Text> </Text>
</Stack> </Stack>
</div> </div>
@@ -349,17 +357,20 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}> <Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
{newThroughput && newThroughputCostElement()} {newThroughput && newThroughputCostElement()}
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}> <Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
Current cost per month {t(Keys.controls.settings.costEstimate.currentCostPerMonth)}
</Text> </Text>
<Stack horizontal style={{ marginTop: 5 }}> <Stack horizontal style={{ marginTop: 5 }}>
<Text style={this.settingsAndScaleStyle.root}> <Text style={this.settingsAndScaleStyle.root}>
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}/hr {prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}
{t(Keys.controls.settings.costEstimate.perHour)}
</Text> </Text>
<Text style={this.settingsAndScaleStyle.root}> <Text style={this.settingsAndScaleStyle.root}>
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}/day {prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}
{t(Keys.controls.settings.costEstimate.perDay)}
</Text> </Text>
<Text style={this.settingsAndScaleStyle.root}> <Text style={this.settingsAndScaleStyle.root}>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}/mo {prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
{t(Keys.controls.settings.costEstimate.perMonth)}
</Text> </Text>
</Stack> </Stack>
</Stack> </Stack>
@@ -444,10 +455,14 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
this.setState({ spendAckChecked: checked }); this.setState({ spendAckChecked: checked });
private getStorageCapacityTitle = (): JSX.Element => { private getStorageCapacityTitle = (): JSX.Element => {
const capacity: string = this.props.isFixed ? "Fixed" : "Unlimited"; const capacity: string = this.props.isFixed
? t(Keys.controls.settings.throughputInput.fixed)
: t(Keys.controls.settings.throughputInput.unlimited);
return ( return (
<Stack {...titleAndInputStackProps}> <Stack {...titleAndInputStackProps}>
<Label style={{ color: "var(--colorNeutralForeground1)" }}>Storage capacity</Label> <Label style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.throughputInput.storageCapacity)}
</Label>
<Text style={{ color: "var(--colorNeutralForeground1)" }}>{capacity}</Text> <Text style={{ color: "var(--colorNeutralForeground1)" }}>{capacity}</Text>
</Stack> </Stack>
); );
@@ -543,9 +558,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
<span style={{ float: "left", transform: "translateX(-50%)" }}> <span style={{ float: "left", transform: "translateX(-50%)" }}>
{this.props.instantMaximumThroughput.toLocaleString(ThroughputInputAutoPilotV3Component.LOCALE_EN_US)} {this.props.instantMaximumThroughput.toLocaleString(ThroughputInputAutoPilotV3Component.LOCALE_EN_US)}
</span> </span>
<span style={{ float: "right" }}>
{this.props.softAllowedMaximumThroughput.toLocaleString(ThroughputInputAutoPilotV3Component.LOCALE_EN_US)}
</span>
<span style={{ float: "right" }} data-test="soft-allowed-maximum-throughput"> <span style={{ float: "right" }} data-test="soft-allowed-maximum-throughput">
{this.props.softAllowedMaximumThroughput.toLocaleString(ThroughputInputAutoPilotV3Component.LOCALE_EN_US)} {this.props.softAllowedMaximumThroughput.toLocaleString(ThroughputInputAutoPilotV3Component.LOCALE_EN_US)}
</span> </span>
@@ -558,10 +570,14 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
/> />
<Stack horizontal> <Stack horizontal>
<Stack.Item style={{ width: "34%", paddingRight: "5px" }}> <Stack.Item style={{ width: "34%", paddingRight: "5px" }}>
<Separator styles={this.thoughputRangeSeparatorStyles}>Instant</Separator> <Separator styles={this.thoughputRangeSeparatorStyles}>
{t(Keys.controls.settings.throughputInput.instant)}
</Separator>
</Stack.Item> </Stack.Item>
<Stack.Item style={{ width: "66%", paddingLeft: "5px" }}> <Stack.Item style={{ width: "66%", paddingLeft: "5px" }}>
<Separator styles={this.thoughputRangeSeparatorStyles}>4-6 hrs</Separator> <Separator styles={this.thoughputRangeSeparatorStyles}>
{t(Keys.controls.settings.throughputInput.fourToSixHrs)}
</Separator>
</Stack.Item> </Stack.Item>
</Stack> </Stack>
</Stack> </Stack>
@@ -641,7 +657,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
variant="small" variant="small"
style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }} style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }}
> >
Minimum RU/s {t(Keys.controls.settings.throughputInput.minimumRuS)}
</Text> </Text>
<FontIcon iconName="Info" style={{ fontSize: 12, color: "var(--colorNeutralForeground2)" }} /> <FontIcon iconName="Info" style={{ fontSize: 12, color: "var(--colorNeutralForeground2)" }} />
</Stack> </Stack>
@@ -675,7 +691,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: "var(--colorNeutralForeground1)", color: "var(--colorNeutralForeground1)",
}} }}
> >
x 10 = {t(Keys.controls.settings.throughputInput.x10Equals)}
</Text> </Text>
{/* Column 3: Maximum RU/s */} {/* Column 3: Maximum RU/s */}
@@ -685,7 +701,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
variant="small" variant="small"
style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }} style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }}
> >
Maximum RU/s {t(Keys.controls.settings.throughputInput.maximumRuS)}
</Text> </Text>
<FontIcon iconName="Info" style={{ fontSize: 12, color: "var(--colorNeutralForeground2)" }} /> <FontIcon iconName="Info" style={{ fontSize: 12, color: "var(--colorNeutralForeground2)" }} />
</Stack> </Stack>
@@ -726,7 +742,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
onGetErrorMessage={(value: string) => { onGetErrorMessage={(value: string) => {
const sanitizedValue = getSanitizedInputValue(value); const sanitizedValue = getSanitizedInputValue(value);
const errorMessage: string = const errorMessage: string =
sanitizedValue % 1000 ? "Throughput value must be in increments of 1000" : this.props.throughputError; sanitizedValue % 1000
? t(Keys.controls.settings.throughput.throughputIncrementError)
: this.props.throughputError;
return <span data-test="autopilot-throughput-input-error">{errorMessage}</span>; return <span data-test="autopilot-throughput-input-error">{errorMessage}</span>;
}} }}
validateOnLoad={false} validateOnLoad={false}
@@ -772,7 +790,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
)} )}
{this.props.isAutoPilotSelected ? ( {this.props.isAutoPilotSelected ? (
<Text style={{ marginTop: "40px", color: "var(--colorNeutralForeground1)" }}> <Text style={{ marginTop: "40px", color: "var(--colorNeutralForeground1)" }}>
Based on usage, your {this.props.collectionName ? "container" : "database"} throughput will scale from{" "} {t(Keys.controls.settings.throughputInput.autoscaleDescription, {
resourceType: this.props.collectionName ? "container" : "database",
})}{" "}
<b> <b>
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)} RU/s (10% of max RU/s) -{" "} {AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)} RU/s (10% of max RU/s) -{" "}
{this.props.maxAutoPilotThroughput} RU/s {this.props.maxAutoPilotThroughput} RU/s
@@ -787,16 +807,19 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
styles={this.darkThemeMessageBarStyles} styles={this.darkThemeMessageBarStyles}
style={{ marginTop: "40px" }} style={{ marginTop: "40px" }}
> >
{`Billing will apply if you provision more than ${SharedConstants.FreeTierLimits.RU} RU/s of manual throughput, or if the resource scales beyond ${SharedConstants.FreeTierLimits.RU} RU/s with autoscale.`} {t(Keys.controls.settings.throughputInput.freeTierWarning, {
ru: String(SharedConstants.FreeTierLimits.RU),
})}
</MessageBar> </MessageBar>
)} )}
</> </>
)} )}
{!this.overrideWithProvisionedThroughputSettings() && ( {!this.overrideWithProvisionedThroughputSettings() && (
<Text style={{ color: "var(--colorNeutralForeground1)" }}> <Text style={{ color: "var(--colorNeutralForeground1)" }}>
Estimate your required RU/s with {t(Keys.controls.settings.throughputInput.capacityCalculator)}
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/"> <Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" /> {t(Keys.controls.settings.throughputInput.capacityCalculatorLink)}{" "}
<FontIcon iconName="NavigateExternalInline" />
</Link> </Link>
</Text> </Text>
)} )}
@@ -809,9 +832,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
onChange={this.onSpendAckChecked} onChange={this.onSpendAckChecked}
/> />
)} )}
{this.props.isFixed && ( {this.props.isFixed && <p>{t(Keys.controls.settings.throughputInput.fixedStorageNote)}</p>}
<p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>
)}
{this.props.collectionName && ( {this.props.collectionName && (
<Stack.Item style={{ marginTop: "40px" }}>{this.getStorageCapacityTitle()}</Stack.Item> <Stack.Item style={{ marginTop: "40px" }}>{this.getStorageCapacityTitle()}</Stack.Item>
)} )}

View File

@@ -426,15 +426,6 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
> >
5,000 5,000
</span> </span>
<span
style={
{
"float": "right",
}
}
>
1,000,000
</span>
<span <span
data-test="soft-allowed-maximum-throughput" data-test="soft-allowed-maximum-throughput"
style={ style={
@@ -561,9 +552,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
} }
} }
> >
You are not able to lower throughput below your current minimum of You are not able to lower throughput below your current minimum of 10000 RU/s. For more information on this limit, please refer to our service quote documentation.
10000
RU/s. For more information on this limit, please refer to our service quote documentation.
<StyledLinkBase <StyledLinkBase
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits" href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank" target="_blank"
@@ -581,9 +570,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
} }
} }
> >
Based on usage, your Based on usage, your container throughput will scale from
container
throughput will scale from
<b> <b>
400 400
@@ -692,7 +679,8 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$ $
35.04 35.04
min
min
</Text> </Text>
<Text <Text
style={ style={
@@ -705,7 +693,8 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$ $
350.40 350.40
max
max
</Text> </Text>
</Stack> </Stack>
</div> </div>
@@ -739,7 +728,8 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$ $
35.04 35.04
min
min
</Text> </Text>
<Text <Text
style={ style={
@@ -752,7 +742,8 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$ $
350.40 350.40
max
max
</Text> </Text>
</Stack> </Stack>
</Stack> </Stack>
@@ -1034,15 +1025,6 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
> >
5,000 5,000
</span> </span>
<span
style={
{
"float": "right",
}
}
>
1,000,000
</span>
<span <span
data-test="soft-allowed-maximum-throughput" data-test="soft-allowed-maximum-throughput"
style={ style={
@@ -1169,9 +1151,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
} }
} }
> >
You are not able to lower throughput below your current minimum of You are not able to lower throughput below your current minimum of 10000 RU/s. For more information on this limit, please refer to our service quote documentation.
10000
RU/s. For more information on this limit, please refer to our service quote documentation.
<StyledLinkBase <StyledLinkBase
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits" href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank" target="_blank"
@@ -1620,15 +1600,6 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
> >
5,000 5,000
</span> </span>
<span
style={
{
"float": "right",
}
}
>
1,000,000
</span>
<span <span
data-test="soft-allowed-maximum-throughput" data-test="soft-allowed-maximum-throughput"
style={ style={
@@ -1755,9 +1726,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
} }
} }
> >
You are not able to lower throughput below your current minimum of You are not able to lower throughput below your current minimum of 10000 RU/s. For more information on this limit, please refer to our service quote documentation.
10000
RU/s. For more information on this limit, please refer to our service quote documentation.
<StyledLinkBase <StyledLinkBase
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits" href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank" target="_blank"

View File

@@ -27,7 +27,8 @@ exports[`ComputedPropertiesComponent renders 1`] = `
iconName="NavigateExternalInline" iconName="NavigateExternalInline"
/> />
</StyledLinkBase> </StyledLinkBase>
  about how to define computed properties and how to use them.  
about how to define computed properties and how to use them.
</Text> </Text>
<div <div
className="settingsV2Editor" className="settingsV2Editor"

View File

@@ -33,8 +33,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
} }
} }
> >
Change Change partition key
partition key
</Text> </Text>
<Stack <Stack
horizontal={true} horizontal={true}
@@ -61,8 +60,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
} }
} }
> >
Current Current partition key
partition key
</Text> </Text>
<Text <Text
styles={ styles={
@@ -223,8 +221,7 @@ exports[`PartitionKeyComponent renders read-only component and matches snapshot
} }
} }
> >
Current Current partition key
partition key
</Text> </Text>
<Text <Text
styles={ styles={

View File

@@ -410,9 +410,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
<Text <Text
className="hintText-115" className="hintText-115"
> >
Large Large partition key has been enabled.
partition key
has been enabled.
</Text> </Text>
<Text <Text
className="hintText-115" className="hintText-115"
@@ -984,9 +982,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
<Text <Text
className="hintText-115" className="hintText-115"
> >
Large Large partition key has been enabled.
partition key
has been enabled.
</Text> </Text>
<Text <Text
className="hintText-115" className="hintText-115"
@@ -1522,9 +1518,7 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
<Text <Text
className="hintText-115" className="hintText-115"
> >
Large Large partition key has been enabled.
partition key
has been enabled.
</Text> </Text>
<Text <Text
className="hintText-115" className="hintText-115"
@@ -2157,9 +2151,7 @@ exports[`SubSettingsComponent renders 1`] = `
<Text <Text
className="hintText-115" className="hintText-115"
> >
Large Large partition key has been enabled.
partition key
has been enabled.
</Text> </Text>
<Text <Text
className="hintText-115" className="hintText-115"
@@ -2729,9 +2721,7 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
<Text <Text
className="hintText-115" className="hintText-115"
> >
Large Large partition key has been enabled.
partition key
has been enabled.
</Text> </Text>
<Text <Text
className="hintText-115" className="hintText-115"

View File

@@ -1,7 +1,10 @@
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { Keys, t } from "Localization";
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil"; import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
import { userContext } from "../../../UserContext";
import { isCapabilityEnabled } from "../../../Utils/CapabilityUtils";
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types"; import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
const zeroValue = 0; const zeroValue = 0;
@@ -88,6 +91,19 @@ export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection):
return database?.isDatabaseShared() && !collection.offer(); return database?.isDatabaseShared() && !collection.offer();
}; };
export const isDataMaskingEnabled = (dataMaskingPolicy?: DataModels.DataMaskingPolicy): boolean => {
const isSqlAccount = userContext.apiType === "SQL";
if (!isSqlAccount) {
return false;
}
const hasDataMaskingCapability = isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking);
const hasDataMaskingPolicyFromCollection =
dataMaskingPolicy?.includedPaths?.length > 0 || dataMaskingPolicy?.excludedPaths?.length > 0;
return hasDataMaskingCapability || hasDataMaskingPolicyFromCollection;
};
export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => { export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => {
// Backend can contain different casing as it does case-insensitive comparisson // Backend can contain different casing as it does case-insensitive comparisson
if (!modeFromBackend) { if (!modeFromBackend) {
@@ -160,25 +176,27 @@ const getStringValue = (value: isDirtyTypes, type: string): string => {
export const getTabTitle = (tab: SettingsV2TabTypes): string => { export const getTabTitle = (tab: SettingsV2TabTypes): string => {
switch (tab) { switch (tab) {
case SettingsV2TabTypes.ScaleTab: case SettingsV2TabTypes.ScaleTab:
return "Scale"; return t(Keys.controls.settings.tabTitles.scale);
case SettingsV2TabTypes.ConflictResolutionTab: case SettingsV2TabTypes.ConflictResolutionTab:
return "Conflict Resolution"; return t(Keys.controls.settings.tabTitles.conflictResolution);
case SettingsV2TabTypes.SubSettingsTab: case SettingsV2TabTypes.SubSettingsTab:
return "Settings"; return t(Keys.controls.settings.tabTitles.settings);
case SettingsV2TabTypes.IndexingPolicyTab: case SettingsV2TabTypes.IndexingPolicyTab:
return "Indexing Policy"; return t(Keys.controls.settings.tabTitles.indexingPolicy);
case SettingsV2TabTypes.PartitionKeyTab: case SettingsV2TabTypes.PartitionKeyTab:
return isFabricNative() ? "Partition Keys" : "Partition Keys (preview)"; return isFabricNative()
? t(Keys.controls.settings.tabTitles.partitionKeys)
: t(Keys.controls.settings.tabTitles.partitionKeysPreview);
case SettingsV2TabTypes.ComputedPropertiesTab: case SettingsV2TabTypes.ComputedPropertiesTab:
return "Computed Properties"; return t(Keys.controls.settings.tabTitles.computedProperties);
case SettingsV2TabTypes.ContainerVectorPolicyTab: case SettingsV2TabTypes.ContainerVectorPolicyTab:
return "Container Policies"; return t(Keys.controls.settings.tabTitles.containerPolicies);
case SettingsV2TabTypes.ThroughputBucketsTab: case SettingsV2TabTypes.ThroughputBucketsTab:
return "Throughput Buckets"; return t(Keys.controls.settings.tabTitles.throughputBuckets);
case SettingsV2TabTypes.GlobalSecondaryIndexTab: case SettingsV2TabTypes.GlobalSecondaryIndexTab:
return "Global Secondary Index (Preview)"; return t(Keys.controls.settings.tabTitles.globalSecondaryIndexPreview);
case SettingsV2TabTypes.DataMaskingTab: case SettingsV2TabTypes.DataMaskingTab:
return "Masking Policy (preview)"; return t(Keys.controls.settings.tabTitles.maskingPolicyPreview);
default: default:
throw new Error(`Unknown tab ${tab}`); throw new Error(`Unknown tab ${tab}`);
} }
@@ -188,19 +206,19 @@ export const getMongoNotification = (description: string, type: MongoIndexTypes)
if (description && !type) { if (description && !type) {
return { return {
type: MongoNotificationType.Warning, type: MongoNotificationType.Warning,
message: "Please select a type for each index.", message: t(Keys.controls.settings.mongoNotifications.selectTypeWarning),
}; };
} }
if (type && (!description || description.trim().length === 0)) { if (type && (!description || description.trim().length === 0)) {
return { return {
type: MongoNotificationType.Error, type: MongoNotificationType.Error,
message: "Please enter a field name.", message: t(Keys.controls.settings.mongoNotifications.enterFieldNameError),
}; };
} else if (type === MongoIndexTypes.Wildcard && description?.indexOf("$**") === -1) { } else if (type === MongoIndexTypes.Wildcard && description?.indexOf("$**") === -1) {
return { return {
type: MongoNotificationType.Error, type: MongoNotificationType.Error,
message: "Wildcard path is not present in the field name. Use a pattern like " + MongoWildcardPlaceHolder, message: t(Keys.controls.settings.mongoNotifications.wildcardPathError) + MongoWildcardPlaceHolder,
}; };
} }
@@ -234,28 +252,29 @@ export const isIndexTransforming = (indexTransformationProgress: number): boolea
indexTransformationProgress !== undefined && indexTransformationProgress !== 100; indexTransformationProgress !== undefined && indexTransformationProgress !== 100;
export const getPartitionKeyName = (apiType: string, isLowerCase?: boolean): string => { export const getPartitionKeyName = (apiType: string, isLowerCase?: boolean): string => {
const partitionKeyName = apiType === "Mongo" ? "Shard key" : "Partition key"; const partitionKeyName =
apiType === "Mongo"
? t(Keys.controls.settings.partitionKey.shardKey)
: t(Keys.controls.settings.partitionKey.partitionKey);
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName; return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
}; };
export const getPartitionKeyTooltipText = (apiType: string): string => { export const getPartitionKeyTooltipText = (apiType: string): string => {
if (apiType === "Mongo") { if (apiType === "Mongo") {
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. Its critical to choose a field that will evenly distribute your data."; return t(Keys.controls.settings.partitionKey.shardKeyTooltip);
} }
let tooltipText = `The ${getPartitionKeyName( let tooltipText = `The ${getPartitionKeyName(apiType, true)} ${t(
apiType, Keys.controls.settings.partitionKey.partitionKeyTooltip,
true, )}`;
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
if (apiType === "SQL") { if (apiType === "SQL") {
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice."; tooltipText += t(Keys.controls.settings.partitionKey.sqlPartitionKeyTooltipSuffix);
} }
return tooltipText; return tooltipText;
}; };
export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: string): string => { export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: string): string => {
if (partitionKeyDefault && (apiType === "SQL" || apiType === "Mongo")) { if (partitionKeyDefault && (apiType === "SQL" || apiType === "Mongo")) {
const subtext = "For small workloads, the item ID is a suitable choice for the partition key."; return t(Keys.controls.settings.partitionKey.partitionKeySubtext);
return subtext;
} }
return ""; return "";
}; };
@@ -263,18 +282,18 @@ export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: st
export const getPartitionKeyPlaceHolder = (apiType: string, index?: number): string => { export const getPartitionKeyPlaceHolder = (apiType: string, index?: number): string => {
switch (apiType) { switch (apiType) {
case "Mongo": case "Mongo":
return "e.g., categoryId"; return t(Keys.controls.settings.partitionKey.mongoPlaceholder);
case "Gremlin": case "Gremlin":
return "e.g., /address"; return t(Keys.controls.settings.partitionKey.gremlinPlaceholder);
case "SQL": case "SQL":
return `${ return `${
index === undefined index === undefined
? "Required - first partition key e.g., /TenantId" ? t(Keys.controls.settings.partitionKey.sqlFirstPartitionKey)
: index === 0 : index === 0
? "second partition key e.g., /UserId" ? t(Keys.controls.settings.partitionKey.sqlSecondPartitionKey)
: "third partition key e.g., /SessionId" : t(Keys.controls.settings.partitionKey.sqlThirdPartitionKey)
}`; }`;
default: default:
return "e.g., /address/zipCode"; return t(Keys.controls.settings.partitionKey.defaultPlaceholder);
} }
}; };

View File

@@ -68,7 +68,6 @@ export const collection = {
dataMaskingPolicy: ko.observable<DataModels.DataMaskingPolicy>({ dataMaskingPolicy: ko.observable<DataModels.DataMaskingPolicy>({
includedPaths: [], includedPaths: [],
excludedPaths: ["/excludedPath"], excludedPaths: ["/excludedPath"],
isPolicyEnabled: true,
}), }),
readSettings: () => { readSettings: () => {
return; return;

View File

@@ -110,6 +110,7 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@@ -231,6 +232,7 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@@ -356,6 +358,7 @@ exports[`SettingsComponent renders 1`] = `
onFullTextPolicyDirtyChange={[Function]} onFullTextPolicyDirtyChange={[Function]}
onVectorEmbeddingPolicyChange={[Function]} onVectorEmbeddingPolicyChange={[Function]}
onVectorEmbeddingPolicyDirtyChange={[Function]} onVectorEmbeddingPolicyDirtyChange={[Function]}
onVectorEmbeddingPolicyValidationChange={[Function]}
resetShouldDiscardContainerPolicyChange={[Function]} resetShouldDiscardContainerPolicyChange={[Function]}
shouldDiscardContainerPolicies={false} shouldDiscardContainerPolicies={false}
vectorEmbeddingPolicy={{}} vectorEmbeddingPolicy={{}}
@@ -453,6 +456,7 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@@ -524,6 +528,7 @@ exports[`SettingsComponent renders 1`] = `
explorer={ explorer={
Explorer { Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@@ -604,6 +609,58 @@ exports[`SettingsComponent renders 1`] = `
/> />
</Stack> </Stack>
</PivotItem> </PivotItem>
<PivotItem
headerButtonProps={
{
"data-test": "settings-tab-header/DataMaskingTab",
}
}
headerText="Masking Policy (preview)"
itemKey="DataMaskingTab"
key="DataMaskingTab"
style={
{
"backgroundColor": "var(--colorNeutralBackground1)",
"color": "var(--colorNeutralForeground1)",
"marginTop": 20,
}
}
>
<Stack
styles={
{
"root": {
"backgroundColor": "var(--colorNeutralBackground1)",
"color": "var(--colorNeutralForeground1)",
},
}
}
>
<DataMaskingComponent
dataMaskingContent={
{
"excludedPaths": [
"/excludedPath",
],
"includedPaths": [],
}
}
dataMaskingContentBaseline={
{
"excludedPaths": [
"/excludedPath",
],
"includedPaths": [],
}
}
onDataMaskingContentChange={[Function]}
onDataMaskingDirtyChange={[Function]}
resetShouldDiscardDataMasking={[Function]}
shouldDiscardDataMasking={false}
validationErrors={[]}
/>
</Stack>
</PivotItem>
<PivotItem <PivotItem
headerButtonProps={ headerButtonProps={
{ {
@@ -640,6 +697,7 @@ exports[`SettingsComponent renders 1`] = `
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
@@ -711,6 +769,7 @@ exports[`SettingsComponent renders 1`] = `
explorer={ explorer={
Explorer { Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],

View File

@@ -128,8 +128,12 @@ exports[`SettingsUtils functions render 1`] = `
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. 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 /> <br />
Database: Database:
sampleDb sampleDb
, Container: ,
Container:
sampleCollection sampleCollection
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000 , Current manual throughput: 1000 RU/s, Target manual throughput: 2000

View File

@@ -18,6 +18,7 @@ describe("AddVectorEmbeddingPolicyForm", () => {
beforeEach(() => { beforeEach(() => {
component = render( component = render(
<VectorEmbeddingPoliciesComponent <VectorEmbeddingPoliciesComponent
vectorEmbeddingsBaseline={mockVectorEmbedding}
vectorEmbeddings={mockVectorEmbedding} vectorEmbeddings={mockVectorEmbedding}
vectorIndexes={mockVectorIndex} vectorIndexes={mockVectorIndex}
onVectorEmbeddingChange={mockOnVectorEmbeddingChange} onVectorEmbeddingChange={mockOnVectorEmbeddingChange}

View File

@@ -20,6 +20,7 @@ import {
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
export interface IVectorEmbeddingPoliciesComponentProps { export interface IVectorEmbeddingPoliciesComponentProps {
vectorEmbeddingsBaseline: VectorEmbedding[];
vectorEmbeddings: VectorEmbedding[]; vectorEmbeddings: VectorEmbedding[];
onVectorEmbeddingChange: ( onVectorEmbeddingChange: (
vectorEmbeddings: VectorEmbedding[], vectorEmbeddings: VectorEmbedding[],
@@ -29,7 +30,6 @@ export interface IVectorEmbeddingPoliciesComponentProps {
vectorIndexes?: VectorIndex[]; vectorIndexes?: VectorIndex[];
discardChanges?: boolean; discardChanges?: boolean;
onChangesDiscarded?: () => void; onChangesDiscarded?: () => void;
disabled?: boolean;
isGlobalSecondaryIndex?: boolean; isGlobalSecondaryIndex?: boolean;
} }
@@ -85,14 +85,28 @@ const dropdownStyles = {
}; };
export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddingPoliciesComponentProps> = ({ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddingPoliciesComponentProps> = ({
vectorEmbeddingsBaseline,
vectorEmbeddings, vectorEmbeddings,
vectorIndexes, vectorIndexes,
onVectorEmbeddingChange, onVectorEmbeddingChange,
discardChanges, discardChanges,
onChangesDiscarded, onChangesDiscarded,
disabled,
isGlobalSecondaryIndex, isGlobalSecondaryIndex,
}): JSX.Element => { }): JSX.Element => {
const isExistingPolicy = (policy: VectorEmbeddingPolicyData): boolean => {
if (!vectorEmbeddingsBaseline || vectorEmbeddingsBaseline.length === 0) {
return false;
}
return vectorEmbeddingsBaseline.some(
(baseline) =>
baseline.path === policy.path &&
baseline.dataType === policy.dataType &&
baseline.dimensions === policy.dimensions &&
baseline.distanceFunction === policy.distanceFunction,
);
};
const onVectorEmbeddingPathError = (path: string, index?: number): string => { const onVectorEmbeddingPathError = (path: string, index?: number): string => {
let error = ""; let error = "";
if (!path) { if (!path) {
@@ -139,7 +153,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
const initializeData = (vectorEmbeddings: VectorEmbedding[], vectorIndexes: VectorIndex[]) => { const initializeData = (vectorEmbeddings: VectorEmbedding[], vectorIndexes: VectorIndex[]) => {
const mergedData: VectorEmbeddingPolicyData[] = []; const mergedData: VectorEmbeddingPolicyData[] = [];
vectorEmbeddings.forEach((embedding) => { vectorEmbeddings?.forEach((embedding) => {
const matchingIndex = displayIndexes ? vectorIndexes.find((index) => index.path === embedding.path) : undefined; const matchingIndex = displayIndexes ? vectorIndexes.find((index) => index.path === embedding.path) : undefined;
mergedData.push({ mergedData.push({
...embedding, ...embedding,
@@ -151,6 +165,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"), dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
}); });
}); });
return mergedData; return mergedData;
}; };
@@ -192,6 +207,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
const validationPassed = vectorEmbeddingPolicyData.every( const validationPassed = vectorEmbeddingPolicyData.every(
(policy: VectorEmbeddingPolicyData) => policy.pathError === "" && policy.dimensionsError === "", (policy: VectorEmbeddingPolicyData) => policy.pathError === "" && policy.dimensionsError === "",
); );
onVectorEmbeddingChange(vectorEmbeddings, vectorIndexes, validationPassed); onVectorEmbeddingChange(vectorEmbeddings, vectorIndexes, validationPassed);
}; };
@@ -300,12 +316,13 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
vectorEmbeddingPolicyData.length > 0 && vectorEmbeddingPolicyData.length > 0 &&
vectorEmbeddingPolicyData.map((vectorEmbeddingPolicy: VectorEmbeddingPolicyData, index: number) => ( vectorEmbeddingPolicyData.map((vectorEmbeddingPolicy: VectorEmbeddingPolicyData, index: number) => (
<CollapsibleSectionComponent <CollapsibleSectionComponent
disabled={disabled} disabled={isExistingPolicy(vectorEmbeddingPolicy)}
key={index} key={index}
isExpandedByDefault={true} isExpandedByDefault={true}
title={`Vector embedding ${index + 1}`} title={`Vector embedding ${index + 1}`}
showDelete={true} showDelete={true}
onDelete={() => onDelete(index)} onDelete={() => onDelete(index)}
disableDelete={false}
> >
<Stack horizontal tokens={{ childrenGap: 4 }}> <Stack horizontal tokens={{ childrenGap: 4 }}>
<Stack <Stack
@@ -319,11 +336,11 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
}} }}
> >
<Stack> <Stack>
<Label disabled={disabled} styles={labelStyles}> <Label disabled={isExistingPolicy(vectorEmbeddingPolicy)} styles={labelStyles}>
Path Path
</Label> </Label>
<TextField <TextField
disabled={disabled} disabled={isExistingPolicy(vectorEmbeddingPolicy)}
id={`vector-policy-path-${index + 1}`} id={`vector-policy-path-${index + 1}`}
required={true} required={true}
placeholder="/vector1" placeholder="/vector1"
@@ -334,11 +351,11 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
/> />
</Stack> </Stack>
<Stack> <Stack>
<Label disabled={disabled} styles={labelStyles}> <Label disabled={isExistingPolicy(vectorEmbeddingPolicy)} styles={labelStyles}>
Data type Data type
</Label> </Label>
<Dropdown <Dropdown
disabled={disabled} disabled={isExistingPolicy(vectorEmbeddingPolicy)}
required={true} required={true}
styles={dropdownStyles} styles={dropdownStyles}
options={getDataTypeOptions()} options={getDataTypeOptions()}
@@ -349,11 +366,11 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
></Dropdown> ></Dropdown>
</Stack> </Stack>
<Stack> <Stack>
<Label disabled={disabled} styles={labelStyles}> <Label disabled={isExistingPolicy(vectorEmbeddingPolicy)} styles={labelStyles}>
Distance function Distance function
</Label> </Label>
<Dropdown <Dropdown
disabled={disabled} disabled={isExistingPolicy(vectorEmbeddingPolicy)}
required={true} required={true}
styles={dropdownStyles} styles={dropdownStyles}
options={getDistanceFunctionOptions()} options={getDistanceFunctionOptions()}
@@ -364,11 +381,11 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
></Dropdown> ></Dropdown>
</Stack> </Stack>
<Stack> <Stack>
<Label disabled={disabled} styles={labelStyles}> <Label disabled={isExistingPolicy(vectorEmbeddingPolicy)} styles={labelStyles}>
Dimensions Dimensions
</Label> </Label>
<TextField <TextField
disabled={disabled} disabled={isExistingPolicy(vectorEmbeddingPolicy)}
id={`vector-policy-dimension-${index + 1}`} id={`vector-policy-dimension-${index + 1}`}
required={true} required={true}
styles={textFieldStyles} styles={textFieldStyles}
@@ -381,11 +398,11 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
</Stack> </Stack>
{displayIndexes && ( {displayIndexes && (
<Stack> <Stack>
<Label disabled={disabled} styles={labelStyles}> <Label disabled={isExistingPolicy(vectorEmbeddingPolicy)} styles={labelStyles}>
Index type Index type
</Label> </Label>
<Dropdown <Dropdown
disabled={disabled} disabled={isExistingPolicy(vectorEmbeddingPolicy)}
required={true} required={true}
styles={dropdownStyles} styles={dropdownStyles}
options={getIndexTypeOptions()} options={getIndexTypeOptions()}
@@ -397,7 +414,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
<Stack style={{ marginLeft: "10px" }}> <Stack style={{ marginLeft: "10px" }}>
<Label <Label
disabled={ disabled={
disabled || isExistingPolicy(vectorEmbeddingPolicy) ||
(vectorEmbeddingPolicy.indexType !== "quantizedFlat" && (vectorEmbeddingPolicy.indexType !== "quantizedFlat" &&
vectorEmbeddingPolicy.indexType !== "diskANN") vectorEmbeddingPolicy.indexType !== "diskANN")
} }
@@ -408,7 +425,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
</Label> </Label>
<TextField <TextField
disabled={ disabled={
disabled || isExistingPolicy(vectorEmbeddingPolicy) ||
(vectorEmbeddingPolicy.indexType !== "quantizedFlat" && (vectorEmbeddingPolicy.indexType !== "quantizedFlat" &&
vectorEmbeddingPolicy.indexType !== "diskANN") vectorEmbeddingPolicy.indexType !== "diskANN")
} }
@@ -421,11 +438,18 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
/> />
</Stack> </Stack>
<Stack style={{ marginLeft: "10px" }}> <Stack style={{ marginLeft: "10px" }}>
<Label disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"} styles={labelStyles}> <Label
disabled={
isExistingPolicy(vectorEmbeddingPolicy) || vectorEmbeddingPolicy.indexType !== "diskANN"
}
styles={labelStyles}
>
Indexing search list size Indexing search list size
</Label> </Label>
<TextField <TextField
disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"} disabled={
isExistingPolicy(vectorEmbeddingPolicy) || vectorEmbeddingPolicy.indexType !== "diskANN"
}
id={`vector-policy-indexingSearchListSize-${index + 1}`} id={`vector-policy-indexingSearchListSize-${index + 1}`}
styles={textFieldStyles} styles={textFieldStyles}
value={String(vectorEmbeddingPolicy.indexingSearchListSize || "")} value={String(vectorEmbeddingPolicy.indexingSearchListSize || "")}
@@ -435,11 +459,18 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
/> />
</Stack> </Stack>
<Stack style={{ marginLeft: "10px" }}> <Stack style={{ marginLeft: "10px" }}>
<Label disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"} styles={labelStyles}> <Label
disabled={
isExistingPolicy(vectorEmbeddingPolicy) || vectorEmbeddingPolicy.indexType !== "diskANN"
}
styles={labelStyles}
>
Vector index shard key Vector index shard key
</Label> </Label>
<TextField <TextField
disabled={disabled || vectorEmbeddingPolicy.indexType !== "diskANN"} disabled={
isExistingPolicy(vectorEmbeddingPolicy) || vectorEmbeddingPolicy.indexType !== "diskANN"
}
id={`vector-policy-vectorIndexShardKey-${index + 1}`} id={`vector-policy-vectorIndexShardKey-${index + 1}`}
styles={textFieldStyles} styles={textFieldStyles}
value={String(vectorEmbeddingPolicy.vectorIndexShardKey?.[0] ?? "")} value={String(vectorEmbeddingPolicy.vectorIndexShardKey?.[0] ?? "")}
@@ -452,12 +483,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
</Stack> </Stack>
</CollapsibleSectionComponent> </CollapsibleSectionComponent>
))} ))}
<DefaultButton <DefaultButton id={`add-vector-policy`} styles={{ root: { maxWidth: 170, fontSize: 12 } }} onClick={onAdd}>
disabled={disabled}
id={`add-vector-policy`}
styles={{ root: { maxWidth: 170, fontSize: 12 } }}
onClick={onAdd}
>
Add vector embedding Add vector embedding
</DefaultButton> </DefaultButton>
</Stack> </Stack>

View File

@@ -1,6 +1,6 @@
import { IDropdownOption } from "@fluentui/react"; import { IDropdownOption } from "@fluentui/react";
const dataTypes = ["float32", "uint8", "int8"]; const dataTypes = ["float32", "uint8", "int8", "float16"];
const distanceFunctions = ["euclidean", "cosine", "dotproduct"]; const distanceFunctions = ["euclidean", "cosine", "dotproduct"];
const indexTypes = ["none", "flat", "diskANN", "quantizedFlat"]; const indexTypes = ["none", "flat", "diskANN", "quantizedFlat"];

View File

@@ -113,7 +113,7 @@ export class ContainerSampleGenerator {
? await createMongoDocument(collection.databaseId, collection, shardKey, doc) ? await createMongoDocument(collection.databaseId, collection, shardKey, doc)
: await createDocument(collection, doc); : await createDocument(collection, doc);
} catch (error) { } catch (error) {
NotificationConsoleUtils.logConsoleError(error); NotificationConsoleUtils.logConsoleError(error instanceof Error ? error.message : String(error));
} }
}), }),
); );

View File

@@ -107,6 +107,12 @@ export default class Explorer {
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
public phoenixClient: PhoenixClient; public phoenixClient: PhoenixClient;
/**
* Resolves when the initial refreshAllDatabases (including collection loading) completes.
* Await this instead of calling refreshAllDatabases again to avoid duplicate concurrent loads.
*/
public databasesRefreshed: Promise<void> = Promise.resolve();
constructor() { constructor() {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
@@ -1197,9 +1203,11 @@ export default class Explorer {
} }
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") { if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
userContext.authType === AuthType.ResourceToken this.databasesRefreshed =
? this.refreshDatabaseForResourceToken() userContext.authType === AuthType.ResourceToken
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow ? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();
await this.databasesRefreshed; // await: we rely on the databases to be loaded before restoring the tabs further in the flow
} }
if (!isFabricNative()) { if (!isFabricNative()) {

View File

@@ -167,7 +167,7 @@ export function createContextCommandBarButtons(
export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] { export function createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [ const buttons: CommandButtonComponentProps[] = [
ThemeToggleButton(), ThemeToggleButton(configContext.platform === Platform.Portal),
{ {
iconSrc: SettingsIcon, iconSrc: SettingsIcon,
iconAlt: "Settings", iconAlt: "Settings",

View File

@@ -5,6 +5,7 @@ import {
IconType, IconType,
IDropdownOption, IDropdownOption,
IDropdownStyles, IDropdownStyles,
TooltipHost,
} from "@fluentui/react"; } from "@fluentui/react";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { KeyboardHandlerMap } from "KeyboardShortcuts"; import { KeyboardHandlerMap } from "KeyboardShortcuts";
@@ -154,6 +155,21 @@ export const convertButton = (btns: CommandButtonComponentProps[], backgroundCol
id: btn.id, id: btn.id,
}; };
if (btn.tooltipContent) {
result.title = undefined;
result.commandBarButtonAs = (props: IComponentAsProps<ICommandBarItemProps>) => {
const { defaultRender: DefaultRender, ...rest } = props;
return React.createElement(
TooltipHost,
{
content: btn.tooltipContent as JSX.Element,
calloutProps: { gapSpace: 0 },
},
React.createElement(DefaultRender, rest),
);
};
}
if (isSplit) { if (isSplit) {
// It's a split button // It's a split button
result.split = true; result.split = true;

View File

@@ -1,10 +1,13 @@
import { Link, Text } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import MoonIcon from "../../../../images/MoonIcon.svg"; import MoonIcon from "../../../../images/MoonIcon.svg";
import SunIcon from "../../../../images/SunIcon.svg"; import SunIcon from "../../../../images/SunIcon.svg";
import { useThemeStore } from "../../../hooks/useTheme"; import { useThemeStore } from "../../../hooks/useTheme";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
export const ThemeToggleButton = (): CommandButtonComponentProps => { const PORTAL_SETTINGS_URL = "https://learn.microsoft.com/azure/azure-portal/set-preferences";
export const ThemeToggleButton = (isPortal?: boolean): CommandButtonComponentProps => {
const [darkMode, setDarkMode] = React.useState(useThemeStore.getState().isDarkMode); const [darkMode, setDarkMode] = React.useState(useThemeStore.getState().isDarkMode);
React.useEffect(() => { React.useEffect(() => {
@@ -16,6 +19,34 @@ export const ThemeToggleButton = (): CommandButtonComponentProps => {
const tooltipText = darkMode ? "Switch to Light Theme" : "Switch to Dark Theme"; const tooltipText = darkMode ? "Switch to Light Theme" : "Switch to Dark Theme";
if (isPortal) {
return {
iconSrc: darkMode ? SunIcon : MoonIcon,
iconAlt: "Theme Toggle",
onCommandClick: undefined,
commandButtonLabel: undefined,
ariaLabel: "Dark Mode is managed in Azure Portal Settings",
tooltipText: undefined,
tooltipContent: React.createElement(
"div",
{ style: { padding: "4px 0" } },
React.createElement(Text, { block: true, variant: "small" }, "Dark Mode is managed in Azure Portal Settings"),
React.createElement(
Link,
{
href: PORTAL_SETTINGS_URL,
target: "_blank",
rel: "noopener noreferrer",
style: { display: "inline-block", marginTop: "4px", fontSize: "12px" },
},
"Open settings",
),
),
hasPopup: false,
disabled: true,
};
}
return { return {
iconSrc: darkMode ? SunIcon : MoonIcon, iconSrc: darkMode ? SunIcon : MoonIcon,
iconAlt: "Theme Toggle", iconAlt: "Theme Toggle",

View File

@@ -329,7 +329,10 @@ export class NotificationConsoleComponent extends React.Component<
} }
private static extractHeaderStatus(consoleData: ConsoleData) { private static extractHeaderStatus(consoleData: ConsoleData) {
return consoleData?.message.split(":\n")[0]; if (!consoleData?.message || typeof consoleData.message !== "string") {
return undefined;
}
return consoleData.message.split(":\n")[0];
} }
private onConsoleWasExpanded = (): void => { private onConsoleWasExpanded = (): void => {

View File

@@ -42,6 +42,7 @@ import {
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility"; } from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import { Keys, t } from "Localization";
import { DEFAULT_FABRIC_NATIVE_CONTAINER_THROUGHPUT, isFabricNative } from "Platform/Fabric/FabricUtil"; import { DEFAULT_FABRIC_NATIVE_CONTAINER_THROUGHPUT, isFabricNative } from "Platform/Fabric/FabricUtil";
import React from "react"; import React from "react";
import { CollectionCreation } from "Shared/Constants"; import { CollectionCreation } from "Shared/Constants";
@@ -177,31 +178,31 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
messageType="info" messageType="info"
showErrorDetails={false} showErrorDetails={false}
link={Constants.Urls.freeTierInformation} link={Constants.Urls.freeTierInformation}
linkText="Learn more" linkText={t(Keys.common.learnMore)}
/> />
)} )}
{this.state.teachingBubbleStep === 1 && ( {this.state.teachingBubbleStep === 1 && (
<TeachingBubble <TeachingBubble
headline="Create sample database" headline={t(Keys.panes.addCollection.teachingBubble.step1Headline)}
target={"#newDatabaseId"} target={"#newDatabaseId"}
calloutProps={{ gapSpace: 16 }} calloutProps={{ gapSpace: 16 }}
primaryButtonProps={{ text: "Next", onClick: () => this.setState({ teachingBubbleStep: 2 }) }} primaryButtonProps={{ text: t(Keys.common.next), onClick: () => this.setState({ teachingBubbleStep: 2 }) }}
secondaryButtonProps={{ text: "Cancel", onClick: () => this.setState({ teachingBubbleStep: 0 }) }} secondaryButtonProps={{
text: t(Keys.common.cancel),
onClick: () => this.setState({ teachingBubbleStep: 0 }),
}}
onDismiss={() => this.setState({ teachingBubbleStep: 0 })} onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
footerContent="Step 1 of 4" footerContent={t(Keys.panes.addCollection.teachingBubble.stepOfTotal, { current: "1", total: "4" })}
> >
<Stack> <Stack>
<Text style={{ color: "white" }}> <Text style={{ color: "white" }}>{t(Keys.panes.addCollection.teachingBubble.step1Body)}</Text>
Database is the parent of a container. You can create a new database or use an existing one. In this
tutorial we are creating a new database named SampleDB.
</Text>
<Link <Link
style={{ color: "white", fontWeight: 600 }} style={{ color: "white", fontWeight: 600 }}
target="_blank" target="_blank"
href="https://aka.ms/TeachingbubbleResources" href="https://aka.ms/TeachingbubbleResources"
> >
Learn more about resources. {t(Keys.panes.addCollection.teachingBubble.step1LearnMore)}
</Link> </Link>
</Stack> </Stack>
</TeachingBubble> </TeachingBubble>
@@ -209,21 +210,21 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.state.teachingBubbleStep === 2 && ( {this.state.teachingBubbleStep === 2 && (
<TeachingBubble <TeachingBubble
headline="Setting throughput" headline={t(Keys.panes.addCollection.teachingBubble.step2Headline)}
target={"#autoscaleRUValueField"} target={"#autoscaleRUValueField"}
calloutProps={{ gapSpace: 16 }} calloutProps={{ gapSpace: 16 }}
primaryButtonProps={{ text: "Next", onClick: () => this.setState({ teachingBubbleStep: 3 }) }} primaryButtonProps={{ text: t(Keys.common.next), onClick: () => this.setState({ teachingBubbleStep: 3 }) }}
secondaryButtonProps={{ text: "Previous", onClick: () => this.setState({ teachingBubbleStep: 1 }) }} secondaryButtonProps={{
text: t(Keys.common.previous),
onClick: () => this.setState({ teachingBubbleStep: 1 }),
}}
onDismiss={() => this.setState({ teachingBubbleStep: 0 })} onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
footerContent="Step 2 of 4" footerContent={t(Keys.panes.addCollection.teachingBubble.stepOfTotal, { current: "2", total: "4" })}
> >
<Stack> <Stack>
<Text style={{ color: "white" }}> <Text style={{ color: "white" }}>{t(Keys.panes.addCollection.teachingBubble.step2Body)}</Text>
Cosmos DB recommends sharing throughput across database. Autoscale will give you a flexible amount of
throughput based on the max RU/s set (Request Units).
</Text>
<Link style={{ color: "white", fontWeight: 600 }} target="_blank" href="https://aka.ms/teachingbubbleRU"> <Link style={{ color: "white", fontWeight: 600 }} target="_blank" href="https://aka.ms/teachingbubbleRU">
Learn more about RU/s. {t(Keys.panes.addCollection.teachingBubble.step2LearnMore)}
</Link> </Link>
</Stack> </Stack>
</TeachingBubble> </TeachingBubble>
@@ -231,36 +232,41 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.state.teachingBubbleStep === 3 && ( {this.state.teachingBubbleStep === 3 && (
<TeachingBubble <TeachingBubble
headline="Naming container" headline={t(Keys.panes.addCollection.teachingBubble.step3Headline)}
target={"#collectionId"} target={"#collectionId"}
calloutProps={{ gapSpace: 16 }} calloutProps={{ gapSpace: 16 }}
primaryButtonProps={{ text: "Next", onClick: () => this.setState({ teachingBubbleStep: 4 }) }} primaryButtonProps={{ text: t(Keys.common.next), onClick: () => this.setState({ teachingBubbleStep: 4 }) }}
secondaryButtonProps={{ text: "Previous", onClick: () => this.setState({ teachingBubbleStep: 2 }) }} secondaryButtonProps={{
text: t(Keys.common.previous),
onClick: () => this.setState({ teachingBubbleStep: 2 }),
}}
onDismiss={() => this.setState({ teachingBubbleStep: 0 })} onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
footerContent="Step 3 of 4" footerContent={t(Keys.panes.addCollection.teachingBubble.stepOfTotal, { current: "3", total: "4" })}
> >
Name your container {t(Keys.panes.addCollection.teachingBubble.step3Body)}
</TeachingBubble> </TeachingBubble>
)} )}
{this.state.teachingBubbleStep === 4 && ( {this.state.teachingBubbleStep === 4 && (
<TeachingBubble <TeachingBubble
headline="Setting partition key" headline={t(Keys.panes.addCollection.teachingBubble.step4Headline)}
target={"#addCollection-partitionKeyValue"} target={"#addCollection-partitionKeyValue"}
calloutProps={{ gapSpace: 16 }} calloutProps={{ gapSpace: 16 }}
primaryButtonProps={{ primaryButtonProps={{
text: "Create container", text: t(Keys.panes.addCollection.teachingBubble.step4CreateContainer),
onClick: () => { onClick: () => {
this.setState({ teachingBubbleStep: 5 }); this.setState({ teachingBubbleStep: 5 });
this.submit(); this.submit();
}, },
}} }}
secondaryButtonProps={{ text: "Previous", onClick: () => this.setState({ teachingBubbleStep: 2 }) }} secondaryButtonProps={{
text: t(Keys.common.previous),
onClick: () => this.setState({ teachingBubbleStep: 2 }),
}}
onDismiss={() => this.setState({ teachingBubbleStep: 0 })} onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
footerContent="Step 4 of 4" footerContent={t(Keys.panes.addCollection.teachingBubble.stepOfTotal, { current: "4", total: "4" })}
> >
Last step - you will need to define a partition key for your collection. /address was chosen for this {t(Keys.panes.addCollection.teachingBubble.step4Body)}
particular example. A good partition key should have a wide range of possible value
</TeachingBubble> </TeachingBubble>
)} )}
@@ -270,21 +276,23 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal> <Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span> <span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
Database {userContext.apiType === "Mongo" ? "name" : "id"} {userContext.apiType === "Mongo"
? t(Keys.panes.addCollection.databaseFieldLabelName)
: t(Keys.panes.addCollection.databaseFieldLabelId)}
</Text> </Text>
<TooltipHost <TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName( content={t(Keys.panes.addCollection.databaseTooltip, {
true, collectionName: getCollectionName(true).toLocaleLowerCase(),
).toLocaleLowerCase()}.`} })}
> >
<Icon <Icon
iconName="Info" iconName="Info"
className="panelInfoIcon" className="panelInfoIcon"
tabIndex={0} tabIndex={0}
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName( ariaLabel={t(Keys.panes.addCollection.databaseTooltip, {
true, collectionName: getCollectionName(true).toLocaleLowerCase(),
).toLocaleLowerCase()}.`} })}
/> />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
@@ -295,7 +303,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={this.state.createNewDatabase} checked={this.state.createNewDatabase}
aria-label="Create new database" aria-label={t(Keys.panes.addCollection.createNewDatabaseAriaLabel)}
aria-checked={this.state.createNewDatabase} aria-checked={this.state.createNewDatabase}
name="databaseType" name="databaseType"
type="radio" type="radio"
@@ -304,12 +312,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0} tabIndex={0}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)} onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
/> />
<span className="panelRadioBtnLabel">Create new</span> <span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.createNew)}</span>
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={!this.state.createNewDatabase} checked={!this.state.createNewDatabase}
aria-label="Use existing database" aria-label={t(Keys.panes.addCollection.useExistingDatabaseAriaLabel)}
aria-checked={!this.state.createNewDatabase} aria-checked={!this.state.createNewDatabase}
name="databaseType" name="databaseType"
type="radio" type="radio"
@@ -317,7 +325,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0} tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)} onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/> />
<span className="panelRadioBtnLabel">Use existing</span> <span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.useExisting)}</span>
</div> </div>
</Stack> </Stack>
)} )}
@@ -333,10 +341,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
autoComplete="off" autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source} pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription} title={ValidCosmosDbIdDescription}
placeholder="Type a new database id" placeholder={t(Keys.panes.addCollection.newDatabaseIdPlaceholder)}
size={40} size={40}
className="panelTextField" className="panelTextField"
aria-label="New database id, Type a new database id" aria-label={t(Keys.panes.addCollection.newDatabaseIdAriaLabel)}
tabIndex={0} tabIndex={0}
value={this.state.newDatabaseId} value={this.state.newDatabaseId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
@@ -347,7 +355,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && ( {!isServerlessAccount() && (
<Stack horizontal> <Stack horizontal>
<Checkbox <Checkbox
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`} label={t(Keys.panes.addCollection.shareThroughput, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
checked={this.state.isSharedThroughputChecked} checked={this.state.isSharedThroughputChecked}
styles={{ styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" }, text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -365,17 +375,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/> />
<TooltipHost <TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
content={`Throughput configured at the database level will be shared across all ${getCollectionName( content={t(Keys.panes.addCollection.shareThroughputTooltip, {
true, collectionName: getCollectionName(true).toLocaleLowerCase(),
).toLocaleLowerCase()} within the database.`} })}
> >
<Icon <Icon
iconName="Info" iconName="Info"
className="panelInfoIcon" className="panelInfoIcon"
tabIndex={0} tabIndex={0}
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName( ariaLabel={t(Keys.panes.addCollection.shareThroughputTooltip, {
true, collectionName: getCollectionName(true).toLocaleLowerCase(),
).toLocaleLowerCase()} within the database.`} })}
/> />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
@@ -400,10 +410,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)} )}
{!this.state.createNewDatabase && ( {!this.state.createNewDatabase && (
<Dropdown <Dropdown
ariaLabel="Choose an existing database" ariaLabel={t(Keys.panes.addCollection.chooseExistingDatabase)}
styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }} styles={{ title: { height: 27, lineHeight: 27 }, dropdownItem: { fontSize: 12 } }}
style={{ width: 300, fontSize: 12 }} style={{ width: 300, fontSize: 12 }}
placeholder="Choose an existing database" placeholder={t(Keys.panes.addCollection.chooseExistingDatabase)}
options={this.getDatabaseOptions()} options={this.getDatabaseOptions()}
onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) => onChange={(event: React.FormEvent<HTMLDivElement>, database: IDropdownOption) =>
this.setState({ selectedDatabaseId: database.key as string }) this.setState({ selectedDatabaseId: database.key as string })
@@ -424,14 +434,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Text> </Text>
<TooltipHost <TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`} content={t(Keys.panes.addCollection.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
> >
<Icon <Icon
role="button" role="button"
iconName="Info" iconName="Info"
className="panelInfoIcon" className="panelInfoIcon"
tabIndex={0} tabIndex={0}
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`} ariaLabel={t(Keys.panes.addCollection.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
/> />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
@@ -445,10 +459,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
autoComplete="off" autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source} pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription} title={ValidCosmosDbIdDescription}
placeholder={`e.g., ${getCollectionName()}1`} placeholder={t(Keys.panes.addCollection.collectionIdPlaceholder, { collectionName: getCollectionName() })}
size={40} size={40}
className="panelTextField" className="panelTextField"
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`} aria-label={t(Keys.panes.addCollection.collectionIdAriaLabel, { collectionName: getCollectionName() })}
value={this.state.collectionId} value={this.state.collectionId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ collectionId: event.target.value }) this.setState({ collectionId: event.target.value })
@@ -462,7 +476,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}> <Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
<span className="mandatoryStar">*&nbsp;</span> <span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
Indexing {t(Keys.panes.addCollection.indexing)}
</Text> </Text>
</Stack> </Stack>
@@ -470,32 +484,32 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={this.state.enableIndexing} checked={this.state.enableIndexing}
aria-label="Turn on indexing" aria-label={t(Keys.panes.addCollection.turnOnIndexing)}
aria-checked={this.state.enableIndexing} aria-checked={this.state.enableIndexing}
type="radio" type="radio"
role="radio" role="radio"
tabIndex={0} tabIndex={0}
onChange={this.onTurnOnIndexing.bind(this)} onChange={this.onTurnOnIndexing.bind(this)}
/> />
<span className="panelRadioBtnLabel">Automatic</span> <span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.automatic)}</span>
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={!this.state.enableIndexing} checked={!this.state.enableIndexing}
aria-label="Turn off indexing" aria-label={t(Keys.panes.addCollection.turnOffIndexing)}
aria-checked={!this.state.enableIndexing} aria-checked={!this.state.enableIndexing}
type="radio" type="radio"
role="radio" role="radio"
tabIndex={0} tabIndex={0}
onChange={this.onTurnOffIndexing.bind(this)} onChange={this.onTurnOffIndexing.bind(this)}
/> />
<span className="panelRadioBtnLabel">Off</span> <span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.off)}</span>
</Stack> </Stack>
<Text variant="small"> <Text variant="small">
{this.getFreeTierIndexingText()}{" "} {this.getFreeTierIndexingText()}{" "}
<Link target="_blank" href="https://aka.ms/cosmos-indexing-policy"> <Link target="_blank" href="https://aka.ms/cosmos-indexing-policy">
Learn more {t(Keys.common.learnMore)}
</Link> </Link>
</Text> </Text>
</Stack> </Stack>
@@ -508,21 +522,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal style={{ marginTop: -5, marginBottom: -4 }}> <Stack horizontal style={{ marginTop: -5, marginBottom: -4 }}>
<span className="mandatoryStar">*&nbsp;</span> <span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
Sharding {t(Keys.panes.addCollection.sharding)}
</Text> </Text>
<TooltipHost <TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
content={ content={t(Keys.panes.addCollection.shardingTooltip)}
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
}
> >
<Icon <Icon
iconName="Info" iconName="Info"
className="panelInfoIcon" className="panelInfoIcon"
tabIndex={0} tabIndex={0}
ariaLabel={ ariaLabel={t(Keys.panes.addCollection.shardingTooltip)}
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
}
/> />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
@@ -531,7 +541,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={!this.state.isSharded} checked={!this.state.isSharded}
aria-label="Unsharded" aria-label={t(Keys.panes.addCollection.unsharded)}
aria-checked={!this.state.isSharded} aria-checked={!this.state.isSharded}
name="unsharded" name="unsharded"
type="radio" type="radio"
@@ -540,12 +550,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0} tabIndex={0}
onChange={this.onUnshardedRadioBtnChange.bind(this)} onChange={this.onUnshardedRadioBtnChange.bind(this)}
/> />
<span className="panelRadioBtnLabel">Unsharded (20GB limit)</span> <span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.unshardedLabel)}</span>
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={this.state.isSharded} checked={this.state.isSharded}
aria-label="Sharded" aria-label={t(Keys.panes.addCollection.sharded)}
aria-checked={this.state.isSharded} aria-checked={this.state.isSharded}
name="sharded" name="sharded"
type="radio" type="radio"
@@ -554,7 +564,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0} tabIndex={0}
onChange={this.onShardedRadioBtnChange.bind(this)} onChange={this.onShardedRadioBtnChange.bind(this)}
/> />
<span className="panelRadioBtnLabel">Sharded</span> <span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.sharded)}</span>
</Stack> </Stack>
</Stack> </Stack>
)} )}
@@ -679,15 +689,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
disabled={this.state.subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition} disabled={this.state.subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
onClick={() => this.setState({ subPartitionKeys: [...this.state.subPartitionKeys, ""] })} onClick={() => this.setState({ subPartitionKeys: [...this.state.subPartitionKeys, ""] })}
> >
Add hierarchical partition key {t(Keys.panes.addCollection.addPartitionKey)}
</DefaultButton> </DefaultButton>
{this.state.subPartitionKeys.length > 0 && ( {this.state.subPartitionKeys.length > 0 && (
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}> <Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to <Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} />{" "}
partition your data with up to three levels of keys for better data distribution. Requires .NET {t(Keys.panes.addCollection.hierarchicalPartitionKeyInfo)}{" "}
V3, Java V4 SDK, or preview JavaScript V3 SDK.{" "}
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank"> <Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
Learn more {t(Keys.common.learnMore)}
</Link> </Link>
</Text> </Text>
)} )}
@@ -700,7 +709,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && ( {!isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && (
<Stack horizontal verticalAlign="center"> <Stack horizontal verticalAlign="center">
<Checkbox <Checkbox
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`} label={t(Keys.panes.addCollection.provisionDedicatedThroughput, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
checked={this.state.enableDedicatedThroughput} checked={this.state.enableDedicatedThroughput}
styles={{ styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" }, text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -718,23 +729,19 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/> />
<TooltipHost <TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
content={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput content={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName( collectionName: getCollectionName().toLocaleLowerCase(),
true, collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
).toLocaleLowerCase()} in the database and })}
does not count towards the throughput you provisioned for the database. This throughput amount will be
billed in addition to the throughput amount you provisioned at the database level.`}
> >
<Icon <Icon
iconName="Info" iconName="Info"
className="panelInfoIcon" className="panelInfoIcon"
tabIndex={0} tabIndex={0}
ariaLabel={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput ariaLabel={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName( collectionName: getCollectionName().toLocaleLowerCase(),
true, collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
).toLocaleLowerCase()} in the database and })}
does not count towards the throughput you provisioned for the database. This throughput amount will be
billed in addition to the throughput amount you provisioned at the database level.`}
/> />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
@@ -769,8 +776,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
autoComplete="off" autoComplete="off"
placeholder={ placeholder={
userContext.apiType === "Mongo" userContext.apiType === "Mongo"
? "Comma separated paths e.g. firstName,address.zipCode" ? t(Keys.panes.addCollection.uniqueKeysPlaceholderMongo)
: "Comma separated paths e.g. /firstName,/address/zipCode" : t(Keys.panes.addCollection.uniqueKeysPlaceholderSql)
} }
className="panelTextField" className="panelTextField"
value={uniqueKey} value={uniqueKey}
@@ -802,7 +809,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
styles={{ root: { padding: 0 }, label: { fontSize: 12, color: "var(--colorNeutralForeground1)" } }} styles={{ root: { padding: 0 }, label: { fontSize: 12, color: "var(--colorNeutralForeground1)" } }}
onClick={() => this.setState({ uniqueKeys: [...this.state.uniqueKeys, ""] })} onClick={() => this.setState({ uniqueKeys: [...this.state.uniqueKeys, ""] })}
> >
Add unique key {t(Keys.panes.addCollection.addUniqueKey)}
</ActionButton> </ActionButton>
</Stack> </Stack>
)} )}
@@ -823,7 +830,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
className="panelRadioBtn" className="panelRadioBtn"
checked={this.state.enableAnalyticalStore} checked={this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()} disabled={!isSynapseLinkEnabled()}
aria-label="Enable analytical store" aria-label={t(Keys.panes.addCollection.enableAnalyticalStore)}
aria-checked={this.state.enableAnalyticalStore} aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore" name="analyticalStore"
type="radio" type="radio"
@@ -832,13 +839,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0} tabIndex={0}
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)} onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
/> />
<span className="panelRadioBtnLabel">On</span> <span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.on)}</span>
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore} checked={!this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()} disabled={!isSynapseLinkEnabled()}
aria-label="Disable analytical store" aria-label={t(Keys.panes.addCollection.disableAnalyticalStore)}
aria-checked={!this.state.enableAnalyticalStore} aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore" name="analyticalStore"
type="radio" type="radio"
@@ -847,26 +854,28 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0} tabIndex={0}
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)} onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
/> />
<span className="panelRadioBtnLabel">Off</span> <span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.off)}</span>
</div> </div>
</Stack> </Stack>
{!isSynapseLinkEnabled() && ( {!isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}> <Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
Azure Synapse Link is required for creating an analytical store{" "} {t(Keys.panes.addCollection.analyticalStoreSynapseLinkRequired, {
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br /> collectionName: getCollectionName().toLocaleLowerCase(),
})}{" "}
<br />
<Link <Link
href="https://aka.ms/cosmosdb-synapselink" href="https://aka.ms/cosmosdb-synapselink"
target="_blank" target="_blank"
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink} aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
className="capacitycalculator-link" className="capacitycalculator-link"
> >
Learn more {t(Keys.common.learnMore)}
</Link> </Link>
</Text> </Text>
<DefaultButton <DefaultButton
text="Enable" text={t(Keys.panes.addCollection.enable)}
onClick={() => this.props.explorer.openEnableSynapseLinkDialog()} onClick={() => this.props.explorer.openEnableSynapseLinkDialog()}
style={{ height: 27, width: 80 }} style={{ height: 27, width: 80 }}
styles={{ label: { fontSize: 12 } }} styles={{ label: { fontSize: 12 } }}
@@ -878,7 +887,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowVectorSearchParameters() && ( {this.shouldShowVectorSearchParameters() && (
<Stack> <Stack>
<CollapsibleSectionComponent <CollapsibleSectionComponent
title="Container Vector Policy" title={t(Keys.panes.addCollection.containerVectorPolicy)}
isExpandedByDefault={false} isExpandedByDefault={false}
onExpand={() => { onExpand={() => {
scrollToSection("collapsibleVectorPolicySectionContent"); scrollToSection("collapsibleVectorPolicySectionContent");
@@ -888,6 +897,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}> <Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}> <Stack styles={{ root: { paddingLeft: 40 } }}>
<VectorEmbeddingPoliciesComponent <VectorEmbeddingPoliciesComponent
vectorEmbeddingsBaseline={[]}
vectorEmbeddings={this.state.vectorEmbeddingPolicy} vectorEmbeddings={this.state.vectorEmbeddingPolicy}
vectorIndexes={this.state.vectorIndexingPolicy} vectorIndexes={this.state.vectorIndexingPolicy}
onVectorEmbeddingChange={( onVectorEmbeddingChange={(
@@ -906,7 +916,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowFullTextSearchParameters() && ( {this.shouldShowFullTextSearchParameters() && (
<Stack> <Stack>
<CollapsibleSectionComponent <CollapsibleSectionComponent
title="Container Full Text Search Policy" title={t(Keys.panes.addCollection.containerFullTextSearchPolicy)}
isExpandedByDefault={false} isExpandedByDefault={false}
onExpand={() => { onExpand={() => {
scrollToSection("collapsibleFullTextPolicySectionContent"); scrollToSection("collapsibleFullTextPolicySectionContent");
@@ -935,7 +945,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)} )}
{!isFabricNative() && userContext.apiType !== "Tables" && ( {!isFabricNative() && userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent <CollapsibleSectionComponent
title="Advanced" title={t(Keys.panes.addCollection.advanced)}
isExpandedByDefault={false} isExpandedByDefault={false}
onExpand={() => { onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection); TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
@@ -948,23 +958,23 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal> <Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span> <span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
Indexing {t(Keys.panes.addCollection.indexing)}
</Text> </Text>
<TooltipHost <TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development." content={t(Keys.panes.addCollection.mongoIndexingTooltip)}
> >
<Icon <Icon
iconName="Info" iconName="Info"
className="panelInfoIcon" className="panelInfoIcon"
tabIndex={0} tabIndex={0}
ariaLabel="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development." ariaLabel={t(Keys.panes.addCollection.mongoIndexingTooltip)}
/> />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
<Checkbox <Checkbox
label="Create a Wildcard Index on all fields" label={t(Keys.panes.addCollection.createWildcardIndex)}
checked={this.state.createMongoWildCardIndex} checked={this.state.createMongoWildCardIndex}
styles={{ styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" }, text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -986,7 +996,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{userContext.apiType === "SQL" && ( {userContext.apiType === "SQL" && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Checkbox <Checkbox
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)" label={t(Keys.panes.addCollection.legacySdkCheckbox)}
checked={this.state.useHashV1} checked={this.state.useHashV1}
styles={{ styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" }, text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -1003,11 +1013,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
/> />
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}> <Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
<Icon iconName="InfoSolid" className="removeIcon" /> To ensure compatibility with older SDKs, the <Icon iconName="InfoSolid" className="removeIcon" /> {t(Keys.panes.addCollection.legacySdkInfo)}{" "}
created container will use a legacy partitioning scheme that supports partition key values of size
only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "}
<Link href="https://aka.ms/cosmos-large-pk" target="_blank"> <Link href="https://aka.ms/cosmos-large-pk" target="_blank">
Learn more {t(Keys.common.learnMore)}
</Link> </Link>
</Text> </Text>
</Stack> </Stack>
@@ -1018,7 +1026,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</div> </div>
{!this.props.isCopyJobFlow && ( {!this.props.isCopyJobFlow && (
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={this.state.isThroughputCapExceeded} /> <PanelFooterComponent buttonLabel={t(Keys.common.ok)} isButtonDisabled={this.state.isThroughputCapExceeded} />
)} )}
{this.state.isExecuting && ( {this.state.isExecuting && (
@@ -1026,16 +1034,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<PanelLoadingScreen /> <PanelLoadingScreen />
{this.state.teachingBubbleStep === 5 && ( {this.state.teachingBubbleStep === 5 && (
<TeachingBubble <TeachingBubble
headline="Creating sample container" headline={t(Keys.panes.addCollection.teachingBubble.step5Headline)}
target={"#loadingScreen"} target={"#loadingScreen"}
onDismiss={() => this.setState({ teachingBubbleStep: 0 })} onDismiss={() => this.setState({ teachingBubbleStep: 0 })}
styles={{ footer: { width: "100%" } }} styles={{ footer: { width: "100%" } }}
> >
A sample container is now being created and we are adding sample data for you. It should take about 1 {t(Keys.panes.addCollection.teachingBubble.step5Body)}
minute.
<br /> <br />
<br /> <br />
Once the sample container is created, review your sample dataset and follow next steps {t(Keys.panes.addCollection.teachingBubble.step5BodyFollowUp)}
<br /> <br />
<br /> <br />
<ProgressIndicator <ProgressIndicator
@@ -1044,7 +1051,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
progressTrack: { backgroundColor: "#A6A6A6" }, progressTrack: { backgroundColor: "#A6A6A6" },
progressBar: { background: "white" }, progressBar: { background: "white" },
}} }}
label="Adding sample data set" label={t(Keys.panes.addCollection.addingSampleDataSet)}
/> />
</TeachingBubble> </TeachingBubble>
)} )}
@@ -1150,8 +1157,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
private getFreeTierIndexingText(): string { private getFreeTierIndexingText(): string {
return this.state.enableIndexing return this.state.enableIndexing
? "All properties in your documents will be indexed by default for flexible and efficient queries." ? t(Keys.panes.addCollection.indexingOnInfo)
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations."; : t(Keys.panes.addCollection.indexingOffInfo);
} }
private getPartitionKeySubtext(): string { private getPartitionKeySubtext(): string {
@@ -1249,14 +1256,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
const throughput = this.state.createNewDatabase ? this.newDatabaseThroughput : this.collectionThroughput; const throughput = this.state.createNewDatabase ? this.newDatabaseThroughput : this.collectionThroughput;
if (throughput > CollectionCreation.DefaultCollectionRUs100K && !this.isCostAcknowledged) { if (throughput > CollectionCreation.DefaultCollectionRUs100K && !this.isCostAcknowledged) {
const errorMessage = this.isNewDatabaseAutoscale const errorMessage = this.isNewDatabaseAutoscale
? "Please acknowledge the estimated monthly spend." ? t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly)
: "Please acknowledge the estimated daily spend."; : t(Keys.panes.addCollection.acknowledgeSpendErrorDaily);
this.setState({ errorMessage }); this.setState({ errorMessage });
return false; return false;
} }
if (throughput > CollectionCreation.MaxRUPerPartition && !this.state.isSharded) { if (throughput > CollectionCreation.MaxRUPerPartition && !this.state.isSharded) {
this.setState({ errorMessage: "Unsharded collections support up to 10,000 RUs" }); this.setState({ errorMessage: t(Keys.panes.addCollection.unshardedMaxRuError) });
return false; return false;
} }
@@ -1270,12 +1277,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
if (this.shouldShowVectorSearchParameters()) { if (this.shouldShowVectorSearchParameters()) {
if (!this.state.vectorPolicyValidated) { if (!this.state.vectorPolicyValidated) {
this.setState({ errorMessage: "Please fix errors in container vector policy" }); this.setState({ errorMessage: t(Keys.panes.addCollection.vectorPolicyError) });
return false; return false;
} }
if (!this.state.fullTextPolicyValidated) { if (!this.state.fullTextPolicyValidated) {
this.setState({ errorMessage: "Please fix errors in container full text search polilcy" }); this.setState({ errorMessage: t(Keys.panes.addCollection.fullTextSearchPolicyError) });
return false; return false;
} }
} }

View File

@@ -3,28 +3,32 @@ import * as Constants from "Common/Constants";
import { configContext, Platform } from "ConfigContext"; import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels"; import * as DataModels from "Contracts/DataModels";
import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent"; import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
import { Keys, t } from "Localization";
import { isFabricNative } from "Platform/Fabric/FabricUtil"; import { isFabricNative } from "Platform/Fabric/FabricUtil";
import React from "react"; import React from "react";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
export function getPartitionKeyTooltipText(): string { export function getPartitionKeyTooltipText(): string {
if (userContext.apiType === "Mongo") { if (userContext.apiType === "Mongo") {
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. Its critical to choose a field that will evenly distribute your data."; return t(Keys.panes.addCollectionUtility.shardKeyTooltip);
} }
let tooltipText = `The ${getPartitionKeyName( let tooltipText = t(Keys.panes.addCollectionUtility.partitionKeyTooltip, {
true, partitionKeyName: getPartitionKeyName(true),
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`; });
if (userContext.apiType === "SQL") { if (userContext.apiType === "SQL") {
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice."; tooltipText += t(Keys.panes.addCollectionUtility.partitionKeyTooltipSqlSuffix);
} }
return tooltipText; return tooltipText;
} }
export function getPartitionKeyName(isLowerCase?: boolean): string { export function getPartitionKeyName(isLowerCase?: boolean): string {
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key"; const partitionKeyName =
userContext.apiType === "Mongo"
? t(Keys.panes.addCollectionUtility.shardKeyLabel)
: t(Keys.panes.addCollectionUtility.partitionKeyLabel);
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName; return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
} }
@@ -32,19 +36,19 @@ export function getPartitionKeyName(isLowerCase?: boolean): string {
export function getPartitionKeyPlaceHolder(index?: number): string { export function getPartitionKeyPlaceHolder(index?: number): string {
switch (userContext.apiType) { switch (userContext.apiType) {
case "Mongo": case "Mongo":
return "e.g., categoryId"; return t(Keys.panes.addCollectionUtility.shardKeyPlaceholder);
case "Gremlin": case "Gremlin":
return "e.g., /address"; return t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderDefault);
case "SQL": case "SQL":
return `${ return `${
index === undefined index === undefined
? "Required - first partition key e.g., /TenantId" ? t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderFirst)
: index === 0 : index === 0
? "second partition key e.g., /UserId" ? t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderSecond)
: "third partition key e.g., /SessionId" : t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderThird)
}`; }`;
default: default:
return "e.g., /address/zipCode"; return t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderGraph);
} }
} }
@@ -69,13 +73,12 @@ export function isFreeTierAccount(): boolean {
} }
export function UniqueKeysHeader(): JSX.Element { export function UniqueKeysHeader(): JSX.Element {
const tooltipContent = const tooltipContent = t(Keys.panes.addCollectionUtility.uniqueKeysTooltip);
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key.";
return ( return (
<Stack horizontal style={{ marginBottom: -2 }}> <Stack horizontal style={{ marginBottom: -2 }}>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
Unique keys {t(Keys.panes.addCollectionUtility.uniqueKeysLabel)}
</Text> </Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}> <TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} /> <Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
@@ -99,12 +102,11 @@ export function shouldShowAnalyticalStoreOptions(): boolean {
} }
export function AnalyticalStoreHeader(): JSX.Element { export function AnalyticalStoreHeader(): JSX.Element {
const tooltipContent = const tooltipContent = t(Keys.panes.addCollectionUtility.analyticalStoreTooltip);
"Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.";
return ( return (
<Stack horizontal style={{ marginBottom: -2 }}> <Stack horizontal style={{ marginBottom: -2 }}>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
Analytical Store {t(Keys.panes.addCollectionUtility.analyticalStoreLabel)}
</Text> </Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}> <TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} /> <Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
@@ -116,14 +118,13 @@ export function AnalyticalStoreHeader(): JSX.Element {
export function AnalyticalStorageContent(): JSX.Element { export function AnalyticalStorageContent(): JSX.Element {
return ( return (
<Text variant="small"> <Text variant="small">
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting {t(Keys.panes.addCollectionUtility.analyticalStoreDescription)}{" "}
the performance of transactional workloads.{" "}
<Link <Link
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore} aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
target="_blank" target="_blank"
href="https://aka.ms/analytical-store-overview" href="https://aka.ms/analytical-store-overview"
> >
Learn more {t(Keys.common.learnMore)}
</Link> </Link>
</Text> </Text>
); );
@@ -155,10 +156,9 @@ export function scrollToSection(id: string): void {
export function ContainerVectorPolicyTooltipContent(): JSX.Element { export function ContainerVectorPolicyTooltipContent(): JSX.Element {
return ( return (
<Text variant="small"> <Text variant="small">
Describe any properties in your data that contain vectors, so that they can be made available for similarity {t(Keys.panes.addCollectionUtility.vectorPolicyTooltip)}{" "}
queries.{" "}
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup"> <Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
Learn more {t(Keys.common.learnMore)}
</Link> </Link>
</Text> </Text>
); );

View File

@@ -29,8 +29,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
className="panelTextBold" className="panelTextBold"
variant="small" variant="small"
> >
Database Database id
id
</Text> </Text>
<StyledTooltipHostBase <StyledTooltipHostBase
content="A database is analogous to a namespace. It is the unit of management for a set of containers." content="A database is analogous to a namespace. It is the unit of management for a set of containers."
@@ -482,10 +481,8 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
} }
variant="small" variant="small"
> >
Azure Synapse Link is required for creating an analytical store Azure Synapse Link is required for creating an analytical store container. Enable Synapse Link for this Cosmos DB account.
container
. Enable Synapse Link for this Cosmos DB account.
<br /> <br />
<StyledLinkBase <StyledLinkBase
aria-label="Learn more about Azure Synapse Link." aria-label="Learn more about Azure Synapse Link."
@@ -608,7 +605,8 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
className="removeIcon" className="removeIcon"
iconName="InfoSolid" iconName="InfoSolid"
/> />
To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme that supports partition key values of size only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.
To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme that supports partition key values of size only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.
<StyledLinkBase <StyledLinkBase
href="https://aka.ms/cosmos-large-pk" href="https://aka.ms/cosmos-large-pk"

View File

@@ -1,5 +1,6 @@
import { Checkbox, Stack, Text, TextField } from "@fluentui/react"; import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility"; import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
import { Keys, t } from "Localization";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils"; import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import React, { FunctionComponent, useEffect, useState } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
@@ -40,15 +41,18 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
const isCassandraAccount: boolean = userContext.apiType === "Cassandra"; const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
const databaseLabel: string = isCassandraAccount ? "keyspace" : "database"; const databaseLabel: string = isCassandraAccount ? "keyspace" : "database";
const collectionsLabel: string = isCassandraAccount ? "tables" : "collections"; const collectionsLabel: string = isCassandraAccount ? "tables" : "collections";
const databaseIdLabel: string = isCassandraAccount ? "Keyspace id" : "Database id"; const databaseIdLabel: string = isCassandraAccount
const databaseIdPlaceHolder: string = isCassandraAccount ? "Type a new keyspace id" : "Type a new database id"; ? t(Keys.panes.addDatabase.keyspaceIdLabel)
: t(Keys.panes.addDatabase.databaseIdLabel);
const databaseIdPlaceHolder: string = t(Keys.panes.addDatabase.databaseIdPlaceholder, { databaseLabel });
const [databaseId, setDatabaseId] = useState<string>(""); const [databaseId, setDatabaseId] = useState<string>("");
const databaseIdTooltipText = `A ${ const databaseIdTooltipText = t(Keys.panes.addDatabase.databaseTooltip, { databaseLabel, collectionsLabel });
isCassandraAccount ? "keyspace" : "database"
} is a logical container of one or more ${isCassandraAccount ? "tables" : "collections"}`;
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`; const databaseLevelThroughputTooltipText = t(Keys.panes.addDatabase.shareThroughputTooltip, {
databaseLabel,
collectionsLabel,
});
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>( const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
getNewDatabaseSharedThroughputDefault(), getNewDatabaseSharedThroughputDefault(),
); );
@@ -144,15 +148,17 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
// TODO add feature flag that disables validation for customers with custom accounts // TODO add feature flag that disables validation for customers with custom accounts
if (isAutoscaleSelected) { if (isAutoscaleSelected) {
if (!AutoPilotUtils.isValidAutoPilotThroughput(throughput)) { if (!AutoPilotUtils.isValidAutoPilotThroughput(throughput)) {
setFormErrors( setFormErrors(t(Keys.panes.addDatabase.greaterThanError, { minValue: AutoPilotUtils.autoPilotThroughput1K }));
`Please enter a value greater than ${AutoPilotUtils.autoPilotThroughput1K} for autopilot throughput`,
);
return false; return false;
} }
} }
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) { if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
setFormErrors(`Please acknowledge the estimated ${isAutoscaleSelected ? "monthly" : "daily"} spend.`); setFormErrors(
isAutoscaleSelected
? t(Keys.panes.addDatabase.acknowledgeSpendErrorMonthly)
: t(Keys.panes.addDatabase.acknowledgeSpendErrorDaily),
);
return false; return false;
} }
@@ -169,7 +175,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
const props: RightPaneFormProps = { const props: RightPaneFormProps = {
formError: formErrors, formError: formErrors,
isExecuting, isExecuting,
submitButtonText: "OK", submitButtonText: t(Keys.common.ok),
isSubmitButtonDisabled: isThroughputCapExceeded, isSubmitButtonDisabled: isThroughputCapExceeded,
onSubmit, onSubmit,
}; };
@@ -187,7 +193,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
messageType="info" messageType="info"
showErrorDetails={false} showErrorDetails={false}
link={Constants.Urls.freeTierInformation} link={Constants.Urls.freeTierInformation}
linkText="Learn more" linkText={t(Keys.common.learnMore)}
/> />
)} )}
<div className="panelMainContent"> <div className="panelMainContent">
@@ -222,7 +228,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
{!isServerlessAccount() && ( {!isServerlessAccount() && (
<Stack horizontal> <Stack horizontal>
<Checkbox <Checkbox
title="Provision shared throughput" title={t(Keys.panes.addDatabase.provisionSharedThroughputTitle)}
styles={{ styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" }, text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
checkbox: { width: 12, height: 12 }, checkbox: { width: 12, height: 12 },
@@ -233,7 +239,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
}, },
}, },
}} }}
label="Provision throughput" label={t(Keys.panes.addDatabase.provisionThroughputLabel)}
checked={databaseCreateNewShared} checked={databaseCreateNewShared}
onChange={() => setDatabaseCreateNewShared(!databaseCreateNewShared)} onChange={() => setDatabaseCreateNewShared(!databaseCreateNewShared)}
/> />

View File

@@ -40,6 +40,7 @@ import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent"
import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen"; import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { Keys, t } from "Localization";
import React, { MutableRefObject, useEffect, useRef, useState } from "react"; import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import { CollectionCreation } from "Shared/Constants"; import { CollectionCreation } from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
@@ -168,19 +169,19 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
} }
if (globalSecondaryIndexThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) { if (globalSecondaryIndexThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
const errorMessage: string = "Please acknowledge the estimated monthly spend."; const errorMessage: string = t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly);
setErrorMessage(errorMessage); setErrorMessage(errorMessage);
return false; return false;
} }
if (showVectorSearchParameters()) { if (showVectorSearchParameters()) {
if (!vectorPolicyValidated) { if (!vectorPolicyValidated) {
setErrorMessage("Please fix errors in container vector policy"); setErrorMessage(t(Keys.panes.addCollection.vectorPolicyError));
return false; return false;
} }
if (!fullTextPolicyValidated) { if (!fullTextPolicyValidated) {
setErrorMessage("Please fix errors in container full text search policy"); setErrorMessage(t(Keys.panes.addCollection.fullTextSearchPolicyError));
return false; return false;
} }
} }
@@ -307,7 +308,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
<Stack horizontal> <Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span> <span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
Global secondary index container id {t(Keys.panes.addGlobalSecondaryIndex.globalSecondaryIndexId)}
</Text> </Text>
</Stack> </Stack>
<input <input
@@ -318,7 +319,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
autoComplete="off" autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source} pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription} title={ValidCosmosDbIdDescription}
placeholder={`e.g., indexbyEmailId`} placeholder={t(Keys.panes.addGlobalSecondaryIndex.globalSecondaryIndexIdPlaceholder)}
size={40} size={40}
className="panelTextField" className="panelTextField"
value={globalSecondaryIndexId} value={globalSecondaryIndexId}
@@ -336,7 +337,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views" href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
target="blank" target="blank"
> >
Learn more about defining global secondary indexes. {t(Keys.panes.addGlobalSecondaryIndex.projectionQueryTooltip)}
</Link> </Link>
} }
> >
@@ -349,7 +350,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
aria-required aria-required
required required
autoComplete="off" autoComplete="off"
placeholder={"SELECT c.email, c.accountId FROM c"} placeholder={t(Keys.panes.addGlobalSecondaryIndex.projectionQueryPlaceholder)}
size={40} size={40}
className="panelTextField" className="panelTextField"
value={definition || ""} value={definition || ""}
@@ -393,7 +394,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
<AdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} /> <AdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} />
</Stack> </Stack>
</div> </div>
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={isThroughputCapExceeded} /> <PanelFooterComponent buttonLabel={t(Keys.common.ok)} isButtonDisabled={isThroughputCapExceeded} />
{isExecuting && <PanelLoadingScreen />} {isExecuting && <PanelLoadingScreen />}
</form> </form>
); );

View File

@@ -40,6 +40,7 @@ export const VectorSearchComponent = (props: VectorSearchComponentProps): JSX.El
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}> <Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}> <Stack styles={{ root: { paddingLeft: 40 } }}>
<VectorEmbeddingPoliciesComponent <VectorEmbeddingPoliciesComponent
vectorEmbeddingsBaseline={[]}
vectorEmbeddings={vectorEmbeddingPolicy} vectorEmbeddings={vectorEmbeddingPolicy}
vectorIndexes={vectorIndexingPolicy} vectorIndexes={vectorIndexingPolicy}
onVectorEmbeddingChange={( onVectorEmbeddingChange={(

View File

@@ -146,6 +146,7 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
explorer={ explorer={
Explorer { Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],

View File

@@ -2,14 +2,15 @@ import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } fro
import * as Constants from "Common/Constants"; import * as Constants from "Common/Constants";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip"; import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { useSidePanel } from "hooks/useSidePanel";
import { Keys, t } from "Localization";
import React, { FunctionComponent, useState } from "react";
import * as SharedConstants from "Shared/Constants"; import * as SharedConstants from "Shared/Constants";
import { Action } from "Shared/Telemetry/TelemetryConstants"; import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { isServerlessAccount } from "Utils/CapabilityUtils"; import { isServerlessAccount } from "Utils/CapabilityUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils"; import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { CassandraAPIDataClient } from "../../Tables/TableDataClient"; import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
@@ -71,8 +72,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) { if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
const errorMessage = const errorMessage =
isNewKeySpaceAutoscale || isTableAutoscale isNewKeySpaceAutoscale || isTableAutoscale
? "Please acknowledge the estimated monthly spend." ? t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly)
: "Please acknowledge the estimated daily spend."; : t(Keys.panes.addCollection.acknowledgeSpendErrorDaily);
setFormError(errorMessage); setFormError(errorMessage);
return; return;
} }
@@ -149,7 +150,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
const props: RightPaneFormProps = { const props: RightPaneFormProps = {
formError, formError,
isExecuting, isExecuting,
submitButtonText: "OK", submitButtonText: t(Keys.common.ok),
isSubmitButtonDisabled: isThroughputCapExceeded, isSubmitButtonDisabled: isThroughputCapExceeded,
onSubmit, onSubmit,
}; };
@@ -161,7 +162,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<Stack horizontal> <Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span> <span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
Keyspace name <InfoTooltip>Select an existing keyspace or enter a new keyspace id.</InfoTooltip> {t(Keys.panes.cassandraAddCollection.keyspaceLabel)}{" "}
<InfoTooltip>{t(Keys.panes.cassandraAddCollection.keyspaceTooltip)}</InfoTooltip>
</Text> </Text>
</Stack> </Stack>
@@ -179,7 +181,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
setExistingKeyspaceId(""); setExistingKeyspaceId("");
}} }}
/> />
<span className="panelRadioBtnLabel">Create new</span> <span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.createNew)}</span>
<input <input
className="panelRadioBtn" className="panelRadioBtn"
@@ -193,7 +195,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
setIsKeyspaceShared(false); setIsKeyspaceShared(false);
}} }}
/> />
<span className="panelRadioBtnLabel">Use existing</span> <span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.useExisting)}</span>
</Stack> </Stack>
{keyspaceCreateNew && ( {keyspaceCreateNew && (
@@ -275,9 +277,9 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<Stack horizontal> <Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span> <span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small"> <Text className="panelTextBold" variant="small">
Enter CQL command to create the table.{" "} {t(Keys.panes.cassandraAddCollection.tableIdLabel)}{" "}
<Link className="underlinedLink" href="https://aka.ms/cassandra-create-table" target="_blank"> <Link className="underlinedLink" href="https://aka.ms/cassandra-create-table" target="_blank">
Learn More {t(Keys.common.learnMore)}
</Link> </Link>
</Text> </Text>
</Stack> </Stack>
@@ -295,7 +297,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
autoComplete="off" autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source} pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription} title={ValidCosmosDbIdDescription}
placeholder="Enter table Id" placeholder={t(Keys.panes.cassandraAddCollection.enterTableId)}
size={20} size={20}
value={tableId} value={tableId}
onChange={(e, newValue) => setTableId(newValue)} onChange={(e, newValue) => setTableId(newValue)}
@@ -307,7 +309,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
multiline multiline
id="editor-area" id="editor-area"
rows={5} rows={5}
ariaLabel="Table schema" ariaLabel={t(Keys.panes.cassandraAddCollection.tableSchemaAriaLabel)}
value={userTableQuery} value={userTableQuery}
onChange={(e, newValue) => setUserTableQuery(newValue)} onChange={(e, newValue) => setUserTableQuery(newValue)}
/> />
@@ -318,17 +320,12 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<input <input
type="checkbox" type="checkbox"
id="tableSharedThroughput" id="tableSharedThroughput"
title="Provision dedicated throughput for this table" title={t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughput)}
checked={dedicateTableThroughput} checked={dedicateTableThroughput}
onChange={(e) => setDedicateTableThroughput(e.target.checked)} onChange={(e) => setDedicateTableThroughput(e.target.checked)}
/> />
<span>Provision dedicated throughput for this table</span> <span>{t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughput)}</span>
<InfoTooltip> <InfoTooltip>{t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughputTooltip)}</InfoTooltip>
You can optionally provision dedicated throughput for a table within a keyspace that has throughput
provisioned. This dedicated throughput amount will not be shared with other tables in the keyspace and
does not count towards the throughput you provisioned for the keyspace. This throughput amount will be
billed in addition to the throughput amount you provisioned at the keyspace level.
</InfoTooltip>
</Stack> </Stack>
)} )}
{!isServerlessAccount() && (!isKeyspaceShared || dedicateTableThroughput) && ( {!isServerlessAccount() && (!isKeyspaceShared || dedicateTableThroughput) && (

View File

@@ -26,6 +26,7 @@ import {
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm"; import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { Keys, t } from "Localization";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils"; import { getCollectionName } from "Utils/APITypeUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils"; import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
@@ -72,7 +73,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
await createDataTransferJob(); await createDataTransferJob();
await onClose(); await onClose();
} catch (error) { } catch (error) {
handleError(error, "ChangePartitionKey", "Failed to start data transfer job"); handleError(error, "ChangePartitionKey", t(Keys.panes.changePartitionKey.failedToStartError));
} }
setIsExecuting(false); setIsExecuting(false);
useSidePanel.getState().closeSidePanel(); useSidePanel.getState().closeSidePanel();
@@ -133,17 +134,21 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
}; };
return ( return (
<RightPaneForm formError={formError} isExecuting={isExecuting} onSubmit={submit} submitButtonText="OK"> <RightPaneForm
formError={formError}
isExecuting={isExecuting}
onSubmit={submit}
submitButtonText={t(Keys.common.ok)}
>
<Stack tokens={{ childrenGap: 10 }} className="panelMainContent"> <Stack tokens={{ childrenGap: 10 }} className="panelMainContent">
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}> <Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
When changing a containers partition key, you will need to create a destination container with the correct {t(Keys.panes.changePartitionKey.description)}&nbsp;
partition key. You may also select an existing destination container.&nbsp;
<Link <Link
href="https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy#container-copy-within-an-azure-cosmos-db-account" href="https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy#container-copy-within-an-azure-cosmos-db-account"
target="_blank" target="_blank"
underline underline
> >
Learn more {t(Keys.common.learnMore)}
</Link> </Link>
</Text> </Text>
<Stack> <Stack>
@@ -218,14 +223,18 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
</Text> </Text>
<TooltipHost <TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`} content={t(Keys.panes.changePartitionKey.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
> >
<Icon <Icon
role="button" role="button"
iconName="Info" iconName="Info"
className="panelInfoIcon" className="panelInfoIcon"
tabIndex={0} tabIndex={0}
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`} ariaLabel={t(Keys.panes.changePartitionKey.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
/> />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
@@ -239,10 +248,14 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
autoComplete="off" autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source} pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription} title={ValidCosmosDbIdDescription}
placeholder={`e.g., ${getCollectionName()}1`} placeholder={t(Keys.panes.changePartitionKey.collectionIdPlaceholder, {
collectionName: getCollectionName(),
})}
size={40} size={40}
className="panelTextField" className="panelTextField"
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`} aria-label={t(Keys.panes.changePartitionKey.collectionIdAriaLabel, {
collectionName: getCollectionName(),
})}
value={targetCollectionId} value={targetCollectionId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setTargetCollectionId(event.target.value)} onChange={(event: React.ChangeEvent<HTMLInputElement>) => setTargetCollectionId(event.target.value)}
/> />
@@ -349,7 +362,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition} disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])} onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
> >
Add hierarchical partition key {t(Keys.panes.addCollection.addPartitionKey)}
</DefaultButton> </DefaultButton>
{subPartitionKeys.length > 0 && ( {subPartitionKeys.length > 0 && (
<Text <Text
@@ -357,11 +370,10 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
variant="small" variant="small"
style={{ color: "var(--colorNeutralForeground1)" }} style={{ color: "var(--colorNeutralForeground1)" }}
> >
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to <Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} />{" "}
partition your data with up to three levels of keys for better data distribution. Requires .NET V3, {t(Keys.panes.addCollection.hierarchicalPartitionKeyInfo)}{" "}
Java V4 SDK, or preview JavaScript V3 SDK.{" "}
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank"> <Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
Learn more {t(Keys.common.learnMore)}
</Link> </Link>
</Text> </Text>
)} )}
@@ -377,14 +389,18 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
</Text> </Text>
<TooltipHost <TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge} directionalHint={DirectionalHint.bottomLeftEdge}
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`} content={t(Keys.panes.changePartitionKey.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
> >
<Icon <Icon
role="button" role="button"
iconName="Info" iconName="Info"
className="panelInfoIcon" className="panelInfoIcon"
tabIndex={0} tabIndex={0}
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`} ariaLabel={t(Keys.panes.changePartitionKey.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
/> />
</TooltipHost> </TooltipHost>
</Stack> </Stack>
@@ -400,7 +416,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
}} }}
defaultSelectedKey={targetCollectionId} defaultSelectedKey={targetCollectionId}
responsiveMode={999} responsiveMode={999}
ariaLabel="Existing Containers" ariaLabel={t(Keys.panes.changePartitionKey.existingContainers)}
/> />
</Stack> </Stack>
)} )}

View File

@@ -4,6 +4,7 @@ import { HttpStatusCodes, PoolIdType } from "../../../Common/Constants";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient"; import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
import { Keys, t } from "Localization";
import * as GitHubUtils from "../../../Utils/GitHubUtils"; import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
@@ -82,14 +83,14 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
const notebookContentItem = await copyNotebook(selectedLocation); const notebookContentItem = await copyNotebook(selectedLocation);
if (!notebookContentItem) { if (!notebookContentItem) {
throw new Error(`Failed to upload ${name}`); throw new Error(t(Keys.panes.copyNotebook.uploadFailedError, { name }));
} }
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${name} to ${destination}`); NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${name} to ${destination}`);
closeSidePanel(); closeSidePanel();
} catch (error) { } catch (error) {
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
setFormError(`Failed to copy ${name} to ${destination}`); setFormError(t(Keys.panes.copyNotebook.copyFailedError, { name, destination }));
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError); handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError);
} finally { } finally {
clearMessage && clearMessage(); clearMessage && clearMessage();
@@ -136,7 +137,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
const props: RightPaneFormProps = { const props: RightPaneFormProps = {
formError, formError,
isExecuting: isExecuting, isExecuting: isExecuting,
submitButtonText: "OK", submitButtonText: t(Keys.common.ok),
onSubmit: () => submit(), onSubmit: () => submit(),
}; };

View File

@@ -12,6 +12,7 @@ import {
import { GitHubReposTitle } from "Explorer/Tree/ResourceTree"; import { GitHubReposTitle } from "Explorer/Tree/ResourceTree";
import React, { FormEvent, FunctionComponent } from "react"; import React, { FormEvent, FunctionComponent } from "react";
import { IPinnedRepo } from "../../../Juno/JunoClient"; import { IPinnedRepo } from "../../../Juno/JunoClient";
import { Keys, t } from "Localization";
import * as GitHubUtils from "../../../Utils/GitHubUtils"; import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { useNotebook } from "../../Notebook/useNotebook"; import { useNotebook } from "../../Notebook/useNotebook";
@@ -96,8 +97,8 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
return options; return options;
}; };
const dropDownProps: IDropdownProps = { const dropDownProps: IDropdownProps = {
label: "Location", label: t(Keys.panes.copyNotebook.location),
ariaLabel: "Location", ariaLabel: t(Keys.panes.copyNotebook.locationAriaLabel),
placeholder: "Select an option", placeholder: "Select an option",
onRenderTitle: onRenderDropDownTitle, onRenderTitle: onRenderDropDownTitle,
onRenderOption: onRenderDropDownOption, onRenderOption: onRenderDropDownOption,
@@ -109,7 +110,7 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
<div className="paneMainContent"> <div className="paneMainContent">
<Stack tokens={{ childrenGap: 10 }}> <Stack tokens={{ childrenGap: 10 }}>
<Stack.Item> <Stack.Item>
<Label htmlFor="notebookName">Name</Label> <Label htmlFor="notebookName">{t(Keys.panes.copyNotebook.name)}</Label>
<Text id="notebookName">{name}</Text> <Text id="notebookName">{name}</Text>
</Stack.Item> </Stack.Item>

View File

@@ -4,6 +4,7 @@ import DeleteFeedback from "Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { deleteCollection } from "Common/dataAccess/deleteCollection"; import { deleteCollection } from "Common/dataAccess/deleteCollection";
import { Collection } from "Contracts/ViewModels"; import { Collection } from "Contracts/ViewModels";
import { Keys, t } from "Localization";
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
@@ -17,6 +18,24 @@ import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
const themedTextFieldStyles = {
fieldGroup: {
width: 300,
backgroundColor: "var(--colorNeutralBackground1)",
borderColor: "var(--colorNeutralStroke1)",
selectors: {
":hover": { borderColor: "var(--colorNeutralStroke1Hover)" },
},
},
field: {
color: "var(--colorNeutralForeground1)",
backgroundColor: "var(--colorNeutralBackground1)",
},
subComponentStyles: {
label: { root: { color: "var(--colorNeutralForeground1)" } },
},
};
export interface DeleteCollectionConfirmationPaneProps { export interface DeleteCollectionConfirmationPaneProps {
refreshDatabases: () => Promise<void>; refreshDatabases: () => Promise<void>;
} }
@@ -34,12 +53,15 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
useDatabases.getState().isLastCollection() && !useDatabases.getState().findSelectedDatabase()?.isDatabaseShared(); useDatabases.getState().isLastCollection() && !useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
const collectionName = getCollectionName().toLocaleLowerCase(); const collectionName = getCollectionName().toLocaleLowerCase();
const paneTitle = "Delete " + collectionName; const paneTitle = t(Keys.panes.deleteCollection.panelTitle, { collectionName });
const onSubmit = async (): Promise<void> => { const onSubmit = async (): Promise<void> => {
const collection = useSelectedNode.getState().findSelectedCollection(); const collection = useSelectedNode.getState().findSelectedCollection();
if (!collection || inputCollectionName !== collection.id()) { if (!collection || inputCollectionName !== collection.id()) {
const errorMessage = "Input id " + inputCollectionName + " does not match the selected " + collection.id(); const errorMessage = t(Keys.panes.deleteCollection.inputMismatch, {
input: inputCollectionName,
selectedId: collection.id(),
});
setFormError(errorMessage); setFormError(errorMessage);
NotificationConsoleUtils.logConsoleError( NotificationConsoleUtils.logConsoleError(
`Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}`, `Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}`,
@@ -106,23 +128,30 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
const props: RightPaneFormProps = { const props: RightPaneFormProps = {
formError: formError, formError: formError,
isExecuting, isExecuting,
submitButtonText: "OK", submitButtonText: t(Keys.common.ok),
onSubmit, onSubmit,
}; };
const confirmContainer = `Confirm by typing the ${collectionName.toLowerCase()} id`; const confirmContainer = t(Keys.panes.deleteCollection.confirmPrompt, {
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${collectionName}?`; collectionName: collectionName.toLowerCase(),
});
const reasonInfo =
t(Keys.panes.deleteCollection.feedbackTitle) +
" " +
t(Keys.panes.deleteCollection.feedbackReason, { collectionName });
return ( return (
<RightPaneForm {...props}> <RightPaneForm {...props}>
<div className="panelFormWrapper"> <div className="panelFormWrapper">
<div className="panelMainContent"> <div className="panelMainContent">
<div className="confirmDeleteInput"> <div className="confirmDeleteInput">
<span className="mandatoryStar">* </span> <span className="mandatoryStar">* </span>
<Text variant="small">Confirm by typing the {collectionName.toLowerCase()} id</Text> <Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
{confirmContainer}
</Text>
<TextField <TextField
id="confirmCollectionId" id="confirmCollectionId"
autoFocus autoFocus
value={inputCollectionName} value={inputCollectionName}
styles={{ fieldGroup: { width: 300 } }} styles={themedTextFieldStyles}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {
setInputCollectionName(newInput); setInputCollectionName(newInput);
}} }}
@@ -132,15 +161,15 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
</div> </div>
{shouldRecordFeedback() && ( {shouldRecordFeedback() && (
<div className="deleteCollectionFeedback"> <div className="deleteCollectionFeedback">
<Text variant="small" block> <Text variant="small" block style={{ color: "var(--colorNeutralForeground1)" }}>
Help us improve Azure Cosmos DB! {t(Keys.panes.deleteCollection.feedbackTitle)}
</Text> </Text>
<Text variant="small" block> <Text variant="small" block style={{ color: "var(--colorNeutralForeground1)" }}>
What is the reason why you are deleting this {collectionName}? {t(Keys.panes.deleteCollection.feedbackReason, { collectionName })}
</Text> </Text>
<TextField <TextField
id="deleteCollectionFeedbackInput" id="deleteCollectionFeedbackInput"
styles={{ fieldGroup: { width: 300 } }} styles={themedTextFieldStyles}
multiline multiline
value={deleteCollectionFeedback} value={deleteCollectionFeedback}
rows={3} rows={3}

View File

@@ -29,14 +29,22 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
* *
</span> </span>
<Text <Text
style={
{
"color": "var(--colorNeutralForeground1)",
}
}
variant="small" variant="small"
> >
<span <span
className="css-109" className="css-109"
style={
{
"color": "var(--colorNeutralForeground1)",
}
}
> >
Confirm by typing the Confirm by typing the container id
container
id
</span> </span>
</Text> </Text>
<StyledTextFieldBase <StyledTextFieldBase
@@ -47,9 +55,27 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
required={true} required={true}
styles={ styles={
{ {
"field": {
"backgroundColor": "var(--colorNeutralBackground1)",
"color": "var(--colorNeutralForeground1)",
},
"fieldGroup": { "fieldGroup": {
"backgroundColor": "var(--colorNeutralBackground1)",
"borderColor": "var(--colorNeutralStroke1)",
"selectors": {
":hover": {
"borderColor": "var(--colorNeutralStroke1Hover)",
},
},
"width": 300, "width": 300,
}, },
"subComponentStyles": {
"label": {
"root": {
"color": "var(--colorNeutralForeground1)",
},
},
},
} }
} }
value="" value=""

View File

@@ -5,6 +5,7 @@ import DeleteFeedback from "Common/DeleteFeedback";
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { deleteDatabase } from "Common/dataAccess/deleteDatabase"; import { deleteDatabase } from "Common/dataAccess/deleteDatabase";
import { Collection, Database } from "Contracts/ViewModels"; import { Collection, Database } from "Contracts/ViewModels";
import { Keys, t } from "Localization";
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
@@ -19,6 +20,24 @@ import { useSelectedNode } from "../useSelectedNode";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent"; import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent";
import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm";
const themedTextFieldStyles = {
fieldGroup: {
width: 300,
backgroundColor: "var(--colorNeutralBackground1)",
borderColor: "var(--colorNeutralStroke1)",
selectors: {
":hover": { borderColor: "var(--colorNeutralStroke1Hover)" },
},
},
field: {
color: "var(--colorNeutralForeground1)",
backgroundColor: "var(--colorNeutralBackground1)",
},
subComponentStyles: {
label: { root: { color: "var(--colorNeutralForeground1)" } },
},
};
interface DeleteDatabaseConfirmationPanelProps { interface DeleteDatabaseConfirmationPanelProps {
refreshDatabases: () => Promise<void>; refreshDatabases: () => Promise<void>;
} }
@@ -38,11 +57,19 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
const submit = async (): Promise<void> => { const submit = async (): Promise<void> => {
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) { if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
setFormError( setFormError(
`Input ${getDatabaseName()} name "${databaseInput}" does not match the selected ${getDatabaseName()} "${selectedDatabase.id()}"`, t(Keys.panes.deleteDatabase.inputMismatch, {
databaseName: getDatabaseName(),
input: databaseInput,
selectedId: selectedDatabase.id(),
}),
); );
logConsoleError(`Error while deleting ${getDatabaseName()} ${selectedDatabase && selectedDatabase.id()}`); logConsoleError(`Error while deleting ${getDatabaseName()} ${selectedDatabase && selectedDatabase.id()}`);
logConsoleError( logConsoleError(
`Input ${getDatabaseName()} name "${databaseInput}" does not match the selected ${getDatabaseName()} "${selectedDatabase.id()}"`, t(Keys.panes.deleteDatabase.inputMismatch, {
databaseName: getDatabaseName(),
input: databaseInput,
selectedId: selectedDatabase.id(),
}),
); );
return; return;
} }
@@ -114,30 +141,34 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
const props: RightPaneFormProps = { const props: RightPaneFormProps = {
formError, formError,
isExecuting: isLoading, isExecuting: isLoading,
submitButtonText: "OK", submitButtonText: t(Keys.common.ok),
onSubmit: () => submit(), onSubmit: () => submit(),
}; };
const errorProps: PanelInfoErrorProps = { const errorProps: PanelInfoErrorProps = {
messageType: "warning", messageType: "warning",
showErrorDetails: false, showErrorDetails: false,
message: message: t(Keys.panes.deleteDatabase.warningMessage),
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
}; };
const confirmDatabase = `Confirm by typing the ${getDatabaseName()} id (name)`; const confirmDatabase = t(Keys.panes.deleteDatabase.confirmPrompt, { databaseName: getDatabaseName() });
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${getDatabaseName()}?`; const reasonInfo =
t(Keys.panes.deleteDatabase.feedbackTitle) +
" " +
t(Keys.panes.deleteDatabase.feedbackReason, { databaseName: getDatabaseName() });
return ( return (
<RightPaneForm {...props}> <RightPaneForm {...props}>
{!formError && <PanelInfoErrorComponent {...errorProps} />} {!formError && <PanelInfoErrorComponent {...errorProps} />}
<div className="panelMainContent"> <div className="panelMainContent">
<div className="confirmDeleteInput"> <div className="confirmDeleteInput">
<span className="mandatoryStar">* </span> <span className="mandatoryStar">* </span>
<Text variant="small">{confirmDatabase}</Text> <Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
{confirmDatabase}
</Text>
<TextField <TextField
id="confirmDatabaseId" id="confirmDatabaseId"
data-test="Input:confirmDatabaseId" data-test="Input:confirmDatabaseId"
autoFocus autoFocus
styles={{ fieldGroup: { width: 300 } }} styles={themedTextFieldStyles}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {
setDatabaseInput(newInput); setDatabaseInput(newInput);
}} }}
@@ -147,15 +178,15 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
</div> </div>
{isLastNonEmptyDatabase() && ( {isLastNonEmptyDatabase() && (
<div className="deleteDatabaseFeedback"> <div className="deleteDatabaseFeedback">
<Text variant="small" block> <Text variant="small" block style={{ color: "var(--colorNeutralForeground1)" }}>
Help us improve Azure Cosmos DB! {t(Keys.panes.deleteDatabase.feedbackTitle)}
</Text> </Text>
<Text variant="small" block> <Text variant="small" block style={{ color: "var(--colorNeutralForeground1)" }}>
What is the reason why you are deleting this {getDatabaseName()}? {t(Keys.panes.deleteDatabase.feedbackReason, { databaseName: getDatabaseName() })}
</Text> </Text>
<TextField <TextField
id="deleteDatabaseFeedbackInput" id="deleteDatabaseFeedbackInput"
styles={{ fieldGroup: { width: 300 } }} styles={themedTextFieldStyles}
multiline multiline
rows={3} rows={3}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {

View File

@@ -3,6 +3,7 @@ import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useRef, useState } from "react"; import React, { FunctionComponent, useRef, useState } from "react";
import AddPropertyIcon from "../../../../images/Add-property.svg"; import AddPropertyIcon from "../../../../images/Add-property.svg";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { Keys, t } from "Localization";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import StoredProcedure from "../../Tree/StoredProcedure"; import StoredProcedure from "../../Tree/StoredProcedure";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
@@ -45,8 +46,8 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
}; };
const setInvalidParamError = (invalidParam: string): void => { const setInvalidParamError = (invalidParam: string): void => {
setFormError(`Invalid param specified: ${invalidParam}`); setFormError(t(Keys.panes.executeStoredProcedure.invalidParamError, { invalidParam }));
logConsoleError(`Invalid param specified: ${invalidParam} is not a valid literal value`); logConsoleError(t(Keys.panes.executeStoredProcedure.invalidParamConsoleError, { invalidParam }));
}; };
const submit = (): void => { const submit = (): void => {
@@ -96,7 +97,7 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
const props: RightPaneFormProps = { const props: RightPaneFormProps = {
formError: formError, formError: formError,
isExecuting: isLoading, isExecuting: isLoading,
submitButtonText: "Execute", submitButtonText: t(Keys.common.execute),
onSubmit: () => submit(), onSubmit: () => submit(),
}; };
@@ -107,9 +108,9 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
inputParameters.push( inputParameters.push(
<InputParameter <InputParameter
key={paramKeyValue.text + i} key={paramKeyValue.text + i}
dropdownLabel={i === 0 ? "Key" : ""} dropdownLabel={i === 0 ? t(Keys.panes.executeStoredProcedure.key) : ""}
inputParameterTitle={i === 0 ? "Enter input parameters (if any)" : ""} inputParameterTitle={i === 0 ? t(Keys.panes.executeStoredProcedure.enterInputParameters) : ""}
inputLabel={i === 0 ? "Param" : ""} inputLabel={i === 0 ? t(Keys.panes.executeStoredProcedure.param) : ""}
isAddRemoveVisible={true} isAddRemoveVisible={true}
onDeleteParamKeyPress={() => deleteParamAtIndex(i)} onDeleteParamKeyPress={() => deleteParamAtIndex(i)}
onAddNewParamKeyPress={() => addNewParamAtIndex(i + 1)} onAddNewParamKeyPress={() => addNewParamAtIndex(i + 1)}
@@ -130,9 +131,9 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
<RightPaneForm {...props}> <RightPaneForm {...props}>
<div className="panelMainContent"> <div className="panelMainContent">
<InputParameter <InputParameter
dropdownLabel="Key" dropdownLabel={t(Keys.panes.executeStoredProcedure.key)}
inputParameterTitle="Partition key value" inputParameterTitle={t(Keys.panes.executeStoredProcedure.partitionKeyValue)}
inputLabel="Value" inputLabel={t(Keys.panes.executeStoredProcedure.value)}
isAddRemoveVisible={false} isAddRemoveVisible={false}
onParamValueChange={(_event, newInput?: string) => (partitionValueRef.current = newInput)} onParamValueChange={(_event, newInput?: string) => (partitionValueRef.current = newInput)}
onParamKeyChange={(_event: React.FormEvent<HTMLDivElement>, item: IDropdownOption) => onParamKeyChange={(_event: React.FormEvent<HTMLDivElement>, item: IDropdownOption) =>
@@ -143,8 +144,8 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
/> />
{getInputParameterComponent()} {getInputParameterComponent()}
<Stack horizontal onClick={() => addNewParamAtLastIndex()} tabIndex={0}> <Stack horizontal onClick={() => addNewParamAtLastIndex()} tabIndex={0}>
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" /> <Image {...imageProps} src={AddPropertyIcon} alt={t(Keys.panes.executeStoredProcedure.addParam)} />
<Text className="addNewParamStyle">Add New Param</Text> <Text className="addNewParamStyle">{t(Keys.panes.executeStoredProcedure.addNewParam)}</Text>
</Stack> </Stack>
</div> </div>
</RightPaneForm> </RightPaneForm>

View File

@@ -11,6 +11,7 @@ import {
import React, { FunctionComponent } from "react"; import React, { FunctionComponent } from "react";
import AddPropertyIcon from "../../../../images/Add-property.svg"; import AddPropertyIcon from "../../../../images/Add-property.svg";
import EntityCancelIcon from "../../../../images/Entity_cancel.svg"; import EntityCancelIcon from "../../../../images/Entity_cancel.svg";
import { Keys, t } from "Localization";
const dropdownStyles: Partial<IDropdownStyles> = { dropdown: { width: 100 } }; const dropdownStyles: Partial<IDropdownStyles> = { dropdown: { width: 100 } };
const options = [ const options = [
@@ -74,7 +75,7 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
<Image <Image
{...imageProps} {...imageProps}
src={EntityCancelIcon} src={EntityCancelIcon}
alt="Delete param" alt={t(Keys.panes.executeStoredProcedure.deleteParam)}
id="deleteparam" id="deleteparam"
role="button" role="button"
onClick={onDeleteParamKeyPress} onClick={onDeleteParamKeyPress}
@@ -84,7 +85,7 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
<Image <Image
{...imageProps} {...imageProps}
src={AddPropertyIcon} src={AddPropertyIcon}
alt="Add param" alt={t(Keys.panes.executeStoredProcedure.addParam)}
id="addparam" id="addparam"
role="button" role="button"
onClick={onAddNewParamKeyPress} onClick={onAddNewParamKeyPress}

View File

@@ -18,6 +18,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
{ {
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],

View File

@@ -1,5 +1,6 @@
import React, { FunctionComponent } from "react"; import React, { FunctionComponent } from "react";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { Keys, t } from "Localization";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { GraphStyleComponent } from "../../Graph/GraphStyleComponent/GraphStyleComponent"; import { GraphStyleComponent } from "../../Graph/GraphStyleComponent/GraphStyleComponent";
import { IGraphConfig } from "../../Tabs/GraphTab"; import { IGraphConfig } from "../../Tabs/GraphTab";
@@ -17,7 +18,7 @@ export const GraphStylingPanel: FunctionComponent<GraphStylingProps> = ({
}: GraphStylingProps): JSX.Element => { }: GraphStylingProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel); const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const buttonLabel = "Ok"; const buttonLabel = t(Keys.common.ok);
const submit = (event: React.FormEvent<HTMLFormElement>) => { const submit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();

View File

@@ -5,6 +5,7 @@ import folderIcon from "../../../../images/folder_16x16.svg";
import { logError } from "../../../Common/Logger"; import { logError } from "../../../Common/Logger";
import { Collection } from "../../../Contracts/ViewModels"; import { Collection } from "../../../Contracts/ViewModels";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { Keys, t } from "Localization";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
@@ -33,8 +34,8 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
const submit = async (): Promise<void> => { const submit = async (): Promise<void> => {
setFormError(""); setFormError("");
if (!selectedFiles || selectedFiles.length === 0) { if (!selectedFiles || selectedFiles.length === 0) {
setFormError("No file specified"); setFormError(t(Keys.panes.loadQuery.noFileSpecifiedError));
logConsoleError("Could not load query -- No file specified. Please input a file."); logConsoleError(t(Keys.panes.loadQuery.noFileSpecifiedError));
return; return;
} }
@@ -48,7 +49,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
setLoadingFalse(); setLoadingFalse();
} catch (error) { } catch (error) {
setLoadingFalse(); setLoadingFalse();
setFormError("Failed to load query"); setFormError(t(Keys.panes.loadQuery.failedToLoadQueryError));
logConsoleError(`Failed to load query from file ${file.name}: ${error}`); logConsoleError(`Failed to load query from file ${file.name}: ${error}`);
} }
}; };
@@ -71,7 +72,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
}; };
reader.onerror = (): void => { reader.onerror = (): void => {
setFormError("Failed to load query"); setFormError(t(Keys.panes.loadQuery.failedToLoadQueryFromFileError, { fileName: file.name }));
logConsoleError(`Failed to load query from file ${file.name}`); logConsoleError(`Failed to load query from file ${file.name}`);
}; };
return reader.readAsText(file); return reader.readAsText(file);
@@ -79,7 +80,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
const props: RightPaneFormProps = { const props: RightPaneFormProps = {
formError: formError, formError: formError,
isExecuting: isLoading, isExecuting: isLoading,
submitButtonText: "Load", submitButtonText: t(Keys.common.load),
onSubmit: () => submit(), onSubmit: () => submit(),
}; };
@@ -90,7 +91,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
<Stack horizontal> <Stack horizontal>
<TextField <TextField
id="confirmCollectionId" id="confirmCollectionId"
label="Select a query document" label={t(Keys.panes.loadQuery.selectFilesToOpen)}
value={selectedFileName} value={selectedFileName}
autoFocus autoFocus
readOnly readOnly

View File

@@ -1,6 +1,7 @@
import { useBoolean } from "@fluentui/react-hooks"; import { useBoolean } from "@fluentui/react-hooks";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { Keys, t } from "Localization";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { NewVertexComponent } from "../../Graph/NewVertexComponent/NewVertexComponent"; import { NewVertexComponent } from "../../Graph/NewVertexComponent/NewVertexComponent";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
@@ -41,7 +42,7 @@ export const NewVertexPanel: FunctionComponent<INewVertexPanelProps> = ({
const props: RightPaneFormProps = { const props: RightPaneFormProps = {
formError: errorMessage, formError: errorMessage,
isExecuting: isLoading, isExecuting: isLoading,
submitButtonText: "OK", submitButtonText: t(Keys.common.ok),
onSubmit: () => submit(), onSubmit: () => submit(),
}; };

View File

@@ -1,4 +1,5 @@
import { Icon, Link, Stack, Text } from "@fluentui/react"; import { Icon, Link, Stack, Text } from "@fluentui/react";
import { Keys, t } from "Localization";
import React from "react"; import React from "react";
import { useNotificationConsole } from "../../hooks/useNotificationConsole"; import { useNotificationConsole } from "../../hooks/useNotificationConsole";
@@ -20,13 +21,17 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
}: PanelInfoErrorProps): JSX.Element => { }: PanelInfoErrorProps): JSX.Element => {
const expandConsole = useNotificationConsole((state) => state.expandConsole); const expandConsole = useNotificationConsole((state) => state.expandConsole);
let icon: JSX.Element = <Icon iconName="InfoSolid" className="panelLargeInfoIcon" aria-label="Infomation" />; let icon: JSX.Element = (
<Icon iconName="InfoSolid" className="panelLargeInfoIcon" aria-label={t(Keys.panes.panelInfo.information)} />
);
if (messageType === "error") { if (messageType === "error") {
icon = <Icon iconName="StatusErrorFull" className="panelErrorIcon" aria-label="error" />; icon = <Icon iconName="StatusErrorFull" className="panelErrorIcon" aria-label="error" />;
} else if (messageType === "warning") { } else if (messageType === "warning") {
icon = <Icon iconName="WarningSolid" className="panelWarningIcon" aria-label="warning" />; icon = <Icon iconName="WarningSolid" className="panelWarningIcon" aria-label="warning" />;
} else if (messageType === "info") { } else if (messageType === "info") {
icon = <Icon iconName="InfoSolid" className="panelLargeInfoIcon" aria-label="Infomation" />; icon = (
<Icon iconName="InfoSolid" className="panelLargeInfoIcon" aria-label={t(Keys.panes.panelInfo.information)} />
);
} }
return ( return (
@@ -43,7 +48,7 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
</Text> </Text>
{showErrorDetails && ( {showErrorDetails && (
<a className="paneErrorLink" role="button" onClick={expandConsole} tabIndex={0} onKeyPress={expandConsole}> <a className="paneErrorLink" role="button" onClick={expandConsole} tabIndex={0} onKeyPress={expandConsole}>
More details {t(Keys.panes.panelInfo.moreDetails)}
</a> </a>
)} )}
</span> </span>

View File

@@ -5,6 +5,7 @@ import { getErrorMessage, getErrorStack, handleError } from "../../../Common/Err
import { useNotebookSnapshotStore } from "../../../hooks/useNotebookSnapshotStore"; import { useNotebookSnapshotStore } from "../../../hooks/useNotebookSnapshotStore";
import { useSidePanel } from "../../../hooks/useSidePanel"; import { useSidePanel } from "../../../hooks/useSidePanel";
import { JunoClient } from "../../../Juno/JunoClient"; import { JunoClient } from "../../../Juno/JunoClient";
import { Keys, t } from "Localization";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor"; import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
@@ -91,7 +92,7 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
let startKey: number; let startKey: number;
if (!notebookName || !notebookDescription || !author || !imageSrc) { if (!notebookName || !notebookDescription || !author || !imageSrc) {
setFormError(`Failed to publish ${notebookName} to gallery`); setFormError(t(Keys.panes.publishNotebook.publishFailedError, { notebookName }));
setFormErrorDetail("Name, description, author and cover image are required"); setFormErrorDetail("Name, description, author and cover image are required");
createFormError(formError, formErrorDetail, "PublishNotebookPaneAdapter/submit"); createFormError(formError, formErrorDetail, "PublishNotebookPaneAdapter/submit");
setIsExecuting(false); setIsExecuting(false);
@@ -143,7 +144,11 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
); );
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
setFormError(`Failed to publish ${FileSystemUtil.stripExtension(notebookName, "ipynb")} to gallery`); setFormError(
t(Keys.panes.publishNotebook.publishFailedError, {
notebookName: FileSystemUtil.stripExtension(notebookName, "ipynb"),
}),
);
setFormErrorDetail(`${errorMessage}`); setFormErrorDetail(`${errorMessage}`);
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", formError); handleError(errorMessage, "PublishNotebookPaneAdapter/submit", formError);
return; return;

View File

@@ -1,6 +1,7 @@
import { Dropdown, IDropdownProps, ITextFieldProps, Stack, Text, TextField } from "@fluentui/react"; import { Dropdown, IDropdownProps, ITextFieldProps, Stack, Text, TextField } from "@fluentui/react";
import { ImmutableNotebook } from "@nteract/commutable"; import { ImmutableNotebook } from "@nteract/commutable";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import { Keys, t } from "Localization";
import { GalleryCardComponent } from "../../Controls/NotebookGallery/Cards/GalleryCardComponent"; import { GalleryCardComponent } from "../../Controls/NotebookGallery/Cards/GalleryCardComponent";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil"; import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import { SnapshotRequest } from "../../Notebook/NotebookComponent/types"; import { SnapshotRequest } from "../../Notebook/NotebookComponent/types";
@@ -57,13 +58,11 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
const maxImageSizeInMib = 1.5; const maxImageSizeInMib = 1.5;
const descriptionPara1 = const descriptionPara1 = t(Keys.panes.publishNotebook.publishDescription);
"When published, this notebook will appear in the Azure Cosmos DB notebooks public gallery. Make sure you have removed any sensitive data or output before publishing.";
const descriptionPara2 = `Would you like to publish and share "${FileSystemUtil.stripExtension( const descriptionPara2 = t(Keys.panes.publishNotebook.publishPrompt, {
notebookName, name: FileSystemUtil.stripExtension(notebookName, "ipynb"),
"ipynb", });
)}" to the gallery?`;
const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url]; const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url];
if (onTakeSnapshot) { if (onTakeSnapshot) {
@@ -74,9 +73,9 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
} }
const thumbnailSelectorProps: IDropdownProps = { const thumbnailSelectorProps: IDropdownProps = {
label: "Cover image", label: t(Keys.panes.publishNotebook.coverImage),
selectedKey: type, selectedKey: type,
ariaLabel: "Cover image", ariaLabel: t(Keys.panes.publishNotebook.coverImage),
options: options.map((value: string) => ({ text: value, key: value })), options: options.map((value: string) => ({ text: value, key: value })),
onChange: async (event, options) => { onChange: async (event, options) => {
setImageSrc(""); setImageSrc("");
@@ -99,7 +98,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
notebookContentRef, notebookContentRef,
}); });
} else { } else {
firstOutputErrorHandler(new Error("Output does not exist for any of the cells.")); firstOutputErrorHandler(new Error(t(Keys.panes.publishNotebook.outputDoesNotExist)));
} }
} }
setType(options.text); setType(options.text);
@@ -107,8 +106,8 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
}; };
const thumbnailUrlProps: ITextFieldProps = { const thumbnailUrlProps: ITextFieldProps = {
label: "Cover image url", label: t(Keys.panes.publishNotebook.coverImageUrl),
ariaLabel: "Cover image url", ariaLabel: t(Keys.panes.publishNotebook.coverImageUrl),
required: true, required: true,
onChange: (event, newValue) => { onChange: (event, newValue) => {
setImageSrc(newValue); setImageSrc(newValue);
@@ -116,7 +115,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
}; };
const firstOutputErrorHandler = (error: Error) => { const firstOutputErrorHandler = (error: Error) => {
const formError = "Failed to capture first output"; const formError = t(Keys.panes.publishNotebook.failedToCaptureOutput);
const formErrorDetail = `${error}`; const formErrorDetail = `${error}`;
const area = "PublishNotebookPaneComponent/UseFirstOutput"; const area = "PublishNotebookPaneComponent/UseFirstOutput";
onError(formError, formErrorDetail, area); onError(formError, formErrorDetail, area);
@@ -130,7 +129,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
}; };
reader.onerror = (error) => { reader.onerror = (error) => {
const formError = `Failed to convert ${file.name} to base64 format`; const formError = t(Keys.panes.publishNotebook.failedToConvertError, { fileName: file.name });
const formErrorDetail = `${error}`; const formErrorDetail = `${error}`;
const area = "PublishNotebookPaneComponent/selectImageFile"; const area = "PublishNotebookPaneComponent/selectImageFile";
onError(formError, formErrorDetail, area); onError(formError, formErrorDetail, area);
@@ -151,7 +150,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
const file = event.target.files[0]; const file = event.target.files[0];
if (file.size / 1024 ** 2 > maxImageSizeInMib) { if (file.size / 1024 ** 2 > maxImageSizeInMib) {
event.target.value = ""; event.target.value = "";
const formError = `Failed to upload ${file.name}`; const formError = t(Keys.panes.publishNotebook.failedToUploadError, { fileName: file.name });
const formErrorDetail = `Image is larger than ${maxImageSizeInMib} MiB. Please Choose a different image.`; const formErrorDetail = `Image is larger than ${maxImageSizeInMib} MiB. Please Choose a different image.`;
const area = "PublishNotebookPaneComponent/selectImageFile"; const area = "PublishNotebookPaneComponent/selectImageFile";
@@ -185,8 +184,8 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
<Stack.Item> <Stack.Item>
<TextField <TextField
label="Name" label={t(Keys.panes.publishNotebook.name)}
ariaLabel="Name" ariaLabel={t(Keys.panes.publishNotebook.name)}
defaultValue={FileSystemUtil.stripExtension(notebookName, "ipynb")} defaultValue={FileSystemUtil.stripExtension(notebookName, "ipynb")}
required required
onChange={(event, newValue) => { onChange={(event, newValue) => {
@@ -198,8 +197,8 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
<Stack.Item> <Stack.Item>
<TextField <TextField
label="Description" label={t(Keys.panes.publishNotebook.description)}
ariaLabel="Description" ariaLabel={t(Keys.panes.publishNotebook.description)}
multiline multiline
rows={3} rows={3}
required required
@@ -211,9 +210,9 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
<Stack.Item> <Stack.Item>
<TextField <TextField
label="Tags" label={t(Keys.panes.publishNotebook.tags)}
ariaLabel="Tags" ariaLabel={t(Keys.panes.publishNotebook.tags)}
placeholder="Optional tag 1, Optional tag 2" placeholder={t(Keys.panes.publishNotebook.tagsPlaceholder)}
onChange={(event, newValue) => { onChange={(event, newValue) => {
setNotebookTags(newValue); setNotebookTags(newValue);
}} }}
@@ -227,7 +226,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
<Stack.Item>{renderThumbnailSelectors(type)}</Stack.Item> <Stack.Item>{renderThumbnailSelectors(type)}</Stack.Item>
<Stack.Item> <Stack.Item>
<Text>Preview</Text> <Text>{t(Keys.panes.publishNotebook.preview)}</Text>
</Stack.Item> </Stack.Item>
<Stack.Item> <Stack.Item>
<GalleryCardComponent <GalleryCardComponent

View File

@@ -4,6 +4,7 @@ import React, { FunctionComponent, useState } from "react";
import { Areas, SavedQueries } from "../../../Common/Constants"; import { Areas, SavedQueries } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { Query } from "../../../Contracts/DataModels"; import { Query } from "../../../Contracts/DataModels";
import { Keys, t } from "Localization";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor"; import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
@@ -28,27 +29,27 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [queryName, setQueryName] = useState<string>(""); const [queryName, setQueryName] = useState<string>("");
const setupSaveQueriesText = `For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “${SavedQueries.DatabaseName}”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.`; const setupSaveQueriesText = t(Keys.panes.saveQuery.setupCostMessage, { databaseName: SavedQueries.DatabaseName });
const title = "Save Query"; const title = t(Keys.panes.saveQuery.panelTitle);
const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled); const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled);
const submit = async (): Promise<void> => { const submit = async (): Promise<void> => {
setFormError(""); setFormError("");
if (!isSaveQueryEnabled()) { if (!isSaveQueryEnabled()) {
setFormError("Cannot save query"); setFormError("Cannot save query");
logConsoleError("Failed to save query: account not setup to save queries"); logConsoleError(t(Keys.panes.saveQuery.accountNotSetupError));
} }
const queryTab = useTabs.getState().activeTab as NewQueryTab; const queryTab = useTabs.getState().activeTab as NewQueryTab;
const query: string = queryToSave || queryTab?.iTabAccessor.onSaveClickEvent(); const query: string = queryToSave || queryTab?.iTabAccessor.onSaveClickEvent();
if (!queryName || queryName.length === 0) { if (!queryName || queryName.length === 0) {
setFormError("No query name specified"); setFormError(t(Keys.panes.saveQuery.noQueryNameError));
logConsoleError("Could not save query -- No query name specified. Please specify a query name."); logConsoleError(t(Keys.panes.saveQuery.noQueryNameError));
return; return;
} else if (!query || query.length === 0) { } else if (!query || query.length === 0) {
setFormError("Invalid query content specified"); setFormError(t(Keys.panes.saveQuery.invalidQueryContentError));
logConsoleError("Could not save query -- Invalid query content specified. Please enter query content."); logConsoleError(t(Keys.panes.saveQuery.invalidQueryContentError));
return; return;
} }
@@ -80,8 +81,8 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
} catch (error) { } catch (error) {
setLoadingFalse(); setLoadingFalse();
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
setFormError("Failed to save query"); setFormError(t(Keys.panes.saveQuery.failedToSaveQueryError, { queryName }));
logConsoleError(`Failed to save query: ${errorMessage}`); logConsoleError(t(Keys.panes.saveQuery.failedToSaveQueryError, { queryName }) + ": " + errorMessage);
traceFailure( traceFailure(
Action.SaveQuery, Action.SaveQuery,
{ {
@@ -126,8 +127,8 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
}, },
startKey, startKey,
); );
setFormError("Failed to setup a container for saved queries"); setFormError(t(Keys.panes.saveQuery.failedToSetupContainerError));
logConsoleError(`Failed to setup a container for saved queries: ${errorMessage}`); logConsoleError(t(Keys.panes.saveQuery.failedToSetupContainerError) + ": " + errorMessage);
} finally { } finally {
setLoadingFalse(); setLoadingFalse();
} }
@@ -136,7 +137,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
const props: RightPaneFormProps = { const props: RightPaneFormProps = {
formError: formError, formError: formError,
isExecuting: isLoading, isExecuting: isLoading,
submitButtonText: isSaveQueryEnabled() ? "Save" : "Complete setup", submitButtonText: isSaveQueryEnabled() ? t(Keys.common.save) : t(Keys.panes.saveQuery.completeSetup),
onSubmit: () => { onSubmit: () => {
isSaveQueryEnabled() ? submit() : setupQueries(); isSaveQueryEnabled() ? submit() : setupQueries();
}, },
@@ -160,7 +161,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
) : ( ) : (
<TextField <TextField
id="saveQueryInput" id="saveQueryInput"
label="Name" label={t(Keys.panes.saveQuery.name)}
autoFocus autoFocus
styles={{ fieldGroup: { width: 300 } }} styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => { onChange={(event, newInput?: string) => {

View File

@@ -24,6 +24,7 @@ import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { Platform, configContext } from "ConfigContext"; import { Platform, configContext } from "ConfigContext";
import { useDialog } from "Explorer/Controls/Dialog"; import { useDialog } from "Explorer/Controls/Dialog";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { Keys, t } from "Localization";
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil"; import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
import { import {
AppStateComponentNames, AppStateComponentNames,
@@ -235,7 +236,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
const regionOptions: IDropdownOption[] = []; const regionOptions: IDropdownOption[] = [];
regionOptions.push({ regionOptions.push({
key: userContext?.databaseAccount?.properties?.documentEndpoint, key: userContext?.databaseAccount?.properties?.documentEndpoint,
text: `Global (Default)`, text: t(Keys.panes.settings.globalDefault),
data: { data: {
isGlobal: true, isGlobal: true,
writeEnabled: true, writeEnabled: true,
@@ -246,7 +247,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
uniqueAccountRegions.add(loc.locationName); uniqueAccountRegions.add(loc.locationName);
regionOptions.push({ regionOptions.push({
key: loc.documentEndpoint, key: loc.documentEndpoint,
text: `${loc.locationName} (Read/Write)`, text: `${loc.locationName} ${t(Keys.panes.settings.readWrite)}`,
data: { data: {
isGlobal: false, isGlobal: false,
writeEnabled: true, writeEnabled: true,
@@ -259,7 +260,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
uniqueAccountRegions.add(loc.locationName); uniqueAccountRegions.add(loc.locationName);
regionOptions.push({ regionOptions.push({
key: loc.documentEndpoint, key: loc.documentEndpoint,
text: `${loc.locationName} (Read)`, text: `${loc.locationName} ${t(Keys.panes.settings.read)}`,
data: { data: {
isGlobal: false, isGlobal: false,
writeEnabled: false, writeEnabled: false,
@@ -317,13 +318,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
authError instanceof msalAuthError && authError instanceof msalAuthError &&
authError.errorCode === msalBrowserAuthErrorMessage.popUpWindowError.code authError.errorCode === msalBrowserAuthErrorMessage.popUpWindowError.code
) { ) {
logConsoleError( logConsoleError(t(Keys.panes.settings.popupsDisabledError));
`We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and click on "Login for Entra ID" button`,
);
} else { } else {
logConsoleError( logConsoleError(t(Keys.panes.settings.failedToAcquireTokenError));
`"Failed to acquire authorization token automatically. Please click on "Login for Entra ID" button to enable Entra ID RBAC operations`,
);
} }
} }
} else { } else {
@@ -485,33 +482,33 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
const genericPaneProps: RightPaneFormProps = { const genericPaneProps: RightPaneFormProps = {
formError: "", formError: "",
isExecuting, isExecuting,
submitButtonText: "Apply", submitButtonText: t(Keys.common.apply),
onSubmit: () => handlerOnSubmit(), onSubmit: () => handlerOnSubmit(),
}; };
const pageOptionList: IChoiceGroupOption[] = [ const pageOptionList: IChoiceGroupOption[] = [
{ key: Constants.Queries.CustomPageOption, text: "Custom" }, { key: Constants.Queries.CustomPageOption, text: t(Keys.panes.settings.custom) },
{ key: Constants.Queries.UnlimitedPageOption, text: "Unlimited" }, { key: Constants.Queries.UnlimitedPageOption, text: t(Keys.panes.settings.unlimited) },
]; ];
const graphAutoOptionList: IChoiceGroupOption[] = [ const graphAutoOptionList: IChoiceGroupOption[] = [
{ key: "false", text: "Graph" }, { key: "false", text: t(Keys.panes.settings.graph) },
{ key: "true", text: "JSON" }, { key: "true", text: t(Keys.panes.settings.json) },
]; ];
const priorityLevelOptionList: IChoiceGroupOption[] = [ const priorityLevelOptionList: IChoiceGroupOption[] = [
{ key: Constants.PriorityLevel.Low, text: "Low" }, { key: Constants.PriorityLevel.Low, text: t(Keys.panes.settings.low) },
{ key: Constants.PriorityLevel.High, text: "High" }, { key: Constants.PriorityLevel.High, text: t(Keys.panes.settings.high) },
]; ];
const dataPlaneRBACOptionsList: IChoiceGroupOption[] = [ const dataPlaneRBACOptionsList: IChoiceGroupOption[] = [
{ key: Constants.RBACOptions.setAutomaticRBACOption, text: "Automatic" }, { key: Constants.RBACOptions.setAutomaticRBACOption, text: t(Keys.panes.settings.automatic) },
{ key: Constants.RBACOptions.setTrueRBACOption, text: "True" }, { key: Constants.RBACOptions.setTrueRBACOption, text: t(Keys.panes.settings["true"]) },
{ key: Constants.RBACOptions.setFalseRBACOption, text: "False" }, { key: Constants.RBACOptions.setFalseRBACOption, text: t(Keys.panes.settings["false"]) },
]; ];
const defaultQueryResultsViewOptionList: IChoiceGroupOption[] = [ const defaultQueryResultsViewOptionList: IChoiceGroupOption[] = [
{ key: SplitterDirection.Vertical, text: "Vertical" }, { key: SplitterDirection.Vertical, text: t(Keys.tabs.query.vertical) },
{ key: SplitterDirection.Horizontal, text: "Horizontal" }, { key: SplitterDirection.Horizontal, text: t(Keys.tabs.query.horizontal) },
]; ];
const mongoGuidRepresentationDropdownOptions: IDropdownOption[] = [ const mongoGuidRepresentationDropdownOptions: IDropdownOption[] = [
@@ -724,13 +721,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowQueryPageOptions && ( {shouldShowQueryPageOptions && (
<AccordionItem value="1"> <AccordionItem value="1">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Page Options</div> <div className={styles.header}>{t(Keys.panes.settings.pageOptions)}</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as {t(Keys.panes.settings.pageOptionsDescription)}
many query results per page.
</div> </div>
<ChoiceGroup <ChoiceGroup
ariaLabelledBy="pageOptions" ariaLabelledBy="pageOptions"
@@ -744,14 +740,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{isCustomPageOptionSelected() && ( {isCustomPageOptionSelected() && (
<div className="tabcontent"> <div className="tabcontent">
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
Query results per page{" "} {t(Keys.panes.settings.queryResultsPerPage)}{" "}
<InfoTooltip className={styles.headerIcon}> <InfoTooltip className={styles.headerIcon}>
Enter the number of query results that should be shown per page. {t(Keys.panes.settings.queryResultsPerPageTooltip)}
</InfoTooltip> </InfoTooltip>
</div> </div>
<SpinButton <SpinButton
ariaLabel="Custom query items per page" ariaLabel={t(Keys.panes.settings.customQueryItemsPerPage)}
value={"" + customItemPerPage} value={"" + customItemPerPage}
onIncrement={(newValue) => { onIncrement={(newValue) => {
setCustomItemPerPage(parseInt(newValue) + 1 || customItemPerPage); setCustomItemPerPage(parseInt(newValue) + 1 || customItemPerPage);
@@ -761,8 +757,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
min={1} min={1}
step={1} step={1}
className="textfontclr" className="textfontclr"
incrementButtonAriaLabel="Increase value by 1" incrementButtonAriaLabel={t(Keys.common.increaseValueBy1)}
decrementButtonAriaLabel="Decrease value by 1" decrementButtonAriaLabel={t(Keys.common.decreaseValueBy1)}
styles={spinButtonStyles} styles={spinButtonStyles}
/> />
</div> </div>
@@ -774,20 +770,19 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{showEnableEntraIdRbac && ( {showEnableEntraIdRbac && (
<AccordionItem value="2"> <AccordionItem value="2">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Enable Entra ID RBAC</div> <div className={styles.header}>{t(Keys.panes.settings.entraIdRbac)}</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra {t(Keys.panes.settings.entraIdRbacDescription)}
ID RBAC.
<a <a
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer" href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{" "} {" "}
Learn more{" "} {t(Keys.common.learnMore)}{" "}
</a> </a>
</div> </div>
<ChoiceGroup <ChoiceGroup
@@ -804,17 +799,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && !isFabric() && ( {userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && !isFabric() && (
<AccordionItem value="3"> <AccordionItem value="3">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Region Selection</div> <div className={styles.header}>{t(Keys.panes.settings.regionSelection)}</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
Changes region the Cosmos Client uses to access account. {t(Keys.panes.settings.regionSelectionDescription)}
</div> </div>
<div> <div>
<span className={styles.subHeader}>Select Region</span> <span className={styles.subHeader}>{t(Keys.panes.settings.selectRegion)}</span>
<InfoTooltip className={styles.headerIcon}> <InfoTooltip className={styles.headerIcon}>
Changes the account endpoint used to perform client operations. {t(Keys.panes.settings.selectRegionTooltip)}
</InfoTooltip> </InfoTooltip>
</div> </div>
<Dropdown <Dropdown
@@ -865,17 +860,16 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
<> <>
<AccordionItem value="4"> <AccordionItem value="4">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Query Timeout</div> <div className={styles.header}>{t(Keys.panes.settings.queryTimeout)}</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
When a query reaches a specified time limit, a popup with an option to cancel the query will {t(Keys.panes.settings.queryTimeoutDescription)}
show unless automatic cancellation has been enabled.
</div> </div>
<Toggle <Toggle
styles={toggleStyles} styles={toggleStyles}
label="Enable query timeout" label={t(Keys.panes.settings.enableQueryTimeout)}
onChange={handleOnQueryTimeoutToggleChange} onChange={handleOnQueryTimeoutToggleChange}
defaultChecked={queryTimeoutEnabled} defaultChecked={queryTimeoutEnabled}
/> />
@@ -883,18 +877,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{queryTimeoutEnabled && ( {queryTimeoutEnabled && (
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<SpinButton <SpinButton
label="Query timeout (ms)" label={t(Keys.panes.settings.queryTimeoutMs)}
labelPosition={Position.top} labelPosition={Position.top}
defaultValue={(queryTimeout || 5000).toString()} defaultValue={(queryTimeout || 5000).toString()}
min={100} min={100}
step={1000} step={1000}
onChange={handleOnQueryTimeoutSpinButtonChange} onChange={handleOnQueryTimeoutSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1000" incrementButtonAriaLabel={t(Keys.panes.settings.increaseValueBy1000)}
decrementButtonAriaLabel="Decrease value by 1000" decrementButtonAriaLabel={t(Keys.panes.settings.decreaseValueBy1000)}
styles={spinButtonStyles} styles={spinButtonStyles}
/> />
<Toggle <Toggle
label="Automatically cancel query after timeout" label={t(Keys.panes.settings.automaticallyCancelQuery)}
styles={toggleStyles} styles={toggleStyles}
onChange={handleOnAutomaticallyCancelQueryToggleChange} onChange={handleOnAutomaticallyCancelQueryToggleChange}
defaultChecked={automaticallyCancelQueryAfterTimeout} defaultChecked={automaticallyCancelQueryAfterTimeout}
@@ -905,16 +899,16 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem> </AccordionItem>
<AccordionItem value="5"> <AccordionItem value="5">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>RU Limit</div> <div className={styles.header}>{t(Keys.panes.settings.ruLimit)}</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
If a query exceeds a configured RU limit, the query will be aborted. {t(Keys.panes.settings.ruLimitDescription)}
</div> </div>
<Toggle <Toggle
styles={toggleStyles} styles={toggleStyles}
label="Enable RU limit" label={t(Keys.panes.settings.enableRuLimit)}
onChange={handleOnRUThresholdToggleChange} onChange={handleOnRUThresholdToggleChange}
defaultChecked={ruThresholdEnabled} defaultChecked={ruThresholdEnabled}
/> />
@@ -922,14 +916,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{ruThresholdEnabled && ( {ruThresholdEnabled && (
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<SpinButton <SpinButton
label="RU Limit (RU)" label={t(Keys.panes.settings.ruLimitLabel)}
labelPosition={Position.top} labelPosition={Position.top}
defaultValue={(ruThreshold || DefaultRUThreshold).toString()} defaultValue={(ruThreshold || DefaultRUThreshold).toString()}
min={1} min={1}
step={1000} step={1000}
onChange={handleOnRUThresholdSpinButtonChange} onChange={handleOnRUThresholdSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1000" incrementButtonAriaLabel={t(Keys.panes.settings.increaseValueBy1000)}
decrementButtonAriaLabel="Decrease value by 1000" decrementButtonAriaLabel={t(Keys.panes.settings.decreaseValueBy1000)}
styles={spinButtonStyles} styles={spinButtonStyles}
/> />
</div> </div>
@@ -939,12 +933,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
<AccordionItem value="6"> <AccordionItem value="6">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Default Query Results View</div> <div className={styles.header}>{t(Keys.panes.settings.defaultQueryResults)}</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
Select the default view to use when displaying query results. {t(Keys.panes.settings.defaultQueryResultsDescription)}
</div> </div>
<ChoiceGroup <ChoiceGroup
ariaLabelledBy="defaultQueryResultsView" ariaLabelledBy="defaultQueryResultsView"
@@ -962,17 +956,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{showRetrySettings && ( {showRetrySettings && (
<AccordionItem value="7"> <AccordionItem value="7">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Retry Settings</div> <div className={styles.header}>{t(Keys.panes.settings.retrySettings)}</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
Retry policy associated with throttled requests during CosmosDB queries. {t(Keys.panes.settings.retrySettingsDescription)}
</div> </div>
<div> <div>
<span className={styles.subHeader}>Max retry attempts</span> <span className={styles.subHeader}>{t(Keys.panes.settings.maxRetryAttempts)}</span>
<InfoTooltip className={styles.headerIcon}> <InfoTooltip className={styles.headerIcon}>
Max number of retries to be performed for a request. Default value 9. {t(Keys.panes.settings.maxRetryAttemptsTooltip)}
</InfoTooltip> </InfoTooltip>
</div> </div>
<SpinButton <SpinButton
@@ -981,18 +975,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
step={1} step={1}
value={"" + retryAttempts} value={"" + retryAttempts}
onChange={handleOnQueryRetryAttemptsSpinButtonChange} onChange={handleOnQueryRetryAttemptsSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1" incrementButtonAriaLabel={t(Keys.common.increaseValueBy1)}
decrementButtonAriaLabel="Decrease value by 1" decrementButtonAriaLabel={t(Keys.common.decreaseValueBy1)}
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)} onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)} onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)} onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
styles={spinButtonStyles} styles={spinButtonStyles}
/> />
<div> <div>
<span className={styles.subHeader}>Fixed retry interval (ms)</span> <span className={styles.subHeader}>{t(Keys.panes.settings.fixedRetryInterval)}</span>
<InfoTooltip className={styles.headerIcon}> <InfoTooltip className={styles.headerIcon}>
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned {t(Keys.panes.settings.fixedRetryIntervalTooltip)}
as part of the response. Default value is 0 milliseconds.
</InfoTooltip> </InfoTooltip>
</div> </div>
<SpinButton <SpinButton
@@ -1001,18 +994,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
step={1000} step={1000}
value={"" + retryInterval} value={"" + retryInterval}
onChange={handleOnRetryIntervalSpinButtonChange} onChange={handleOnRetryIntervalSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1000" incrementButtonAriaLabel={t(Keys.panes.settings.increaseValueBy1000)}
decrementButtonAriaLabel="Decrease value by 1000" decrementButtonAriaLabel={t(Keys.panes.settings.decreaseValueBy1000)}
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)} onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)} onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)} onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
styles={spinButtonStyles} styles={spinButtonStyles}
/> />
<div> <div>
<span className={styles.subHeader}>Max wait time (s)</span> <span className={styles.subHeader}>{t(Keys.panes.settings.maxWaitTime)}</span>
<InfoTooltip className={styles.headerIcon}> <InfoTooltip className={styles.headerIcon}>
Max wait time in seconds to wait for a request while the retries are happening. Default value 30 {t(Keys.panes.settings.maxWaitTimeTooltip)}
seconds.
</InfoTooltip> </InfoTooltip>
</div> </div>
<SpinButton <SpinButton
@@ -1021,8 +1013,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
step={1} step={1}
value={"" + MaxWaitTimeInSeconds} value={"" + MaxWaitTimeInSeconds}
onChange={handleOnMaxWaitTimeSpinButtonChange} onChange={handleOnMaxWaitTimeSpinButtonChange}
incrementButtonAriaLabel="Increase value by 1" incrementButtonAriaLabel={t(Keys.common.increaseValueBy1)}
decrementButtonAriaLabel="Decrease value by 1" decrementButtonAriaLabel={t(Keys.common.decreaseValueBy1)}
onIncrement={(newValue) => onIncrement={(newValue) =>
setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds) setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)
} }
@@ -1039,24 +1031,26 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{!isEmulator && ( {!isEmulator && (
<AccordionItem value="8"> <AccordionItem value="8">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Enable container pagination</div> <div className={styles.header}>{t(Keys.panes.settings.enableContainerPagination)}</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order. {t(Keys.panes.settings.enableContainerPaginationDescription)}
</div> </div>
<Checkbox <Checkbox
styles={{ styles={{
label: { padding: 0 }, label: { padding: 0 },
}} }}
className="padding" className="padding"
ariaLabel="Enable container pagination" ariaLabel={t(Keys.panes.settings.enableContainerPagination)}
checked={containerPaginationEnabled} checked={containerPaginationEnabled}
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)} onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
label="Enable container pagination" label={t(Keys.panes.settings.enableContainerPagination)}
onRenderLabel={() => ( onRenderLabel={() => (
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable container pagination</span> <span style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.settings.enableContainerPagination)}
</span>
)} )}
/> />
</div> </div>
@@ -1066,24 +1060,25 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowCrossPartitionOption && ( {shouldShowCrossPartitionOption && (
<AccordionItem value="9"> <AccordionItem value="9">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Enable cross-partition query</div> <div className={styles.header}>{t(Keys.panes.settings.enableCrossPartitionQuery)}</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
Send more than one request while executing a query. More than one request is necessary if the {t(Keys.panes.settings.enableCrossPartitionQueryDescription)}
query is not scoped to single partition key value.
</div> </div>
<Checkbox <Checkbox
styles={{ styles={{
label: { padding: 0 }, label: { padding: 0 },
}} }}
className="padding" className="padding"
ariaLabel="Enable cross partition query" ariaLabel={t(Keys.panes.settings.enableCrossPartitionQuery)}
checked={crossPartitionQueryEnabled} checked={crossPartitionQueryEnabled}
onChange={() => setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)} onChange={() => setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)}
onRenderLabel={() => ( onRenderLabel={() => (
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable cross-partition query</span> <span style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.settings.enableCrossPartitionQuery)}
</span>
)} )}
/> />
</div> </div>
@@ -1093,19 +1088,19 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowEnhancedQueryControl && ( {shouldShowEnhancedQueryControl && (
<AccordionItem value="10"> <AccordionItem value="10">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Enhanced query control</div> <div className={styles.header}>{t(Keys.panes.settings.enhancedQueryControl)}</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
Query up to the max degree of parallelism. {t(Keys.panes.settings.maxDegreeOfParallelismQuery)}
<a <a
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/performance-tips-query-sdk?tabs=v3&pivots=programming-language-nodejs#enhanced-query-control" href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/performance-tips-query-sdk?tabs=v3&pivots=programming-language-nodejs#enhanced-query-control"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{" "} {" "}
Learn more{" "} {t(Keys.common.learnMore)}{" "}
</a> </a>
</div> </div>
<Checkbox <Checkbox
@@ -1113,11 +1108,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
label: { padding: 0 }, label: { padding: 0 },
}} }}
className="padding" className="padding"
ariaLabel="EnableQueryControl" ariaLabel={t(Keys.panes.settings.enableQueryControl)}
checked={queryControlEnabled} checked={queryControlEnabled}
onChange={() => setQueryControlEnabled(!queryControlEnabled)} onChange={() => setQueryControlEnabled(!queryControlEnabled)}
onRenderLabel={() => ( onRenderLabel={() => (
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable query control</span> <span style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.settings.enableQueryControl)}
</span>
)} )}
/> />
</div> </div>
@@ -1127,14 +1124,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowParallelismOption && ( {shouldShowParallelismOption && (
<AccordionItem value="10"> <AccordionItem value="10">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Max degree of parallelism</div> <div className={styles.header}>{t(Keys.panes.settings.maxDegreeOfParallelism)}</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
Gets or sets the number of concurrent operations run client side during parallel query execution. {t(Keys.panes.settings.maxDegreeOfParallelismDescription)}
A positive property value limits the number of concurrent operations to the set value. If it is
set to less than 0, the system automatically decides the number of concurrent operations to run.
</div> </div>
<SpinButton <SpinButton
min={-1} min={-1}
@@ -1150,8 +1145,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism) setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism)
} }
onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)} onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)}
ariaLabel="Max degree of parallelism" ariaLabel={t(Keys.panes.settings.maxDegreeOfParallelism)}
label="Max degree of parallelism" label={t(Keys.panes.settings.maxDegreeOfParallelism)}
styles={spinButtonStyles} styles={spinButtonStyles}
/> />
</div> </div>
@@ -1161,14 +1156,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowPriorityLevelOption && ( {shouldShowPriorityLevelOption && (
<AccordionItem value="11"> <AccordionItem value="11">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Priority Level</div> <div className={styles.header}>{t(Keys.panes.settings.priorityLevel)}</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
Sets the priority level for data-plane requests from Data Explorer when using Priority-Based {t(Keys.panes.settings.priorityLevelDescription)}
Execution. If &quot;None&quot; is selected, Data Explorer will not specify priority level, and the
server-side default priority level will be used.
</div> </div>
<ChoiceGroup <ChoiceGroup
ariaLabelledBy="priorityLevel" ariaLabelledBy="priorityLevel"
@@ -1184,19 +1177,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowGraphAutoVizOption && ( {shouldShowGraphAutoVizOption && (
<AccordionItem value="12"> <AccordionItem value="12">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Display Gremlin query results as:&nbsp;</div> <div className={styles.header}>{t(Keys.panes.settings.displayGremlinQueryResults)}&nbsp;</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
Select Graph to automatically visualize the query results as a Graph or JSON to display the {t(Keys.panes.settings.displayGremlinQueryResultsDescription)}
results as JSON.
</div> </div>
<ChoiceGroup <ChoiceGroup
selectedKey={graphAutoVizDisabled} selectedKey={graphAutoVizDisabled}
options={graphAutoOptionList} options={graphAutoOptionList}
onChange={handleOnGremlinChange} onChange={handleOnGremlinChange}
aria-label="Graph Auto-visualization" aria-label={t(Keys.panes.settings.graphAutoVisualization)}
/> />
</div> </div>
</AccordionPanel> </AccordionPanel>
@@ -1205,25 +1197,25 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowCopilotSampleDBOption && ( {shouldShowCopilotSampleDBOption && (
<AccordionItem value="13"> <AccordionItem value="13">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Enable sample database</div> <div className={styles.header}>{t(Keys.panes.settings.enableSampleDatabase)}</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
This is a sample database and collection with synthetic product data you can use to explore using {t(Keys.panes.settings.enableSampleDatabaseDescription)}
NoSQL queries. This will appear as another database in the Data Explorer UI, and is created by,
and maintained by Microsoft at no cost to you.
</div> </div>
<Checkbox <Checkbox
styles={{ styles={{
label: { padding: 0 }, label: { padding: 0 },
}} }}
className="padding" className="padding"
ariaLabel="Enable sample db for query exploration" ariaLabel={t(Keys.panes.settings.enableSampleDbAriaLabel)}
checked={copilotSampleDBEnabled} checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange} onChange={handleSampleDatabaseChange}
onRenderLabel={() => ( onRenderLabel={() => (
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable sample database</span> <span style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.settings.enableSampleDatabase)}
</span>
)} )}
/> />
</div> </div>
@@ -1233,13 +1225,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowMongoGuidRepresentationOption && ( {shouldShowMongoGuidRepresentationOption && (
<AccordionItem value="14"> <AccordionItem value="14">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Guid Representation</div> <div className={styles.header}>{t(Keys.panes.settings.guidRepresentation)}</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}> <div className={styles.settingsSectionDescription}>
GuidRepresentation in MongoDB refers to how Globally Unique Identifiers (GUIDs) are serialized and {t(Keys.panes.settings.guidRepresentationDescription)}
deserialized when stored in BSON documents. This will apply to all document operations.
</div> </div>
<Dropdown <Dropdown
aria-labelledby="mongoGuidRepresentation" aria-labelledby="mongoGuidRepresentation"
@@ -1253,7 +1244,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)} )}
<AccordionItem value="15"> <AccordionItem value="15">
<AccordionHeader> <AccordionHeader>
<div className={styles.header}>Advanced Settings</div> <div className={styles.header}>{t(Keys.panes.settings.advancedSettings)}</div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>
<div className={styles.settingsSectionContainer}> <div className={styles.settingsSectionContainer}>
@@ -1283,14 +1274,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
}, },
}} }}
className="padding" className="padding"
ariaLabel="Ignore partition key on document update" ariaLabel={t(Keys.panes.settings.ignorePartitionKey)}
checked={ignorePartitionKeyOnDocumentUpdate} checked={ignorePartitionKeyOnDocumentUpdate}
onChange={handleOnIgnorePartitionKeyOnDocumentUpdateChange} onChange={handleOnIgnorePartitionKeyOnDocumentUpdateChange}
label="Ignore partition key on document update" label={t(Keys.panes.settings.ignorePartitionKey)}
/> />
<InfoTooltip className={styles.headerIcon}> <InfoTooltip className={styles.headerIcon}>
If checked, the partition key value will not be used to locate the document during update {t(Keys.panes.settings.ignorePartitionKeyTooltip)}
operations. Only use this if document updates are failing due to an abnormal partition key.
</InfoTooltip> </InfoTooltip>
</Stack> </Stack>
</div> </div>
@@ -1320,9 +1310,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
}} }}
onClick={() => { onClick={() => {
useDialog.getState().showOkCancelModalDialog( useDialog.getState().showOkCancelModalDialog(
"Clear History", t(Keys.panes.settings.clearHistory),
undefined, undefined,
"Are you sure you want to proceed?", t(Keys.panes.settings.clearHistoryConfirm),
() => { () => {
deleteAllStates(); deleteAllStates();
updateUserContext({ updateUserContext({
@@ -1332,35 +1322,33 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
}); });
useClientWriteEnabled.setState({ clientWriteEnabled: true }); useClientWriteEnabled.setState({ clientWriteEnabled: true });
}, },
"Cancel", t(Keys.common.cancel),
undefined, undefined,
<> <>
<span> <span>{t(Keys.panes.settings.clearHistoryDescription)}</span>
This action will clear the all customizations for this account in this browser, including:
</span>
<ul className={styles.bulletList}> <ul className={styles.bulletList}>
<li>Reset your customized tab layout, including the splitter positions</li> <li>{t(Keys.panes.settings.clearHistoryTabLayout)}</li>
<li>Erase your table column preferences, including any custom columns</li> <li>{t(Keys.panes.settings.clearHistoryTableColumns)}</li>
<li>Clear your filter history</li> <li>{t(Keys.panes.settings.clearHistoryFilters)}</li>
<li>Reset region selection to global</li> <li>{t(Keys.panes.settings.clearHistoryRegion)}</li>
</ul> </ul>
</>, </>,
); );
}} }}
> >
Clear History {t(Keys.panes.settings.clearHistory)}
</DefaultButton> </DefaultButton>
</div> </div>
</div> </div>
<div className="settingsSection"> <div className="settingsSection">
<div className={`settingsSectionPart ${styles.settingsSectionContainer}`}> <div className={`settingsSectionPart ${styles.settingsSectionContainer}`}>
<div className="settingsSectionLabel">Explorer Version</div> <div className="settingsSectionLabel">{t(Keys.panes.settings.explorerVersion)}</div>
<div>{explorerVersion}</div> <div>{explorerVersion}</div>
</div> </div>
</div> </div>
<div className="settingsSection"> <div className="settingsSection">
<div className="settingsSectionPart"> <div className="settingsSectionPart">
<div className="settingsSectionLabel">Session ID</div> <div className="settingsSectionLabel">{t(Keys.panes.settings.sessionId)}</div>
<div>{sessionId}</div> <div>{sessionId}</div>
</div> </div>
</div> </div>

View File

@@ -660,7 +660,7 @@ exports[`Settings Pane should render Default properly 1`] = `
Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value. Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.
</div> </div>
<StyledCheckboxBase <StyledCheckboxBase
ariaLabel="Enable cross partition query" ariaLabel="Enable cross-partition query"
checked={false} checked={false}
className="padding" className="padding"
onChange={[Function]} onChange={[Function]}
@@ -705,7 +705,7 @@ exports[`Settings Pane should render Default properly 1`] = `
</a> </a>
</div> </div>
<StyledCheckboxBase <StyledCheckboxBase
ariaLabel="EnableQueryControl" ariaLabel="Enable query control"
checked={false} checked={false}
className="padding" className="padding"
onChange={[Function]} onChange={[Function]}
@@ -1190,7 +1190,8 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
<div <div
className="___j7dlp70_0000000 fq02s40 f19n0e5" className="___j7dlp70_0000000 fq02s40 f19n0e5"
> >
Display Gremlin query results as:  Display Gremlin query results as:
 
</div> </div>
</AccordionHeader> </AccordionHeader>
<AccordionPanel> <AccordionPanel>

View File

@@ -8,6 +8,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
explorer={ explorer={
Explorer { Explorer {
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],

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