mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-03-07 00:35:17 +00:00
Compare commits
43 Commits
users/aisa
...
loc_batch4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3385b62b60 | ||
|
|
79a00080bd | ||
|
|
8bc875ae92 | ||
|
|
47b8737ab1 | ||
|
|
a106b0ebac | ||
|
|
dafb257fa3 | ||
|
|
b256ac1e1f | ||
|
|
915f549df9 | ||
|
|
77132be3b3 | ||
|
|
c343bad630 | ||
|
|
9579d1270b | ||
|
|
9496cf83ee | ||
|
|
dbe26654f1 | ||
|
|
b478f2732c | ||
|
|
204444b878 | ||
|
|
2e5c355479 | ||
|
|
5832170b2b | ||
|
|
cc26e2800e | ||
|
|
39ac7cf3f2 | ||
|
|
ed3a79f880 | ||
|
|
68ba19d406 | ||
|
|
81ad13508b | ||
|
|
3f959e2235 | ||
|
|
67250f0f6b | ||
|
|
df6312038a | ||
|
|
e6c27f39be | ||
|
|
937989a47d | ||
|
|
f166ef9c66 | ||
|
|
b83f54e253 | ||
|
|
a255dd502b | ||
|
|
2998f14d52 | ||
|
|
05407b3e0f | ||
|
|
703218debf | ||
|
|
f83a2c4442 | ||
|
|
2ff01c6379 | ||
|
|
31385950dd | ||
|
|
6dce2632c8 | ||
|
|
80ad5f10d4 | ||
|
|
f02611c90e | ||
|
|
9646dfcf04 | ||
|
|
90f3c3a79e | ||
|
|
b922086cc0 | ||
|
|
375ec350dc |
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -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)
|
||||
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
|
||||
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)
|
||||
# echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
|
||||
# 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)
|
||||
# echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
|
||||
# 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)
|
||||
# echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
|
||||
# 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)
|
||||
# echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
|
||||
# echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_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)
|
||||
echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
|
||||
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)
|
||||
echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
|
||||
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)
|
||||
echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
|
||||
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)
|
||||
echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
|
||||
echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
- name: List test files for shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
||||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --list
|
||||
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
||||
|
||||
4
.github/workflows/cleanup.yml
vendored
4
.github/workflows/cleanup.yml
vendored
@@ -6,8 +6,8 @@ on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# Once every two hours
|
||||
- cron: "0 */2 * * *"
|
||||
# Once every day at 7 AM PST
|
||||
- cron: "0 13 * * *"
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,6 +17,7 @@ Contracts/*
|
||||
failure.png
|
||||
screenshots/*
|
||||
GettingStarted-ignore*.ipynb
|
||||
src/Localization/Keys.generated.ts
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
|
||||
14319
package-lock.json
generated
14319
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
69
package.json
69
package.json
@@ -4,7 +4,7 @@
|
||||
"description": "Cosmos Explorer",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/arm-cosmosdb": "16.4.0",
|
||||
"@azure/cosmos": "4.7.0",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "4.5.0",
|
||||
@@ -14,12 +14,12 @@
|
||||
"@fluentui/react": "8.119.0",
|
||||
"@fluentui/react-components": "9.54.2",
|
||||
"@jupyterlab/services": "6.0.2",
|
||||
"@jupyterlab/terminal": "3.0.3",
|
||||
"@jupyterlab/terminal": "3.6.8",
|
||||
"@microsoft/applicationinsights-web": "2.6.1",
|
||||
"@nteract/commutable": "7.5.1",
|
||||
"@nteract/connected-components": "6.8.2",
|
||||
"@nteract/core": "15.1.9",
|
||||
"@nteract/data-explorer": "8.0.3",
|
||||
"@nteract/data-explorer": "8.2.12",
|
||||
"@nteract/directory-listing": "2.0.6",
|
||||
"@nteract/dropdown-menu": "1.0.1",
|
||||
"@nteract/editor": "10.1.12",
|
||||
@@ -31,7 +31,7 @@
|
||||
"@nteract/monaco-editor": "3.2.2",
|
||||
"@nteract/octicons": "2.0.0",
|
||||
"@nteract/outputs": "3.0.9",
|
||||
"@nteract/presentational-components": "3.0.7",
|
||||
"@nteract/presentational-components": "3.4.12",
|
||||
"@nteract/stateful-components": "1.7.0",
|
||||
"@nteract/styles": "2.0.2",
|
||||
"@nteract/transform-geojson": "5.1.8",
|
||||
@@ -39,25 +39,26 @@
|
||||
"@nteract/transform-plotly": "6.1.6",
|
||||
"@nteract/transform-vdom": "4.0.11",
|
||||
"@nteract/transform-vega": "7.0.6",
|
||||
"@octokit/request": "8.4.1",
|
||||
"@octokit/rest": "17.9.2",
|
||||
"@phosphor/widgets": "1.9.3",
|
||||
"@testing-library/jest-dom": "6.4.6",
|
||||
"@types/lodash": "4.14.171",
|
||||
"@types/mkdirp": "1.0.1",
|
||||
"@types/node-fetch": "2.5.7",
|
||||
"@types/node-fetch": "2.6.13",
|
||||
"@xmldom/xmldom": "0.7.13",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/xterm": "5.5.0",
|
||||
"allotment": "1.20.2",
|
||||
"applicationinsights": "1.8.0",
|
||||
"bootstrap": "3.4.1",
|
||||
"canvas": "2.11.2",
|
||||
"canvas": "3.2.1",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"clipboard-copy": "4.0.1",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"crossroads": "0.12.2",
|
||||
"css-element-queries": "1.1.1",
|
||||
"d3": "7.8.5",
|
||||
"d3": "7.9.0",
|
||||
"datatables.net-colreorder-dt": "1.7.0",
|
||||
"datatables.net-dt": "1.13.8",
|
||||
"date-fns": "1.29.0",
|
||||
@@ -70,7 +71,8 @@
|
||||
"html2canvas": "1.0.0-rc.5",
|
||||
"i18next": "23.11.5",
|
||||
"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",
|
||||
"immer": "9.0.6",
|
||||
"immutable": "4.0.0-rc.12",
|
||||
@@ -79,12 +81,15 @@
|
||||
"jquery-typeahead": "2.11.1",
|
||||
"jquery-ui-dist": "1.13.2",
|
||||
"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",
|
||||
"monaco-editor": "0.44.0",
|
||||
"ms": "2.1.3",
|
||||
"nanoid": "3.3.8",
|
||||
"p-retry": "6.2.1",
|
||||
"patch-package": "8.0.0",
|
||||
"patch-package": "8.0.1",
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"post-robot": "10.0.42",
|
||||
"q": "1.5.1",
|
||||
@@ -103,7 +108,6 @@
|
||||
"react-youtube": "9.0.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rx-jupyter": "5.5.12",
|
||||
"sanitize-html": "2.3.3",
|
||||
"shell-quote": "1.7.3",
|
||||
"styled-components": "5.0.1",
|
||||
"swr": "0.4.0",
|
||||
@@ -111,16 +115,30 @@
|
||||
"tinykeys": "2.1.0",
|
||||
"underscore": "1.12.1",
|
||||
"utility-types": "3.10.0",
|
||||
"web-vitals": "4.2.4",
|
||||
"uuid": "9.0.0",
|
||||
"web-vitals": "4.2.4",
|
||||
"ws": "8.17.1",
|
||||
"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": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-react": "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",
|
||||
"@types/applicationinsights-js": "1.0.7",
|
||||
"@types/codemirror": "0.0.56",
|
||||
@@ -134,7 +152,7 @@
|
||||
"@types/hasher": "0.0.31",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/jquery": "3.5.29",
|
||||
"@types/node": "12.11.1",
|
||||
"@types/node": "18.19.0",
|
||||
"@types/post-robot": "10.0.1",
|
||||
"@types/q": "1.5.1",
|
||||
"@types/react": "17.0.44",
|
||||
@@ -145,7 +163,7 @@
|
||||
"@types/react-window": "1.8.8",
|
||||
"@types/sanitize-html": "1.27.2",
|
||||
"@types/sinon": "2.3.3",
|
||||
"@types/styled-components": "5.1.1",
|
||||
"@types/styled-components": "5.1.32",
|
||||
"@types/underscore": "1.7.36",
|
||||
"@types/youtube-player": "5.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.4",
|
||||
@@ -153,6 +171,7 @@
|
||||
"@webpack-cli/serve": "2.0.5",
|
||||
"babel-jest": "29.7.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"brace-expansion": "1.1.12",
|
||||
"buffer": "5.1.0",
|
||||
"case-sensitive-paths-webpack-plugin": "2.4.0",
|
||||
"create-file-webpack": "1.0.2",
|
||||
@@ -170,6 +189,7 @@
|
||||
"html-inline-css-webpack-plugin": "1.11.2",
|
||||
"html-loader": "5.0.0",
|
||||
"html-webpack-plugin": "5.5.3",
|
||||
"i18next-resources-for-ts": "2.0.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-canvas-mock": "2.5.2",
|
||||
"jest-circus": "29.7.0",
|
||||
@@ -177,7 +197,8 @@
|
||||
"jest-html-loader": "1.0.0",
|
||||
"jest-react-hooks-shallow": "1.5.1",
|
||||
"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-vars-loader": "1.1.0",
|
||||
"mini-css-extract-plugin": "2.1.0",
|
||||
@@ -195,14 +216,17 @@
|
||||
"typedoc": "0.26.2",
|
||||
"typescript": "4.9.5",
|
||||
"url-loader": "4.1.1",
|
||||
"wait-on": "4.0.2",
|
||||
"webpack": "5.88.2",
|
||||
"webpack-bundle-analyzer": "4.9.1",
|
||||
"values-to-keys": "1.1.0",
|
||||
"wait-on": "9.0.3",
|
||||
"webpack": "5.104.1",
|
||||
"webpack-bundle-analyzer": "5.2.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "4.15.2"
|
||||
"webpack-dev-server": "5.2.3",
|
||||
"ws": "8.17.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"postinstall": "patch-package && npm run generate:i18n-keys",
|
||||
"prestart": "npm run generate:i18n-keys",
|
||||
"start": "webpack serve --mode development",
|
||||
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
|
||||
"build:dataExplorer:ci": "npm run build:ci",
|
||||
@@ -229,6 +253,7 @@
|
||||
"strict:find": "node ./strict-null-checks/find.js",
|
||||
"strict:add": "node ./strict-null-checks/auto-add.js",
|
||||
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks",
|
||||
"generate:i18n-keys": "node utils/generateI18nKeys.mjs",
|
||||
"generateARMClients": "npx ts-node utils/armClientGenerator/generator.ts"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
@@ -26,15 +26,6 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
launchOptions: {
|
||||
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "firefox",
|
||||
use: {
|
||||
|
||||
587
preview/package-lock.json
generated
587
preview/package-lock.json
generated
@@ -8,9 +8,10 @@
|
||||
"name": "cosmos-explorer-preview",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.3",
|
||||
"express": "^4.21.2",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"body-parser": "^2.2.2",
|
||||
"express": "^5.2.1",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
"node": "^20.19.5",
|
||||
"node-fetch": "^2.6.1",
|
||||
"path-to-regexp": "^0.1.12"
|
||||
@@ -18,8 +19,7 @@
|
||||
},
|
||||
"node_modules/@types/http-proxy": {
|
||||
"version": "1.17.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz",
|
||||
"integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -29,50 +29,40 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
"mime-types": "^3.0.0",
|
||||
"negotiator": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.13.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^1.0.5",
|
||||
"debug": "^4.4.3",
|
||||
"http-errors": "^2.0.0",
|
||||
"iconv-lite": "^0.7.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.14.1",
|
||||
"raw-body": "^3.0.1",
|
||||
"type-is": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
"node": ">=18"
|
||||
},
|
||||
"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": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -83,8 +73,7 @@
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
@@ -95,8 +84,7 @@
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
@@ -109,13 +97,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
||||
"integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
@@ -127,20 +117,22 @@
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"license": "MIT"
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -160,18 +152,9 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -195,24 +178,21 @@
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
@@ -238,104 +218,77 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
"content-disposition": "^1.0.0",
|
||||
"content-type": "^1.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"cookie-signature": "^1.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"finalhandler": "^2.1.0",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"merge-descriptors": "^2.0.0",
|
||||
"mime-types": "^3.0.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"once": "^1.4.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"proxy-addr": "^2.0.7",
|
||||
"qs": "^6.14.0",
|
||||
"range-parser": "^1.2.1",
|
||||
"router": "^2.2.0",
|
||||
"send": "^1.1.0",
|
||||
"serve-static": "^2.2.0",
|
||||
"statuses": "^2.0.1",
|
||||
"type-is": "^2.0.1",
|
||||
"vary": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"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": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "2.0.1",
|
||||
"unpipe": "~1.0.0"
|
||||
"debug": "^4.4.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"parseurl": "^1.3.3",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"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": {
|
||||
"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": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
@@ -353,25 +306,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
@@ -393,8 +344,7 @@
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
@@ -405,8 +355,7 @@
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -416,8 +365,7 @@
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -427,8 +375,7 @@
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
@@ -437,17 +384,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy": {
|
||||
@@ -480,8 +432,7 @@
|
||||
},
|
||||
"node_modules/http-proxy-middleware/node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
@@ -491,8 +442,7 @@
|
||||
},
|
||||
"node_modules/http-proxy-middleware/node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@@ -502,16 +452,14 @@
|
||||
},
|
||||
"node_modules/http-proxy-middleware/node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-middleware/node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
@@ -522,8 +470,7 @@
|
||||
},
|
||||
"node_modules/http-proxy-middleware/node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
@@ -532,14 +479,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
@@ -570,62 +521,58 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"license": "MIT",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"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": {
|
||||
"version": "1.52.0",
|
||||
"license": "MIT",
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"license": "MIT",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
@@ -633,32 +580,27 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node": {
|
||||
"version": "20.19.5",
|
||||
"resolved": "https://registry.npmjs.org/node/-/node-20.19.5.tgz",
|
||||
"integrity": "sha512-9fJOHEP8AVrwpbhlUxnbudW8IbkseQVxl4yNQyI/rDfP+gNwKEmfPtBc/Luyf677i5Y0HKIBHiApiB9S9vvxKw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"node-bin-setup": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node": "bin/node"
|
||||
"node": "bin/node.exe"
|
||||
},
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "2.6.7",
|
||||
"license": "MIT",
|
||||
@@ -677,10 +619,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node/node_modules/node-bin-setup": {
|
||||
"version": "1.1.4",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -698,6 +643,14 @@
|
||||
"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": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -708,8 +661,7 @@
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
@@ -740,11 +692,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"version": "6.14.1",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
@@ -762,40 +713,46 @@
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.7.0",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"is-promise": "^4.0.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"path-to-regexp": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/router/node_modules/path-to-regexp": {
|
||||
"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": {
|
||||
"version": "2.1.2",
|
||||
@@ -803,61 +760,46 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "2.0.1"
|
||||
"debug": "^4.4.3",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.1",
|
||||
"mime-types": "^3.0.2",
|
||||
"ms": "^2.1.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"range-parser": "^1.2.1",
|
||||
"statuses": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/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/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": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
|
||||
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.19.0"
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"parseurl": "^1.3.3",
|
||||
"send": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
@@ -866,8 +808,7 @@
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
@@ -884,8 +825,7 @@
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
@@ -899,8 +839,7 @@
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -916,8 +855,7 @@
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -933,8 +871,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"license": "MIT",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -951,11 +890,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"license": "MIT",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
"content-type": "^1.0.5",
|
||||
"media-typer": "^1.1.0",
|
||||
"mime-types": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -968,13 +909,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
@@ -993,6 +927,11 @@
|
||||
"tr46": "~0.0.3",
|
||||
"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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
"keywords": [],
|
||||
"author": "Microsoft Corporation",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.3",
|
||||
"express": "^4.21.2",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"body-parser": "^2.2.2",
|
||||
"express": "^5.2.1",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
"node": "^20.19.5",
|
||||
"node-fetch": "^2.6.1",
|
||||
"path-to-regexp": "^0.1.12"
|
||||
|
||||
11
src/@types/i18next.d.ts
vendored
Normal file
11
src/@types/i18next.d.ts
vendored
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
||||
import { isExpectedError } from "../Metrics/ErrorClassification";
|
||||
import { scenarioMonitor } from "../Metrics/ScenarioMonitor";
|
||||
import { userContext } from "../UserContext";
|
||||
import { ARMError } from "../Utils/arm/request";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
@@ -7,19 +9,36 @@ import { HttpStatusCodes } from "./Constants";
|
||||
import { logError } from "./Logger";
|
||||
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 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;
|
||||
logConsoleError(consoleErrorMessage);
|
||||
|
||||
// logs error to both app insight and kusto
|
||||
logError(errorMessage, area, errorCode);
|
||||
// logs error to both app insight and kusto (use redacted message if provided)
|
||||
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
|
||||
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 => {
|
||||
|
||||
@@ -38,7 +38,7 @@ export function queryIterator(databaseId: string, collection: Collection, query:
|
||||
let continuationToken: string;
|
||||
return {
|
||||
fetchNext: () => {
|
||||
return queryDocuments(databaseId, collection, false, query).then((response) => {
|
||||
return queryDocuments(databaseId, collection, false, query, continuationToken).then((response) => {
|
||||
continuationToken = response.continuationToken;
|
||||
const headers: { [key: string]: string | number } = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
|
||||
@@ -44,7 +44,8 @@ export const deleteDocuments = async (
|
||||
documentIds: DocumentId[],
|
||||
abortSignal: AbortSignal,
|
||||
): Promise<IBulkDeleteResult[]> => {
|
||||
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
|
||||
const totalCount = documentIds.length;
|
||||
const clearMessage = logConsoleProgress(`Deleting ${totalCount} ${getEntityName(true)}`);
|
||||
try {
|
||||
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);
|
||||
return flatAllResult;
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
"DeleteDocuments",
|
||||
`Error while deleting ${documentIds.length} ${getEntityName(documentIds.length > 1)}`,
|
||||
);
|
||||
handleError(error, "DeleteDocuments", `Error while deleting ${totalCount} ${getEntityName(totalCount > 1)}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
|
||||
171
src/Common/dataAccess/queryDocumentsPage.test.ts
Normal file
171
src/Common/dataAccess/queryDocumentsPage.test.ts
Normal 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__");
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,51 @@ import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
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 (
|
||||
resourceName: string,
|
||||
documentsIterator: MinimalQueryIterator,
|
||||
@@ -18,7 +63,12 @@ export const queryDocumentsPage = async (
|
||||
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
||||
return result;
|
||||
} 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;
|
||||
} finally {
|
||||
clearMessage();
|
||||
|
||||
@@ -77,6 +77,12 @@ let configContext: Readonly<ConfigContext> = {
|
||||
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
|
||||
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
||||
`^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
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
|
||||
@@ -46,6 +46,10 @@ export type DataExploreMessageV3 =
|
||||
params: {
|
||||
updateType: "created" | "deleted" | "settings";
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: FabricMessageTypes.RestoreContainer;
|
||||
params: [];
|
||||
};
|
||||
export interface GetCosmosTokenMessageOptions {
|
||||
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
||||
|
||||
@@ -275,8 +275,7 @@ export interface DataMaskingPolicy {
|
||||
startPosition: number;
|
||||
length: number;
|
||||
}>;
|
||||
excludedPaths: string[];
|
||||
isPolicyEnabled: boolean;
|
||||
excludedPaths?: string[];
|
||||
}
|
||||
|
||||
export interface MaterializedView {
|
||||
@@ -348,7 +347,6 @@ export interface Offer {
|
||||
export interface ThroughputBucket {
|
||||
id: number;
|
||||
maxThroughputPercentage: number;
|
||||
isDefaultBucket?: boolean;
|
||||
}
|
||||
|
||||
export interface SDKOfferDefinition extends Resource {
|
||||
|
||||
@@ -184,5 +184,10 @@ export default {
|
||||
Skipped: "Cancelled",
|
||||
Cancelled: "Cancelled",
|
||||
},
|
||||
dialog: {
|
||||
heading: "",
|
||||
confirmButtonText: "Confirm",
|
||||
cancelButtonText: "Cancel",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -395,6 +395,14 @@ describe("CopyJobUtils", () => {
|
||||
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", () => {
|
||||
const result = CopyJobUtils.isEqual([], []);
|
||||
expect(result).toBe(true);
|
||||
|
||||
@@ -142,7 +142,7 @@ export function isEqual(prevJobs: CopyJobType[], newJobs: CopyJobType[]): boolea
|
||||
if (!newJob) {
|
||||
return false;
|
||||
}
|
||||
return prevJob.Status === newJob.Status;
|
||||
return prevJob.Status === newJob.Status && prevJob.CompletionPercentage === newJob.CompletionPercentage;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
@@ -5,6 +6,20 @@ import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../E
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
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", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
@@ -18,6 +33,11 @@ jest.mock("../../ContainerCopyMessages", () => ({
|
||||
cancel: "Cancel",
|
||||
complete: "Complete",
|
||||
},
|
||||
dialog: {
|
||||
heading: "Confirm Action",
|
||||
confirmButtonText: "Confirm",
|
||||
cancelButtonText: "Cancel",
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -50,6 +70,9 @@ describe("CopyJobActionMenu", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockShowOkCancelModalDialog.mockClear();
|
||||
mockCloseDialog.mockClear();
|
||||
mockOpenDialog.mockClear();
|
||||
});
|
||||
|
||||
describe("Component Rendering", () => {
|
||||
@@ -266,7 +289,29 @@ describe("CopyJobActionMenu", () => {
|
||||
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 });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
@@ -277,6 +322,9 @@ describe("CopyJobActionMenu", () => {
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
|
||||
onOkCallback();
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function));
|
||||
});
|
||||
|
||||
@@ -294,7 +342,33 @@ describe("CopyJobActionMenu", () => {
|
||||
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({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
@@ -308,10 +382,87 @@ describe("CopyJobActionMenu", () => {
|
||||
const completeButton = screen.getByText("Complete");
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
|
||||
onOkCallback();
|
||||
|
||||
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", () => {
|
||||
const TestComponentWrapper: React.FC<{
|
||||
job: CopyJobType;
|
||||
@@ -339,8 +490,13 @@ describe("CopyJobActionMenu", () => {
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
fireEvent.click(actionButton);
|
||||
const pauseButtonAfterClick = screen.getByText("Pause");
|
||||
|
||||
const pauseButtonAfterClick = screen.getByText("Pause").closest("button");
|
||||
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", () => {
|
||||
@@ -360,23 +516,7 @@ describe("CopyJobActionMenu", () => {
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should properly handle multiple action types being disabled for the same job", () => {
|
||||
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", () => {
|
||||
it("should disable complete action when job is being updated", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
@@ -390,8 +530,34 @@ describe("CopyJobActionMenu", () => {
|
||||
const completeButton = screen.getByText("Complete");
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
// Simulate dialog confirmation to trigger state update
|
||||
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
|
||||
onOkCallback();
|
||||
|
||||
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("title", "Actions");
|
||||
expect(actionButton).toHaveAttribute("role", "button");
|
||||
|
||||
const moreIcon = actionButton.querySelector('[data-icon-name="More"]');
|
||||
expect(moreIcon || actionButton).toBeInTheDocument();
|
||||
@@ -608,4 +775,129 @@ describe("CopyJobActionMenu", () => {
|
||||
}).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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IconButton, IContextualMenuProps } from "@fluentui/react";
|
||||
import { DirectionalHint, IconButton, IContextualMenuProps, Stack } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import { useDialog } from "../../../Controls/Dialog";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
@@ -9,6 +10,28 @@ interface CopyJobActionMenuProps {
|
||||
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 [updatingJobAction, setUpdatingJobAction] = React.useState<{ jobName: string; action: string } | null>(null);
|
||||
if (
|
||||
@@ -22,9 +45,22 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
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 isThisJobUpdating = updatingJobAction?.jobName === job.Name;
|
||||
const updatingAction = updatingJobAction?.action;
|
||||
|
||||
const baseItems = [
|
||||
{
|
||||
@@ -32,21 +68,21 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.pause,
|
||||
iconProps: { iconName: "Pause" },
|
||||
onClick: () => handleClick(job, CopyJobActions.pause, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.pause,
|
||||
disabled: isThisJobUpdating,
|
||||
},
|
||||
{
|
||||
key: CopyJobActions.cancel,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.cancel,
|
||||
iconProps: { iconName: "Cancel" },
|
||||
onClick: () => handleClick(job, CopyJobActions.cancel, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.cancel,
|
||||
onClick: () => showActionConfirmationDialog(job, CopyJobActions.cancel),
|
||||
disabled: isThisJobUpdating,
|
||||
},
|
||||
{
|
||||
key: CopyJobActions.resume,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.resume,
|
||||
iconProps: { iconName: "Play" },
|
||||
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,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.complete,
|
||||
iconProps: { iconName: "CheckMark" },
|
||||
onClick: () => handleClick(job, CopyJobActions.complete, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.complete,
|
||||
onClick: () => showActionConfirmationDialog(job, CopyJobActions.complete),
|
||||
disabled: isThisJobUpdating,
|
||||
});
|
||||
}
|
||||
return filteredItems;
|
||||
@@ -86,8 +122,8 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
data-test={`CopyJobActionMenu/Button:${job.Name}`}
|
||||
role="button"
|
||||
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
|
||||
menuProps={{ items: getMenuItems() }}
|
||||
menuIconProps={{ iconName: "" }}
|
||||
menuProps={{ items: getMenuItems(), directionalHint: DirectionalHint.leftTopEdge, directionalHintFixed: false }}
|
||||
menuIconProps={{ iconName: "", className: "hidden" }}
|
||||
ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
||||
title={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
||||
/>
|
||||
|
||||
@@ -11,9 +11,17 @@ jest.mock("../../Actions/CopyJobActions", () => ({
|
||||
|
||||
jest.mock("./CopyJobColumns", () => ({
|
||||
getColumns: jest.fn(() => [
|
||||
{
|
||||
key: "LastUpdatedTime",
|
||||
name: "Date & time",
|
||||
fieldName: "LastUpdatedTime",
|
||||
minWidth: 140,
|
||||
maxWidth: 300,
|
||||
isResizable: true,
|
||||
},
|
||||
{
|
||||
key: "Name",
|
||||
name: "Name",
|
||||
name: "Job name",
|
||||
fieldName: "Name",
|
||||
minWidth: 140,
|
||||
maxWidth: 300,
|
||||
@@ -165,6 +173,165 @@ describe("CopyJobsList", () => {
|
||||
expect(screen.getByTestId("action-menu-job-2")).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", () => {
|
||||
@@ -342,7 +509,7 @@ describe("CopyJobsList", () => {
|
||||
|
||||
describe("Component Props", () => {
|
||||
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],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
@@ -351,7 +518,7 @@ describe("CopyJobsList", () => {
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
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 () => {
|
||||
@@ -440,7 +607,33 @@ describe("CopyJobsList", () => {
|
||||
render(<CopyJobsList jobs={largeJobsList} handleActionClick={mockHandleActionClick} />);
|
||||
}).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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,8 +12,9 @@ import {
|
||||
Stack,
|
||||
Sticky,
|
||||
StickyPositionType,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import Pager from "../../../../Common/Pager";
|
||||
import { useThemeStore } from "../../../../hooks/useTheme";
|
||||
import { getThemeTokens } from "../../../Theme/ThemeUtil";
|
||||
@@ -30,9 +31,15 @@ interface CopyJobsListProps {
|
||||
const styles = {
|
||||
container: { height: "100%" } 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 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 [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined);
|
||||
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(() => {
|
||||
setSortedJobs(jobs);
|
||||
@@ -64,7 +88,15 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
|
||||
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) => {
|
||||
openCopyJobDetailsPanel(job);
|
||||
@@ -81,14 +113,25 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<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}>
|
||||
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
|
||||
<ShimmeredDetailsList
|
||||
className="CopyJobListContainer"
|
||||
onRenderRow={_onRenderRow}
|
||||
checkboxVisibility={2}
|
||||
columns={columns}
|
||||
items={sortedJobs.slice(startIndex, startIndex + pageSize)}
|
||||
columns={sortableColumns}
|
||||
items={filteredJobs.slice(startIndex, startIndex + pageSize)}
|
||||
enableShimmer={false}
|
||||
constrainMode={ConstrainMode.unconstrained}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
@@ -117,12 +160,12 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
|
||||
/>
|
||||
</ScrollablePane>
|
||||
</Stack.Item>
|
||||
{sortedJobs.length > pageSize && (
|
||||
{filteredJobs.length > pageSize && (
|
||||
<Stack.Item>
|
||||
<Pager
|
||||
disabled={false}
|
||||
startIndex={startIndex}
|
||||
totalCount={sortedJobs.length}
|
||||
totalCount={filteredJobs.length}
|
||||
pageSize={pageSize}
|
||||
onLoadPage={(startIdx /* pageSize */) => {
|
||||
setStartIndex(startIdx);
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
@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
|
||||
.themeText {
|
||||
color: var(--colorNeutralForeground1);
|
||||
@@ -119,25 +141,8 @@
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.ms-TextField {
|
||||
.ms-TextField-fieldGroup {
|
||||
background-color: var(--colorNeutralBackground1);
|
||||
border-color: var(--colorNeutralStroke1);
|
||||
}
|
||||
.themedTextFieldStyles();
|
||||
|
||||
.ms-TextField-field {
|
||||
color: var(--colorNeutralForeground1);
|
||||
background-color: var(--colorNeutralBackground1);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--colorNeutralForeground4);
|
||||
}
|
||||
}
|
||||
|
||||
.ms-Label {
|
||||
color: var(--colorNeutralForeground1);
|
||||
}
|
||||
}
|
||||
.migrationTypeDescription {
|
||||
p {
|
||||
color: var(--colorNeutralForeground1);
|
||||
@@ -173,6 +178,11 @@
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
body.isDarkMode & {
|
||||
.themedTextFieldStyles();
|
||||
}
|
||||
|
||||
.ms-DetailsList {
|
||||
width: 100%;
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
AddGlobalSecondaryIndexPanelProps,
|
||||
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { isFabric, isFabricNative, openRestoreContainerDialog } from "Platform/Fabric/FabricUtil";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
@@ -24,6 +26,7 @@ import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
|
||||
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
|
||||
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
@@ -56,10 +59,21 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
||||
{
|
||||
iconSrc: AddCollectionIcon,
|
||||
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)) {
|
||||
items.push({
|
||||
iconSrc: DeleteDatabaseIcon,
|
||||
@@ -68,11 +82,11 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getDatabaseName(),
|
||||
t(Keys.contextMenu.deleteDatabase, { databaseName: getDatabaseName() }),
|
||||
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
},
|
||||
label: `Delete ${getDatabaseName()}`,
|
||||
label: t(Keys.contextMenu.deleteDatabase, { databaseName: getDatabaseName() }),
|
||||
styleClass: "deleteDatabaseMenuItem",
|
||||
});
|
||||
}
|
||||
@@ -88,7 +102,7 @@ export const createCollectionContextMenuButton = (
|
||||
items.push({
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, undefined),
|
||||
label: "New SQL Query",
|
||||
label: t(Keys.contextMenu.newSqlQuery),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -96,7 +110,7 @@ export const createCollectionContextMenuButton = (
|
||||
items.push({
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, undefined),
|
||||
label: "New Query",
|
||||
label: t(Keys.contextMenu.newQuery),
|
||||
});
|
||||
|
||||
items.push({
|
||||
@@ -111,8 +125,8 @@ export const createCollectionContextMenuButton = (
|
||||
},
|
||||
label:
|
||||
useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell
|
||||
? "Open Mongo Shell"
|
||||
: "New Shell",
|
||||
? t(Keys.contextMenu.openMongoShell)
|
||||
: t(Keys.contextMenu.newShell),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -125,7 +139,7 @@ export const createCollectionContextMenuButton = (
|
||||
onClick: () => {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||
},
|
||||
label: "Open Cassandra Shell",
|
||||
label: t(Keys.contextMenu.openCassandraShell),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -138,7 +152,7 @@ export const createCollectionContextMenuButton = (
|
||||
onClick: () => {
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
|
||||
},
|
||||
label: "New Stored Procedure",
|
||||
label: t(Keys.contextMenu.newStoredProcedure),
|
||||
});
|
||||
|
||||
items.push({
|
||||
@@ -146,7 +160,7 @@ export const createCollectionContextMenuButton = (
|
||||
onClick: () => {
|
||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
||||
},
|
||||
label: "New UDF",
|
||||
label: t(Keys.contextMenu.newUdf),
|
||||
});
|
||||
|
||||
items.push({
|
||||
@@ -154,7 +168,7 @@ export const createCollectionContextMenuButton = (
|
||||
onClick: () => {
|
||||
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
|
||||
},
|
||||
label: "New Trigger",
|
||||
label: t(Keys.contextMenu.newTrigger),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,11 +181,11 @@ export const createCollectionContextMenuButton = (
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getCollectionName(),
|
||||
t(Keys.contextMenu.deleteContainer, { containerName: getCollectionName() }),
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
},
|
||||
label: `Delete ${getCollectionName()}`,
|
||||
label: t(Keys.contextMenu.deleteContainer, { containerName: getCollectionName() }),
|
||||
styleClass: "deleteCollectionMenuItem",
|
||||
});
|
||||
}
|
||||
@@ -208,14 +222,14 @@ export const createSampleCollectionContextMenuButton = (): TreeNodeMenuItem[] =>
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
|
||||
},
|
||||
label: "New SQL Query",
|
||||
label: t(Keys.contextMenu.newSqlQuery),
|
||||
});
|
||||
} else if (copilotVersion === "v2.0") {
|
||||
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
|
||||
items.push({
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
onClick: () => sampleCollection && sampleCollection.onNewQueryClick(sampleCollection, undefined),
|
||||
label: "New SQL Query",
|
||||
label: t(Keys.contextMenu.newSqlQuery),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -235,7 +249,7 @@ export const createStoreProcedureContextMenuItems = (
|
||||
{
|
||||
iconSrc: DeleteSprocIcon,
|
||||
onClick: () => storedProcedure.delete(),
|
||||
label: "Delete Stored Procedure",
|
||||
label: t(Keys.contextMenu.deleteStoredProcedure),
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -249,7 +263,7 @@ export const createTriggerContextMenuItems = (container: Explorer, trigger: Trig
|
||||
{
|
||||
iconSrc: DeleteTriggerIcon,
|
||||
onClick: () => trigger.delete(),
|
||||
label: "Delete Trigger",
|
||||
label: t(Keys.contextMenu.deleteTrigger),
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -266,7 +280,7 @@ export const createUserDefinedFunctionContextMenuItems = (
|
||||
{
|
||||
iconSrc: DeleteUDFIcon,
|
||||
onClick: () => userDefinedFunction.delete(),
|
||||
label: "Delete User Defined Function",
|
||||
label: t(Keys.contextMenu.deleteUdf),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import React, { FC, useEffect } from "react";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { Keys } from "../../Localization/Keys.generated";
|
||||
import { t } from "../../Localization/t";
|
||||
|
||||
export interface DialogState {
|
||||
visible: boolean;
|
||||
@@ -88,7 +90,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
||||
isModal: true,
|
||||
title,
|
||||
subText,
|
||||
primaryButtonText: "Close",
|
||||
primaryButtonText: t(Keys.common.close),
|
||||
secondaryButtonText: undefined,
|
||||
onPrimaryButtonClick: () => {
|
||||
get().closeDialog();
|
||||
|
||||
@@ -30,7 +30,6 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
|
||||
dataMaskingPolicy: {
|
||||
includedPaths: [],
|
||||
excludedPaths: ["/excludedPath"],
|
||||
isPolicyEnabled: true,
|
||||
},
|
||||
indexes: [],
|
||||
}),
|
||||
@@ -307,12 +306,10 @@ describe("SettingsComponent", () => {
|
||||
dataMaskingContent: {
|
||||
includedPaths: [],
|
||||
excludedPaths: ["/excludedPath"],
|
||||
isPolicyEnabled: true,
|
||||
},
|
||||
dataMaskingContentBaseline: {
|
||||
includedPaths: [],
|
||||
excludedPaths: [],
|
||||
isPolicyEnabled: false,
|
||||
},
|
||||
isDataMaskingDirty: true,
|
||||
});
|
||||
@@ -326,7 +323,6 @@ describe("SettingsComponent", () => {
|
||||
expect(wrapper.state("dataMaskingContentBaseline")).toEqual({
|
||||
includedPaths: [],
|
||||
excludedPaths: ["/excludedPath"],
|
||||
isPolicyEnabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -340,7 +336,6 @@ describe("SettingsComponent", () => {
|
||||
const invalidPolicy: InvalidPolicy = {
|
||||
includedPaths: "invalid",
|
||||
excludedPaths: [],
|
||||
isPolicyEnabled: false,
|
||||
};
|
||||
// Use type assertion since we're deliberately testing with invalid data
|
||||
settingsComponentInstance["onDataMaskingContentChange"](invalidPolicy as unknown as DataModels.DataMaskingPolicy);
|
||||
@@ -349,7 +344,6 @@ describe("SettingsComponent", () => {
|
||||
expect(wrapper.state("dataMaskingContent")).toEqual({
|
||||
includedPaths: "invalid",
|
||||
excludedPaths: [],
|
||||
isPolicyEnabled: false,
|
||||
});
|
||||
expect(wrapper.state("dataMaskingValidationErrors")).toEqual(["includedPaths must be an array"]);
|
||||
|
||||
@@ -364,7 +358,6 @@ describe("SettingsComponent", () => {
|
||||
},
|
||||
],
|
||||
excludedPaths: ["/excludedPath"],
|
||||
isPolicyEnabled: true,
|
||||
};
|
||||
|
||||
settingsComponentInstance["onDataMaskingContentChange"](validPolicy);
|
||||
@@ -388,7 +381,6 @@ describe("SettingsComponent", () => {
|
||||
},
|
||||
],
|
||||
excludedPaths: ["/excludedPath1"],
|
||||
isPolicyEnabled: false,
|
||||
};
|
||||
|
||||
const modifiedPolicy = {
|
||||
@@ -401,7 +393,6 @@ describe("SettingsComponent", () => {
|
||||
},
|
||||
],
|
||||
excludedPaths: ["/excludedPath2"],
|
||||
isPolicyEnabled: true,
|
||||
};
|
||||
|
||||
// Set initial state
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { isCapabilityEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||
import * as React from "react";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
@@ -44,6 +44,8 @@ import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter
|
||||
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||
import "./SettingsComponent.less";
|
||||
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
import {
|
||||
ConflictResolutionComponent,
|
||||
ConflictResolutionComponentProps,
|
||||
@@ -70,6 +72,7 @@ import {
|
||||
getMongoNotification,
|
||||
getTabTitle,
|
||||
hasDatabaseSharedThroughput,
|
||||
isDataMaskingEnabled,
|
||||
isDirty,
|
||||
parseConflictResolutionMode,
|
||||
parseConflictResolutionProcedure,
|
||||
@@ -686,22 +689,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.setState({ isComputedPropertiesDirty: isComputedPropertiesDirty });
|
||||
|
||||
private onDataMaskingContentChange = (newDataMasking: DataModels.DataMaskingPolicy): void => {
|
||||
if (!newDataMasking.excludedPaths) {
|
||||
newDataMasking.excludedPaths = [];
|
||||
}
|
||||
if (!newDataMasking.includedPaths) {
|
||||
newDataMasking.includedPaths = [];
|
||||
}
|
||||
|
||||
const validationErrors = [];
|
||||
if (!Array.isArray(newDataMasking.includedPaths)) {
|
||||
validationErrors.push("includedPaths must be an array");
|
||||
if (newDataMasking.includedPaths === undefined || newDataMasking.includedPaths === null) {
|
||||
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)) {
|
||||
validationErrors.push("excludedPaths must be an array");
|
||||
}
|
||||
if (typeof newDataMasking.isPolicyEnabled !== "boolean") {
|
||||
validationErrors.push("isPolicyEnabled must be a boolean");
|
||||
if (newDataMasking.excludedPaths !== undefined && !Array.isArray(newDataMasking.excludedPaths)) {
|
||||
validationErrors.push(t(Keys.controls.settings.dataMasking.excludedPathsMustBeArray));
|
||||
}
|
||||
|
||||
this.setState({
|
||||
@@ -842,7 +837,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const dataMaskingContent: DataModels.DataMaskingPolicy = {
|
||||
includedPaths: this.collection.dataMaskingPolicy?.()?.includedPaths || [],
|
||||
excludedPaths: this.collection.dataMaskingPolicy?.()?.excludedPaths || [],
|
||||
isPolicyEnabled: this.collection.dataMaskingPolicy?.()?.isPolicyEnabled ?? true,
|
||||
};
|
||||
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
||||
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
||||
@@ -904,7 +898,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
const isExecuting = this.props.settingsTab.isExecuting();
|
||||
if (this.saveSettingsButton.isVisible()) {
|
||||
const label = "Save";
|
||||
const label = t(Keys.common.save);
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
@@ -917,7 +911,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
|
||||
if (this.discardSettingsChangesButton.isVisible()) {
|
||||
const label = "Discard";
|
||||
const label = t(Keys.common.discard);
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
@@ -942,9 +936,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
|
||||
const throughputDelta = (newThroughput - this.offer.autoscaleMaxThroughput) * numberOfRegions;
|
||||
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 ${
|
||||
this.totalThroughputUsed + throughputDelta
|
||||
} RU/s. Change total throughput limit in cost management.`;
|
||||
throughputError = t(Keys.controls.settings.throughput.throughputCapError, {
|
||||
throughputCap: String(throughputCap),
|
||||
newTotalThroughput: String(this.totalThroughputUsed + throughputDelta),
|
||||
});
|
||||
}
|
||||
this.setState({ autoPilotThroughput: newThroughput, throughputError });
|
||||
};
|
||||
@@ -955,9 +950,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
|
||||
const throughputDelta = (newThroughput - this.offer.manualThroughput) * numberOfRegions;
|
||||
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 ${
|
||||
this.totalThroughputUsed + throughputDelta
|
||||
} RU/s. Change total throughput limit in cost management.`;
|
||||
throughputError = t(Keys.controls.settings.throughput.throughputCapError, {
|
||||
throughputCap: String(throughputCap),
|
||||
newTotalThroughput: String(this.totalThroughputUsed + throughputDelta),
|
||||
});
|
||||
}
|
||||
this.setState({ throughput: newThroughput, throughputError });
|
||||
};
|
||||
@@ -1073,8 +1069,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
newCollection.fullTextPolicy = this.state.fullTextPolicy;
|
||||
|
||||
// Only send data masking policy if it was modified (dirty)
|
||||
if (this.state.isDataMaskingDirty && isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking)) {
|
||||
// Only send data masking policy if it was modified (dirty) and data masking is enabled
|
||||
if (this.state.isDataMaskingDirty && isDataMaskingEnabled(this.collection.dataMaskingPolicy?.())) {
|
||||
newCollection.dataMaskingPolicy = this.state.dataMaskingContent;
|
||||
}
|
||||
|
||||
@@ -1463,15 +1459,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
});
|
||||
}
|
||||
|
||||
// Check if DDM should be enabled
|
||||
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()) {
|
||||
if (isDataMaskingEnabled(this.collection.dataMaskingPolicy?.())) {
|
||||
const dataMaskingComponentProps: DataMaskingComponentProps = {
|
||||
shouldDiscardDataMasking: this.state.shouldDiscardDataMasking,
|
||||
resetShouldDiscardDataMasking: this.resetShouldDiscardDataMasking,
|
||||
@@ -1576,7 +1564,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
>
|
||||
{this.shouldShowKeyspaceSharedThroughputMessage() && (
|
||||
<div>This table shared throughput is configured at the keyspace</div>
|
||||
<div>{t(Keys.controls.settings.scale.keyspaceSharedThroughput)}</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
import * as React from "react";
|
||||
import { Urls } from "../../../Common/Constants";
|
||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
import { hoursInAMonth } from "../../../Shared/Constants";
|
||||
import {
|
||||
computeRUUsagePriceHourly,
|
||||
@@ -338,10 +340,12 @@ export const getEstimatedSpendingElement = (
|
||||
const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : "";
|
||||
return (
|
||||
<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}
|
||||
<Text style={{ fontWeight: 600, marginTop: 15, color: "var(--colorNeutralForeground1)" }}>
|
||||
How we calculate this
|
||||
{t(Keys.controls.settings.costEstimate.howWeCalculate)}
|
||||
</Text>
|
||||
<Stack id="throughputSpendElement" style={{ marginTop: 5 }}>
|
||||
<span>
|
||||
@@ -353,7 +357,8 @@ export const getEstimatedSpendingElement = (
|
||||
</span>
|
||||
<span>
|
||||
{priceBreakdown.currencySign}
|
||||
{priceBreakdown.pricePerRu}/RU
|
||||
{priceBreakdown.pricePerRu}
|
||||
{t(Keys.controls.settings.costEstimate.perRu)}
|
||||
</span>
|
||||
</Stack>
|
||||
<Text style={{ marginTop: 15, color: "var(--colorNeutralForeground1)" }}>
|
||||
@@ -365,18 +370,16 @@ export const getEstimatedSpendingElement = (
|
||||
|
||||
export const manualToAutoscaleDisclaimerElement: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
|
||||
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings
|
||||
and storage of your resource. After autoscale has been enabled, you can change the max RU/s.{" "}
|
||||
{t(Keys.controls.settings.throughput.manualToAutoscaleDisclaimer)}{" "}
|
||||
<Link href={Urls.autoscaleMigration}>Learn more</Link>
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const ttlWarning: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete
|
||||
operation explicitly issued by a client application. For more information see,{" "}
|
||||
{t(Keys.controls.settings.throughput.ttlWarningText)}{" "}
|
||||
<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>
|
||||
.
|
||||
</Text>
|
||||
@@ -384,29 +387,28 @@ export const ttlWarning: JSX.Element = (
|
||||
|
||||
export const unsavedEditorWarningMessage = (editor: editorType): JSX.Element => (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
You have not saved the latest changes made to your{" "}
|
||||
{t(Keys.controls.settings.throughput.unsavedEditorWarningPrefix)}{" "}
|
||||
{editor === "indexPolicy"
|
||||
? "indexing policy"
|
||||
? t(Keys.controls.settings.throughput.unsavedIndexingPolicy)
|
||||
: editor === "dataMasking"
|
||||
? "data masking policy"
|
||||
: "computed properties"}
|
||||
. Please click save to confirm the changes.
|
||||
? t(Keys.controls.settings.throughput.unsavedDataMaskingPolicy)
|
||||
: t(Keys.controls.settings.throughput.unsavedComputedProperties)}
|
||||
{t(Keys.controls.settings.throughput.unsavedEditorWarningSuffix)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
|
||||
You are about to request an increase in throughput beyond the pre-allocated capacity. This operation will take some
|
||||
time to complete.
|
||||
{t(Keys.controls.settings.throughput.updateDelayedApplyWarning)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const getUpdateThroughputBeyondInstantLimitMessage = (instantMaximumThroughput: number): JSX.Element => {
|
||||
return (
|
||||
<Text id="updateThroughputDelayedApplyWarningMessage">
|
||||
Scaling up will take 4-6 hours as it exceeds what Azure Cosmos DB can instantly support currently based on your
|
||||
number of physical partitions. You can increase your throughput to {instantMaximumThroughput} instantly or proceed
|
||||
with this value and wait until the scale-up is completed.
|
||||
{t(Keys.controls.settings.throughput.scalingUpDelayMessage, {
|
||||
instantMaximumThroughput: String(instantMaximumThroughput),
|
||||
})}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -418,22 +420,26 @@ export const getUpdateThroughputBeyondSupportLimitMessage = (
|
||||
return (
|
||||
<>
|
||||
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
|
||||
Your request to increase throughput exceeds the pre-allocated capacity which may take longer than expected.
|
||||
There are three options you can choose from to proceed:
|
||||
{t(Keys.controls.settings.throughput.exceedPreAllocatedMessage)}
|
||||
</Text>
|
||||
<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 && (
|
||||
<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>
|
||||
Your current quota max is {maximumThroughput} RU/s. To go over this limit, you must request a quota increase
|
||||
and the Azure Cosmos DB team will review.
|
||||
{t(Keys.controls.settings.throughput.quotaMaxOption, { maximumThroughput: String(maximumThroughput) })}
|
||||
<Link
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</li>
|
||||
</ol>
|
||||
@@ -444,23 +450,19 @@ export const getUpdateThroughputBeyondSupportLimitMessage = (
|
||||
export const getUpdateThroughputBelowMinimumMessage = (minimum: number): JSX.Element => {
|
||||
return (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
You are not able to lower throughput below your current minimum of {minimum} RU/s. For more information on this
|
||||
limit, please refer to our service quote documentation.
|
||||
{t(Keys.controls.settings.throughput.belowMinimumMessage, { minimum: String(minimum) })}
|
||||
<Link
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export const saveThroughputWarningMessage: JSX.Element = (
|
||||
<Text>
|
||||
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below
|
||||
before saving your changes
|
||||
</Text>
|
||||
<Text>{t(Keys.controls.settings.throughput.saveThroughputWarning)}</Text>
|
||||
);
|
||||
|
||||
const getCurrentThroughput = (
|
||||
@@ -472,23 +474,29 @@ const getCurrentThroughput = (
|
||||
if (targetThroughput) {
|
||||
if (throughput) {
|
||||
return isAutoscale
|
||||
? `, Current autoscale throughput: ${Math.round(
|
||||
? `, ${t(Keys.controls.settings.throughput.currentAutoscaleThroughput)} ${Math.round(
|
||||
throughput / 10,
|
||||
)} - ${throughput} ${throughputUnit}, Target autoscale throughput: ${Math.round(
|
||||
targetThroughput / 10,
|
||||
)} - ${targetThroughput} ${throughputUnit}`
|
||||
: `, Current manual throughput: ${throughput} ${throughputUnit}, Target manual throughput: ${targetThroughput}`;
|
||||
)} - ${throughput} ${throughputUnit}, ${t(
|
||||
Keys.controls.settings.throughput.targetAutoscaleThroughput,
|
||||
)} ${Math.round(targetThroughput / 10)} - ${targetThroughput} ${throughputUnit}`
|
||||
: `, ${t(Keys.controls.settings.throughput.currentManualThroughput)} ${throughput} ${throughputUnit}, ${t(
|
||||
Keys.controls.settings.throughput.targetManualThroughput,
|
||||
)} ${targetThroughput}`;
|
||||
} else {
|
||||
return isAutoscale
|
||||
? `, Target autoscale throughput: ${Math.round(targetThroughput / 10)} - ${targetThroughput} ${throughputUnit}`
|
||||
: `, Target manual throughput: ${targetThroughput} ${throughputUnit}`;
|
||||
? `, ${t(Keys.controls.settings.throughput.targetAutoscaleThroughput)} ${Math.round(
|
||||
targetThroughput / 10,
|
||||
)} - ${targetThroughput} ${throughputUnit}`
|
||||
: `, ${t(Keys.controls.settings.throughput.targetManualThroughput)} ${targetThroughput} ${throughputUnit}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetThroughput && throughput) {
|
||||
return isAutoscale
|
||||
? `, Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} ${throughputUnit}`
|
||||
: `, Current manual throughput: ${throughput} ${throughputUnit}`;
|
||||
? `, ${t(Keys.controls.settings.throughput.currentAutoscaleThroughput)} ${Math.round(
|
||||
throughput / 10,
|
||||
)} - ${throughput} ${throughputUnit}`
|
||||
: `, ${t(Keys.controls.settings.throughput.currentManualThroughput)} ${throughput} ${throughputUnit}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
@@ -503,10 +511,10 @@ export const getThroughputApplyDelayedMessage = (
|
||||
requestedThroughput: number,
|
||||
): JSX.Element => (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
The request to increase the throughput has successfully been submitted. This operation will take 1-3 business days
|
||||
to complete. View the latest status in Notifications.
|
||||
{t(Keys.controls.settings.throughput.applyDelayedMessage)}
|
||||
<br />
|
||||
Database: {databaseName}, Container: {collectionName}{" "}
|
||||
{t(Keys.controls.settings.throughput.databaseLabel)} {databaseName},{" "}
|
||||
{t(Keys.controls.settings.throughput.containerLabel)} {collectionName}{" "}
|
||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
|
||||
</Text>
|
||||
);
|
||||
@@ -519,9 +527,13 @@ export const getThroughputApplyShortDelayMessage = (
|
||||
collectionName: string,
|
||||
): JSX.Element => (
|
||||
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
|
||||
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
||||
{t(Keys.controls.settings.throughput.applyShortDelayMessage)}
|
||||
<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)}
|
||||
</Text>
|
||||
);
|
||||
@@ -535,10 +547,13 @@ export const getThroughputApplyLongDelayMessage = (
|
||||
requestedThroughput: number,
|
||||
): JSX.Element => (
|
||||
<Text styles={infoAndToolTipTextStyle} id="throughputApplyLongDelayMessage">
|
||||
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to
|
||||
complete. View the latest status in Notifications.
|
||||
{t(Keys.controls.settings.throughput.applyLongDelayMessage)}
|
||||
<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)}
|
||||
</Text>
|
||||
);
|
||||
@@ -547,63 +562,49 @@ export const getToolTipContainer = (content: string | JSX.Element): JSX.Element
|
||||
content ? <Text styles={infoAndToolTipTextStyle}>{content}</Text> : undefined;
|
||||
|
||||
export const conflictResolutionLwwTooltip: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
Gets or sets the name of a integer property in your documents which is used for the Last Write Wins (LWW) based
|
||||
conflict resolution scheme. By default, the system uses the system defined timestamp property, _ts to decide the
|
||||
winner for the conflicting versions of the document. Specify your own integer property if you want to override the
|
||||
default timestamp based conflict resolution.
|
||||
</Text>
|
||||
<Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.conflictResolution.lwwTooltip)}</Text>
|
||||
);
|
||||
|
||||
export const conflictResolutionCustomToolTip: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
Gets or sets the name of a stored procedure (aka merge procedure) for resolving the conflicts. You can write
|
||||
application defined logic to determine the winner of the conflicting versions of a document. The stored procedure
|
||||
will get executed transactionally, exactly once, on the server side. If you do not provide a stored procedure, the
|
||||
conflicts will be populated in the
|
||||
{t(Keys.controls.settings.conflictResolution.customTooltip)}
|
||||
<Link className="linkDarkBackground" href="https://aka.ms/dataexplorerconflics" target="_blank">
|
||||
{` conflicts feed`}
|
||||
{t(Keys.controls.settings.conflictResolution.customTooltipConflictsFeed)}
|
||||
</Link>
|
||||
. You can update/re-register the stored procedure at any time.
|
||||
{t(Keys.controls.settings.conflictResolution.customTooltipSuffix)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const changeFeedPolicyToolTip: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
Enable change feed log retention policy to retain last 10 minutes of history for items in the container by default.
|
||||
To support this, the request unit (RU) charge for this container will be multiplied by a factor of two for writes.
|
||||
Reads are unaffected.
|
||||
</Text>
|
||||
<Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.changeFeed.tooltip)}</Text>
|
||||
);
|
||||
|
||||
export const mongoIndexingPolicyDisclaimer: JSX.Element = (
|
||||
<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
|
||||
href="https://docs.microsoft.com/azure/cosmos-db/mongodb-indexing#index-types"
|
||||
target="_blank"
|
||||
style={{ color: "var(--colorBrandForeground1)" }}
|
||||
>
|
||||
{` Compound indexes `}
|
||||
{t(Keys.controls.settings.mongoIndexing.disclaimerCompoundIndexesLink)}
|
||||
</Link>
|
||||
are only used for sorting query results. If you need to add a compound index, you can create one using the Mongo
|
||||
shell.
|
||||
{t(Keys.controls.settings.mongoIndexing.disclaimerSuffix)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const mongoCompoundIndexNotSupportedMessage: JSX.Element = (
|
||||
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
Collections with compound indexes are not yet supported in the indexing editor. To modify indexing policy for this
|
||||
collection, use the Mongo Shell.
|
||||
{t(Keys.controls.settings.mongoIndexing.compoundNotSupported)}
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const mongoIndexingPolicyAADError: JSX.Element = (
|
||||
<MessageBar messageBarType={MessageBarType.error}>
|
||||
<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">
|
||||
{"azure portal."}
|
||||
{t(Keys.controls.settings.mongoIndexing.aadErrorLink)}
|
||||
</Link>
|
||||
</Text>
|
||||
</MessageBar>
|
||||
@@ -611,7 +612,7 @@ export const mongoIndexingPolicyAADError: JSX.Element = (
|
||||
|
||||
export const mongoIndexTransformationRefreshingMessage: JSX.Element = (
|
||||
<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} />
|
||||
</Stack>
|
||||
);
|
||||
@@ -623,15 +624,18 @@ export const renderMongoIndexTransformationRefreshMessage = (
|
||||
if (progress === 0) {
|
||||
return (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
{"You can make more indexing changes once the current index transformation is complete. "}
|
||||
<Link onClick={performRefresh}>{"Refresh to check if it has completed."}</Link>
|
||||
{t(Keys.controls.settings.mongoIndexing.canMakeMoreChangesZero)}
|
||||
<Link onClick={performRefresh}>{t(Keys.controls.settings.mongoIndexing.refreshToCheck)}</Link>
|
||||
</Text>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
{`You can make more indexing changes once the current index transformation has completed. It is ${progress}% complete. `}
|
||||
<Link onClick={performRefresh}>{"Refresh to check the progress."}</Link>
|
||||
{`${t(Keys.controls.settings.mongoIndexing.canMakeMoreChangesProgress).replace(
|
||||
"{{progress}}",
|
||||
String(progress),
|
||||
)} `}
|
||||
<Link onClick={performRefresh}>{t(Keys.controls.settings.mongoIndexing.refreshToCheckProgress)}</Link>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { titleAndInputStackProps, unsavedEditorWarningMessage } from "Explorer/C
|
||||
import { isDirty } from "Explorer/Controls/Settings/SettingsUtils";
|
||||
import { loadMonaco } from "Explorer/LazyMonaco";
|
||||
import { monacoTheme, useThemeStore } from "hooks/useTheme";
|
||||
import { Keys } from "../../../../Localization/Keys.generated";
|
||||
import { t } from "../../../../Localization/t";
|
||||
import * as monaco from "monaco-editor";
|
||||
import * as React from "react";
|
||||
export interface ComputedPropertiesComponentProps {
|
||||
@@ -107,7 +109,7 @@ export class ComputedPropertiesComponent extends React.Component<
|
||||
this.computedPropertiesEditor = monaco.editor.create(this.computedPropertiesDiv.current, {
|
||||
value: value,
|
||||
language: "json",
|
||||
ariaLabel: "Computed properties",
|
||||
ariaLabel: t(Keys.controls.settings.computedProperties.ariaLabel),
|
||||
theme: monacoTheme(),
|
||||
});
|
||||
if (this.computedPropertiesEditor) {
|
||||
@@ -151,9 +153,9 @@ export class ComputedPropertiesComponent extends React.Component<
|
||||
)}
|
||||
<Text style={{ marginLeft: "30px", marginBottom: "10px", color: "var(--colorNeutralForeground1)" }}>
|
||||
<Link target="_blank" href="https://aka.ms/computed-properties-preview/">
|
||||
{"Learn more"} <FontIcon iconName="NavigateExternalInline" />
|
||||
{t(Keys.common.learnMore)} <FontIcon iconName="NavigateExternalInline" />
|
||||
</Link>
|
||||
  about how to define computed properties and how to use them.
|
||||
  {t(Keys.controls.settings.computedProperties.learnMorePrefix)}
|
||||
</Text>
|
||||
<div
|
||||
className="settingsV2Editor"
|
||||
|
||||
@@ -2,6 +2,8 @@ import { ChoiceGroup, IChoiceGroupOption, ITextFieldProps, Stack, TextField } fr
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import { Keys } from "../../../../Localization/Keys.generated";
|
||||
import { t } from "../../../../Localization/t";
|
||||
import {
|
||||
conflictResolutionCustomToolTip,
|
||||
conflictResolutionLwwTooltip,
|
||||
@@ -32,9 +34,12 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
|
||||
private conflictResolutionChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{
|
||||
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 {
|
||||
@@ -85,7 +90,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
|
||||
|
||||
private getConflictResolutionModeComponent = (): JSX.Element => (
|
||||
<ChoiceGroup
|
||||
label="Mode"
|
||||
label={t(Keys.controls.settings.conflictResolution.mode)}
|
||||
selectedKey={this.props.conflictResolutionPolicyMode}
|
||||
options={this.conflictResolutionChoiceGroupOptions}
|
||||
onChange={this.onConflictResolutionPolicyModeChange}
|
||||
@@ -103,7 +108,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
|
||||
private getConflictResolutionLWWComponent = (): JSX.Element => (
|
||||
<TextField
|
||||
id="conflictResolutionLwwTextField"
|
||||
label={"Conflict Resolver Property"}
|
||||
label={t(Keys.controls.settings.conflictResolution.conflictResolverProperty)}
|
||||
onRenderLabel={this.onRenderLwwComponentTextField}
|
||||
styles={{
|
||||
fieldGroup: {
|
||||
@@ -158,7 +163,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
|
||||
return (
|
||||
<TextField
|
||||
id="conflictResolutionCustomTextField"
|
||||
label="Stored procedure"
|
||||
label={t(Keys.controls.settings.conflictResolution.storedProcedure)}
|
||||
onRenderLabel={this.onRenderCustomComponentTextField}
|
||||
styles={{
|
||||
fieldGroup: {
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils";
|
||||
import { ContainerPolicyTabTypes, isDirty } from "Explorer/Controls/Settings/SettingsUtils";
|
||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||
import { Keys } from "../../../../Localization/Keys.generated";
|
||||
import { t } from "../../../../Localization/t";
|
||||
import React from "react";
|
||||
|
||||
export interface ContainerPolicyComponentProps {
|
||||
@@ -153,7 +155,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
|
||||
<PivotItem
|
||||
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.VectorPolicyTab]}
|
||||
style={{ marginTop: 20, color: "var(--colorNeutralForeground1)" }}
|
||||
headerText="Vector Policy"
|
||||
headerText={t(Keys.controls.settings.containerPolicy.vectorPolicy)}
|
||||
>
|
||||
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
|
||||
{vectorEmbeddings && (
|
||||
@@ -175,7 +177,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
|
||||
<PivotItem
|
||||
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.FullTextPolicyTab]}
|
||||
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" } }}>
|
||||
{fullTextSearchPolicy ? (
|
||||
@@ -218,7 +220,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
|
||||
});
|
||||
}}
|
||||
>
|
||||
Create new full text search policy
|
||||
{t(Keys.controls.settings.containerPolicy.createFullTextPolicy)}
|
||||
</DefaultButton>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -53,7 +53,6 @@ describe("DataMaskingComponent", () => {
|
||||
},
|
||||
],
|
||||
excludedPaths: [],
|
||||
isPolicyEnabled: false,
|
||||
};
|
||||
|
||||
let changeContentCallback: () => void;
|
||||
@@ -78,7 +77,7 @@ describe("DataMaskingComponent", () => {
|
||||
<DataMaskingComponent
|
||||
{...mockProps}
|
||||
dataMaskingContent={samplePolicy}
|
||||
dataMaskingContentBaseline={{ ...samplePolicy, isPolicyEnabled: true }}
|
||||
dataMaskingContentBaseline={{ ...samplePolicy, excludedPaths: ["/excluded"] }}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -123,7 +122,7 @@ describe("DataMaskingComponent", () => {
|
||||
});
|
||||
|
||||
it("resets content when shouldDiscardDataMasking is true", async () => {
|
||||
const baselinePolicy = { ...samplePolicy, isPolicyEnabled: true };
|
||||
const baselinePolicy = { ...samplePolicy, excludedPaths: ["/excluded"] };
|
||||
|
||||
const wrapper = mount(
|
||||
<DataMaskingComponent
|
||||
@@ -159,7 +158,7 @@ describe("DataMaskingComponent", () => {
|
||||
wrapper.update();
|
||||
|
||||
// Update baseline to trigger componentDidUpdate
|
||||
const newBaseline = { ...samplePolicy, isPolicyEnabled: true };
|
||||
const newBaseline = { ...samplePolicy, excludedPaths: ["/excluded"] };
|
||||
wrapper.setProps({ dataMaskingContentBaseline: newBaseline });
|
||||
|
||||
expect(mockProps.onDataMaskingDirtyChange).toHaveBeenCalledWith(true);
|
||||
@@ -174,7 +173,6 @@ describe("DataMaskingComponent", () => {
|
||||
const invalidPolicy: Record<string, unknown> = {
|
||||
includedPaths: "not an array",
|
||||
excludedPaths: [] as string[],
|
||||
isPolicyEnabled: "not a boolean",
|
||||
};
|
||||
|
||||
mockGetValue.mockReturnValue(JSON.stringify(invalidPolicy));
|
||||
@@ -197,7 +195,7 @@ describe("DataMaskingComponent", () => {
|
||||
wrapper.update();
|
||||
|
||||
// First change
|
||||
const modifiedPolicy1 = { ...samplePolicy, isPolicyEnabled: true };
|
||||
const modifiedPolicy1 = { ...samplePolicy, excludedPaths: ["/path1"] };
|
||||
mockGetValue.mockReturnValue(JSON.stringify(modifiedPolicy1));
|
||||
changeContentCallback();
|
||||
expect(mockProps.onDataMaskingDirtyChange).toHaveBeenCalledWith(true);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { MessageBar, MessageBarType, Stack } from "@fluentui/react";
|
||||
import * as monaco from "monaco-editor";
|
||||
import * as React from "react";
|
||||
import * as Constants from "../../../../Common/Constants";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import { isCapabilityEnabled } from "../../../../Utils/CapabilityUtils";
|
||||
import { Keys } from "../../../../Localization/Keys.generated";
|
||||
import { t } from "../../../../Localization/t";
|
||||
import { loadMonaco } from "../../../LazyMonaco";
|
||||
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
|
||||
import { isDirty as isContentDirty } from "../SettingsUtils";
|
||||
import { isDirty as isContentDirty, isDataMaskingEnabled } from "../SettingsUtils";
|
||||
|
||||
export interface DataMaskingComponentProps {
|
||||
shouldDiscardDataMasking: boolean;
|
||||
@@ -24,16 +24,8 @@ interface DataMaskingComponentState {
|
||||
}
|
||||
|
||||
const emptyDataMaskingPolicy: DataModels.DataMaskingPolicy = {
|
||||
includedPaths: [
|
||||
{
|
||||
path: "/",
|
||||
strategy: "Default",
|
||||
startPosition: 0,
|
||||
length: -1,
|
||||
},
|
||||
],
|
||||
includedPaths: [],
|
||||
excludedPaths: [],
|
||||
isPolicyEnabled: true,
|
||||
};
|
||||
|
||||
export class DataMaskingComponent extends React.Component<DataMaskingComponentProps, DataMaskingComponentState> {
|
||||
@@ -99,7 +91,7 @@ export class DataMaskingComponent extends React.Component<DataMaskingComponentPr
|
||||
value: value,
|
||||
language: "json",
|
||||
automaticLayout: true,
|
||||
ariaLabel: "Data Masking Policy",
|
||||
ariaLabel: t(Keys.controls.settings.dataMasking.ariaLabel),
|
||||
fontSize: 13,
|
||||
minimap: { enabled: false },
|
||||
wordWrap: "off",
|
||||
@@ -140,7 +132,7 @@ export class DataMaskingComponent extends React.Component<DataMaskingComponentPr
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (!isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking)) {
|
||||
if (!isDataMaskingEnabled(this.props.dataMaskingContent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -152,7 +144,7 @@ export class DataMaskingComponent extends React.Component<DataMaskingComponentPr
|
||||
)}
|
||||
{this.props.validationErrors.length > 0 && (
|
||||
<MessageBar messageBarType={MessageBarType.error}>
|
||||
Validation failed: {this.props.validationErrors.join(", ")}
|
||||
{t(Keys.controls.settings.dataMasking.validationFailed)} {this.props.validationErrors.join(", ")}
|
||||
</MessageBar>
|
||||
)}
|
||||
<div className="settingsV2Editor" tabIndex={0} ref={this.dataMaskingDiv}></div>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { FontIcon, Link, Stack, Text } from "@fluentui/react";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import React from "react";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import { Keys } from "../../../../Localization/Keys.generated";
|
||||
import { t } from "../../../../Localization/t";
|
||||
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
|
||||
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
|
||||
|
||||
@@ -21,7 +23,9 @@ export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexCompone
|
||||
<Stack tokens={{ childrenGap: 8 }} styles={{ root: { maxWidth: 600 } }}>
|
||||
<Stack horizontal verticalAlign="center" wrap tokens={{ childrenGap: 8 }}>
|
||||
{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>
|
||||
<Link
|
||||
@@ -31,7 +35,7 @@ export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexCompone
|
||||
Learn more
|
||||
<FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} />
|
||||
</Link>{" "}
|
||||
about how to define global secondary indexes and how to use them.
|
||||
{t(Keys.controls.settings.globalSecondaryIndex.learnMoreSuffix)}
|
||||
</Text>
|
||||
</Stack>
|
||||
{isSourceContainer && <GlobalSecondaryIndexSourceComponent collection={collection} explorer={explorer} />}
|
||||
|
||||
@@ -9,6 +9,8 @@ import { useSidePanel } from "hooks/useSidePanel";
|
||||
import * as monaco from "monaco-editor";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import { Keys } from "../../../../Localization/Keys.generated";
|
||||
import { t } from "../../../../Localization/t";
|
||||
|
||||
export interface GlobalSecondaryIndexSourceComponentProps {
|
||||
collection: ViewModels.Collection;
|
||||
@@ -67,7 +69,7 @@ export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexS
|
||||
editorRef.current = monacoInstance.editor.create(editorContainerRef.current, {
|
||||
value: jsonValue,
|
||||
language: "json",
|
||||
ariaLabel: "Global Secondary Index JSON",
|
||||
ariaLabel: t(Keys.controls.settings.globalSecondaryIndex.jsonAriaLabel),
|
||||
readOnly: true,
|
||||
});
|
||||
};
|
||||
@@ -98,7 +100,7 @@ export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexS
|
||||
}}
|
||||
/>
|
||||
<PrimaryButton
|
||||
text="Add index"
|
||||
text={t(Keys.controls.settings.globalSecondaryIndex.addIndex)}
|
||||
styles={{ root: { width: "fit-content", marginTop: 12 } }}
|
||||
onClick={() =>
|
||||
useSidePanel
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Stack, Text } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import { Keys } from "../../../../Localization/Keys.generated";
|
||||
import { t } from "../../../../Localization/t";
|
||||
|
||||
export interface GlobalSecondaryIndexTargetComponentProps {
|
||||
collection: ViewModels.Collection;
|
||||
@@ -25,17 +27,21 @@ export const GlobalSecondaryIndexTargetComponent: React.FC<GlobalSecondaryIndexT
|
||||
|
||||
return (
|
||||
<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 }}>
|
||||
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
|
||||
<Text styles={{ root: { fontWeight: "600" } }}>
|
||||
{t(Keys.controls.settings.globalSecondaryIndex.sourceContainer)}
|
||||
</Text>
|
||||
<Stack styles={valueBoxStyle}>
|
||||
<Text>{globalSecondaryIndexDefinition?.sourceCollectionId}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<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}>
|
||||
<Text>{globalSecondaryIndexDefinition?.definition}</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { monacoTheme, useThemeStore } from "hooks/useTheme";
|
||||
import * as monaco from "monaco-editor";
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import { Keys } from "../../../../Localization/Keys.generated";
|
||||
import { t } from "../../../../Localization/t";
|
||||
import { loadMonaco } from "../../../LazyMonaco";
|
||||
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
|
||||
import { isDirty, isIndexTransforming } from "../SettingsUtils";
|
||||
@@ -119,7 +121,7 @@ export class IndexingPolicyComponent extends React.Component<
|
||||
value: value,
|
||||
language: "json",
|
||||
readOnly: isIndexTransforming(this.props.indexTransformationProgress),
|
||||
ariaLabel: "Indexing Policy",
|
||||
ariaLabel: t(Keys.controls.settings.indexingPolicy.ariaLabel),
|
||||
theme: monacoTheme(),
|
||||
});
|
||||
if (this.indexingPolicyEditor) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { MessageBar, MessageBarType } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
|
||||
import { Keys } from "../../../../../Localization/Keys.generated";
|
||||
import { t } from "../../../../../Localization/t";
|
||||
import {
|
||||
mongoIndexTransformationRefreshingMessage,
|
||||
renderMongoIndexTransformationRefreshMessage,
|
||||
@@ -46,7 +48,11 @@ export class IndexingPolicyRefreshComponent extends React.Component<
|
||||
try {
|
||||
await this.props.refreshIndexTransformationProgress();
|
||||
} catch (error) {
|
||||
handleError(error, "RefreshIndexTransformationProgress", "Refreshing index transformation progress failed");
|
||||
handleError(
|
||||
error,
|
||||
"RefreshIndexTransformationProgress",
|
||||
t(Keys.controls.settings.indexingPolicyRefresh.refreshFailed),
|
||||
);
|
||||
} finally {
|
||||
this.setState({ isRefreshing: false });
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
|
||||
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
|
||||
<StyledLinkBase
|
||||
onClick={[Function]}
|
||||
>
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
IDropdownOption,
|
||||
ITextField,
|
||||
} from "@fluentui/react";
|
||||
import { Keys } from "../../../../../Localization/Keys.generated";
|
||||
import { t } from "../../../../../Localization/t";
|
||||
import {
|
||||
addMongoIndexSubElementsTokens,
|
||||
mongoErrorMessageStyles,
|
||||
@@ -66,7 +68,7 @@ export class AddMongoIndexComponent extends React.Component<AddMongoIndexCompone
|
||||
<Stack {...mongoWarningStackProps}>
|
||||
<Stack horizontal tokens={addMongoIndexSubElementsTokens}>
|
||||
<TextField
|
||||
ariaLabel={"Index Field Name " + this.props.position}
|
||||
ariaLabel={t(Keys.controls.settings.mongoIndexing.indexFieldName) + " " + this.props.position}
|
||||
disabled={this.props.disabled}
|
||||
styles={shortWidthTextFieldStyles}
|
||||
componentRef={this.setRef}
|
||||
@@ -76,17 +78,17 @@ export class AddMongoIndexComponent extends React.Component<AddMongoIndexCompone
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
ariaLabel={"Index Type " + this.props.position}
|
||||
ariaLabel={t(Keys.controls.settings.mongoIndexing.indexType) + " " + this.props.position}
|
||||
disabled={this.props.disabled}
|
||||
styles={shortWidthDropDownStyles}
|
||||
placeholder="Select an index type"
|
||||
placeholder={t(Keys.controls.settings.mongoIndexing.selectIndexType)}
|
||||
selectedKey={this.props.type}
|
||||
options={this.indexTypes}
|
||||
onChange={this.onTypeChange}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
ariaLabel={"Undo Button " + this.props.position}
|
||||
ariaLabel={t(Keys.controls.settings.mongoIndexing.undoButton) + " " + this.props.position}
|
||||
iconProps={{ iconName: "Undo" }}
|
||||
disabled={!this.props.description && !this.props.type}
|
||||
onClick={() => this.props.onDiscard()}
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { MongoIndex } from "../../../../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { Keys } from "../../../../../Localization/Keys.generated";
|
||||
import { t } from "../../../../../Localization/t";
|
||||
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import {
|
||||
addMongoIndexStackProps,
|
||||
@@ -83,11 +85,25 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
};
|
||||
|
||||
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",
|
||||
name: "Drop Index",
|
||||
name: t(Keys.controls.settings.mongoIndexing.dropIndexColumn),
|
||||
fieldName: "actionButton",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
@@ -96,11 +112,25 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
];
|
||||
|
||||
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",
|
||||
name: "Add index back",
|
||||
name: t(Keys.controls.settings.mongoIndexing.addIndexBackColumn),
|
||||
fieldName: "actionButton",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
@@ -161,7 +191,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
private getActionButton = (arrayPosition: number, isCurrentIndex: boolean): JSX.Element => {
|
||||
return isCurrentIndex ? (
|
||||
<IconButton
|
||||
ariaLabel="Delete index Button"
|
||||
ariaLabel={t(Keys.controls.settings.mongoIndexing.deleteIndexButton)}
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
disabled={isIndexTransforming(this.props.indexTransformationProgress)}
|
||||
onClick={() => {
|
||||
@@ -170,7 +200,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
ariaLabel="Add back Index Button"
|
||||
ariaLabel={t(Keys.controls.settings.mongoIndexing.addBackIndexButton)}
|
||||
iconProps={{ iconName: "Add" }}
|
||||
onClick={() => {
|
||||
this.props.onRevertIndexDrop(arrayPosition);
|
||||
@@ -258,7 +288,10 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
|
||||
return (
|
||||
<Stack {...createAndAddMongoIndexStackProps} styles={mediumWidthStackStyles}>
|
||||
<CollapsibleSectionComponent title="Current index(es)" isExpandedByDefault={true}>
|
||||
<CollapsibleSectionComponent
|
||||
title={t(Keys.controls.settings.mongoIndexing.currentIndexes)}
|
||||
isExpandedByDefault={true}
|
||||
>
|
||||
{
|
||||
<>
|
||||
<DetailsList
|
||||
@@ -285,7 +318,10 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
|
||||
return (
|
||||
<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 && (
|
||||
<DetailsList
|
||||
styles={customDetailsListStyles}
|
||||
|
||||
@@ -18,6 +18,8 @@ import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/da
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
|
||||
import { Keys } from "../../../../Localization/Keys.generated";
|
||||
import { t } from "../../../../Localization/t";
|
||||
import {
|
||||
CosmosSqlDataTransferDataSourceSink,
|
||||
DataTransferJobGetResults,
|
||||
@@ -80,7 +82,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
||||
return (collection.partitionKeyProperties || []).map((property) => "/" + property).join(", ");
|
||||
};
|
||||
|
||||
const partitionKeyName = "Partition key";
|
||||
const partitionKeyName = t(Keys.controls.settings.partitionKey.partitionKey);
|
||||
const partitionKeyValue = getPartitionKeyValue();
|
||||
|
||||
const textHeadingStyle = {
|
||||
@@ -148,7 +150,13 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
||||
const getProgressDescription = (): string => {
|
||||
const processedCount = portalDataTransferJob?.properties?.processedCount;
|
||||
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}`;
|
||||
};
|
||||
|
||||
@@ -181,16 +189,28 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 20 }} styles={{ root: { maxWidth: 600 } }}>
|
||||
<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 tokens={{ childrenGap: 5 }}>
|
||||
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
|
||||
<Text styles={textSubHeadingStyle}>Partitioning</Text>
|
||||
<Text styles={textSubHeadingStyle}>
|
||||
{t(Keys.controls.settings.partitionKeyEditor.currentPartitionKey, {
|
||||
partitionKeyName: partitionKeyName.toLowerCase(),
|
||||
})}
|
||||
</Text>
|
||||
<Text styles={textSubHeadingStyle}>{t(Keys.controls.settings.partitionKeyEditor.partitioning)}</Text>
|
||||
</Stack>
|
||||
<Stack tokens={{ childrenGap: 5 }} data-test="partition-key-values">
|
||||
<Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text>
|
||||
<Text styles={textSubHeadingStyle1}>
|
||||
{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}
|
||||
{isHierarchicalPartitionedContainer()
|
||||
? t(Keys.controls.settings.partitionKeyEditor.hierarchical)
|
||||
: t(Keys.controls.settings.partitionKeyEditor.nonHierarchical)}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -204,33 +224,33 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
||||
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||
styles={darkThemeMessageBarStyles}
|
||||
>
|
||||
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to
|
||||
the source container for the entire duration of the partition key change process.
|
||||
{t(Keys.controls.settings.partitionKeyEditor.safeguardWarning)}
|
||||
<Link
|
||||
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
|
||||
target="_blank"
|
||||
underline
|
||||
style={{ color: "var(--colorBrandForeground1)" }}
|
||||
>
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</MessageBar>
|
||||
<Text styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>
|
||||
To change the partition key, a new destination container must be created or an existing destination
|
||||
container selected. Data will then be copied to the destination container.
|
||||
{t(Keys.controls.settings.partitionKeyEditor.changeDescription)}
|
||||
</Text>
|
||||
{configContext.platform !== Platform.Emulator && (
|
||||
<PrimaryButton
|
||||
data-test="change-partition-key-button"
|
||||
styles={{ root: { width: "fit-content" } }}
|
||||
text="Change"
|
||||
text={t(Keys.controls.settings.partitionKeyEditor.changeButton)}
|
||||
onClick={startPartitionkeyChangeWorkflow}
|
||||
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
||||
/>
|
||||
)}
|
||||
{portalDataTransferJob && (
|
||||
<Stack>
|
||||
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
||||
<Text styles={textHeadingStyle}>
|
||||
{t(Keys.controls.settings.partitionKeyEditor.changeJob, { partitionKeyName })}
|
||||
</Text>
|
||||
<Stack
|
||||
horizontal
|
||||
tokens={{ childrenGap: 20 }}
|
||||
@@ -251,7 +271,10 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
||||
}}
|
||||
></ProgressIndicator>
|
||||
{isCurrentJobInProgress(portalDataTransferJob) && (
|
||||
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
|
||||
<DefaultButton
|
||||
text={t(Keys.controls.settings.partitionKeyEditor.cancelButton)}
|
||||
onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -4,6 +4,8 @@ import * as Constants from "../../../../Common/Constants";
|
||||
import { Platform, configContext } from "../../../../ConfigContext";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import { Keys } from "../../../../Localization/Keys.generated";
|
||||
import { t } from "../../../../Localization/t";
|
||||
import * as SharedConstants from "../../../../Shared/Constants";
|
||||
import { userContext } from "../../../../UserContext";
|
||||
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
||||
@@ -156,14 +158,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
const freeTierLimits = SharedConstants.FreeTierLimits;
|
||||
return (
|
||||
<Text>
|
||||
With free tier, you will get the first {freeTierLimits.RU} RU/s and {freeTierLimits.Storage} GB of storage in
|
||||
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.
|
||||
{t(Keys.controls.settings.scale.freeTierInfo, { ru: freeTierLimits.RU, storage: freeTierLimits.Storage })}
|
||||
<Link
|
||||
href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more.
|
||||
{t(Keys.controls.settings.scale.freeTierLearnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
@@ -188,12 +188,9 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
{/* TODO: Replace link with call to the Azure Support blade */}
|
||||
{this.isAutoScaleEnabled() && (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<Text>Throughput (RU/s)</Text>
|
||||
<Text>{t(Keys.controls.settings.scale.throughputRuS)}</Text>
|
||||
<TextField disabled styles={getTextFieldStyles(undefined, undefined)} />
|
||||
<Text>
|
||||
Your account has custom settings that prevents setting throughput at the container level. Please work with
|
||||
your Cosmos DB engineering team point of contact to make changes.
|
||||
</Text>
|
||||
<Text>{t(Keys.controls.settings.scale.autoScaleCustomSettings)}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import { Keys } from "../../../../Localization/Keys.generated";
|
||||
import { t } from "../../../../Localization/t";
|
||||
import { userContext } from "../../../../UserContext";
|
||||
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
import {
|
||||
@@ -85,9 +87,12 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
constructor(props: SubSettingsComponentProps) {
|
||||
super(props);
|
||||
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.uniqueKeyName = "Unique keys";
|
||||
this.uniqueKeyName = t(Keys.controls.settings.subSettings.uniqueKeys);
|
||||
this.uniqueKeyValue = this.getUniqueKeyValue();
|
||||
}
|
||||
|
||||
@@ -143,9 +148,13 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
};
|
||||
|
||||
private ttlChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: TtlType.Off, text: "Off", 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.Off, text: t(Keys.controls.settings.subSettings.ttlOff), ariaLabel: "ttl-off-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 => {
|
||||
@@ -216,13 +225,13 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
To enable time-to-live (TTL) for your collection/documents,{" "}
|
||||
{t(Keys.controls.settings.subSettings.mongoTtlMessage)}{" "}
|
||||
<Link
|
||||
href="https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-time-to-live"
|
||||
target="_blank"
|
||||
style={{ color: "var(--colorBrandForeground1)" }}
|
||||
>
|
||||
create a TTL index
|
||||
{t(Keys.controls.settings.subSettings.mongoTtlLinkText)}
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
@@ -231,7 +240,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<ChoiceGroup
|
||||
id="timeToLive"
|
||||
label="Time to Live"
|
||||
label={t(Keys.controls.settings.subSettings.timeToLive)}
|
||||
selectedKey={this.props.timeToLive}
|
||||
options={this.ttlChoiceGroupOptions}
|
||||
onChange={this.onTtlChange}
|
||||
@@ -255,8 +264,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
max={Int32.Max}
|
||||
value={this.props.displayedTtlSeconds}
|
||||
onChange={this.onTimeToLiveSecondsChange}
|
||||
suffix="second(s)"
|
||||
ariaLabel={`Time to live in seconds`}
|
||||
suffix={t(Keys.controls.settings.subSettings.seconds)}
|
||||
ariaLabel={t(Keys.controls.settings.subSettings.timeToLiveInSeconds)}
|
||||
data-test="ttl-input"
|
||||
/>
|
||||
)}
|
||||
@@ -264,16 +273,16 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
);
|
||||
|
||||
private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: TtlType.Off, text: "Off", disabled: true },
|
||||
{ key: TtlType.OnNoDefault, text: "On (no default)" },
|
||||
{ key: TtlType.On, text: "On" },
|
||||
{ key: TtlType.Off, text: t(Keys.controls.settings.subSettings.ttlOff), disabled: true },
|
||||
{ key: TtlType.OnNoDefault, text: t(Keys.controls.settings.subSettings.ttlOnNoDefault) },
|
||||
{ key: TtlType.On, text: t(Keys.controls.settings.subSettings.ttlOn) },
|
||||
];
|
||||
|
||||
private getAnalyticalStorageTtlComponent = (): JSX.Element => (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<ChoiceGroup
|
||||
id="analyticalStorageTimeToLive"
|
||||
label="Analytical Storage Time to Live"
|
||||
label={t(Keys.controls.settings.subSettings.analyticalStorageTtl)}
|
||||
selectedKey={this.props.analyticalStorageTtlSelection}
|
||||
options={this.analyticalTtlChoiceGroupOptions}
|
||||
onChange={this.onAnalyticalStorageTtlSelectionChange}
|
||||
@@ -294,7 +303,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
min={1}
|
||||
max={Int32.Max}
|
||||
value={this.props.analyticalStorageTtlSeconds?.toString()}
|
||||
suffix="second(s)"
|
||||
suffix={t(Keys.controls.settings.subSettings.seconds)}
|
||||
onChange={this.onAnalyticalStorageTtlSecondsChange}
|
||||
/>
|
||||
)}
|
||||
@@ -302,14 +311,22 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
);
|
||||
|
||||
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 => (
|
||||
<ChoiceGroup
|
||||
id="geoSpatialConfig"
|
||||
label="Geospatial Configuration"
|
||||
label={t(Keys.controls.settings.subSettings.geospatialConfiguration)}
|
||||
selectedKey={this.props.geospatialConfigType}
|
||||
options={this.geoSpatialConfigTypeChoiceGroupOptions}
|
||||
onChange={this.onGeoSpatialConfigTypeChange}
|
||||
@@ -318,8 +335,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
);
|
||||
|
||||
private changeFeedChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: ChangeFeedPolicyState.Off, text: "Off" },
|
||||
{ key: ChangeFeedPolicyState.On, text: "On" },
|
||||
{ key: ChangeFeedPolicyState.Off, text: t(Keys.controls.settings.subSettings.ttlOff) },
|
||||
{ key: ChangeFeedPolicyState.On, text: t(Keys.controls.settings.subSettings.ttlOn) },
|
||||
];
|
||||
|
||||
private getChangeFeedComponent = (): JSX.Element => {
|
||||
@@ -328,7 +345,10 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
return (
|
||||
<Stack>
|
||||
<Label id={labelId}>
|
||||
<ToolTipLabelComponent label="Change feed log retention policy" toolTipElement={changeFeedPolicyToolTip} />
|
||||
<ToolTipLabelComponent
|
||||
label={t(Keys.controls.settings.changeFeed.label)}
|
||||
toolTipElement={changeFeedPolicyToolTip}
|
||||
/>
|
||||
</Label>
|
||||
<ChoiceGroup
|
||||
id="changeFeedPolicy"
|
||||
@@ -373,14 +393,20 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
)}
|
||||
|
||||
{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" &&
|
||||
(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>
|
||||
);
|
||||
|
||||
@@ -76,11 +76,11 @@ describe("ThroughputBucketsComponent", () => {
|
||||
fireEvent.change(input, { target: { value: "70" } });
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 70, isDefaultBucket: false },
|
||||
{ id: 2, maxThroughputPercentage: 60, isDefaultBucket: false },
|
||||
{ id: 3, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||
{ id: 4, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||
{ id: 5, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -102,11 +102,11 @@ describe("ThroughputBucketsComponent", () => {
|
||||
fireEvent.change(input2, { target: { value: "80" } });
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 70, isDefaultBucket: false },
|
||||
{ id: 2, maxThroughputPercentage: 80, isDefaultBucket: false },
|
||||
{ id: 3, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||
{ id: 4, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||
{ id: 5, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||
{ id: 1, maxThroughputPercentage: 70 },
|
||||
{ id: 2, maxThroughputPercentage: 80 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -134,8 +134,8 @@ describe("ThroughputBucketsComponent", () => {
|
||||
<ThroughputBucketsComponent
|
||||
{...defaultProps}
|
||||
currentBuckets={[
|
||||
{ id: 1, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||
{ id: 2, maxThroughputPercentage: 50, isDefaultBucket: false },
|
||||
{ id: 1, maxThroughputPercentage: 100 },
|
||||
{ id: 2, maxThroughputPercentage: 50 },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
@@ -157,21 +157,21 @@ describe("ThroughputBucketsComponent", () => {
|
||||
fireEvent.click(toggles[0]);
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||
{ id: 2, maxThroughputPercentage: 60, isDefaultBucket: false },
|
||||
{ id: 3, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||
{ id: 4, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||
{ id: 5, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||
{ id: 1, maxThroughputPercentage: 100 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
|
||||
fireEvent.click(toggles[0]);
|
||||
|
||||
expect(mockOnBucketsChange).toHaveBeenCalledWith([
|
||||
{ id: 1, maxThroughputPercentage: 50, isDefaultBucket: false },
|
||||
{ id: 2, maxThroughputPercentage: 60, isDefaultBucket: false },
|
||||
{ id: 3, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||
{ id: 4, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||
{ id: 5, maxThroughputPercentage: 100, isDefaultBucket: false },
|
||||
{ id: 1, maxThroughputPercentage: 50 },
|
||||
{ id: 2, maxThroughputPercentage: 60 },
|
||||
{ id: 3, maxThroughputPercentage: 100 },
|
||||
{ id: 4, maxThroughputPercentage: 100 },
|
||||
{ id: 5, maxThroughputPercentage: 100 },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
import {
|
||||
Dropdown,
|
||||
Icon,
|
||||
IDropdownOption,
|
||||
Label,
|
||||
Link,
|
||||
Slider,
|
||||
Stack,
|
||||
Text,
|
||||
TextField,
|
||||
Toggle,
|
||||
TooltipHost,
|
||||
} from "@fluentui/react";
|
||||
import { Label, Slider, Stack, TextField, Toggle } from "@fluentui/react";
|
||||
import { ThroughputBucket } from "Contracts/DataModels";
|
||||
import { Keys } from "../../../../../Localization/Keys.generated";
|
||||
import { t } from "../../../../../Localization/t";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { isDirty } from "../../SettingsUtils";
|
||||
|
||||
@@ -20,7 +10,6 @@ const MAX_BUCKET_SIZES = 5;
|
||||
const DEFAULT_BUCKETS = Array.from({ length: MAX_BUCKET_SIZES }, (_, i) => ({
|
||||
id: i + 1,
|
||||
maxThroughputPercentage: 100,
|
||||
isDefaultBucket: false,
|
||||
}));
|
||||
|
||||
export interface ThroughputBucketsComponentProps {
|
||||
@@ -36,51 +25,19 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
||||
onBucketsChange,
|
||||
onSaveableChange,
|
||||
}) => {
|
||||
const NoDefaultThroughputSelectedKey: number = -1;
|
||||
const getThroughputBuckets = (buckets: ThroughputBucket[]): ThroughputBucket[] => {
|
||||
if (!buckets || buckets.length === 0) {
|
||||
return DEFAULT_BUCKETS;
|
||||
}
|
||||
const maxBuckets = Math.max(DEFAULT_BUCKETS.length, buckets.length);
|
||||
const adjustedDefaultBuckets: ThroughputBucket[] = Array.from(
|
||||
{ length: maxBuckets },
|
||||
(_, i) =>
|
||||
({
|
||||
id: i + 1,
|
||||
maxThroughputPercentage: 100,
|
||||
isDefaultBucket: false,
|
||||
}) as ThroughputBucket,
|
||||
);
|
||||
|
||||
return adjustedDefaultBuckets.map((defaultBucket: ThroughputBucket) => {
|
||||
const incoming: ThroughputBucket = buckets?.find((bucket) => bucket.id === defaultBucket.id);
|
||||
|
||||
return {
|
||||
...defaultBucket,
|
||||
...incoming,
|
||||
...(incoming?.isDefaultBucket && { isDefaultBucket: true }),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const getThroughputBucketOptions = (): IDropdownOption[] => {
|
||||
const noDefaultThroughputBucketSelected: IDropdownOption = {
|
||||
key: NoDefaultThroughputSelectedKey,
|
||||
text: "No Default Throughput Bucket Selected",
|
||||
};
|
||||
|
||||
const throughputBucketOptions: IDropdownOption[] = throughputBuckets.map((bucket) => ({
|
||||
key: bucket.id,
|
||||
text: `Bucket ${bucket.id} - ${bucket.maxThroughputPercentage}%`,
|
||||
disabled: bucket.maxThroughputPercentage === 100,
|
||||
...(bucket.maxThroughputPercentage === 100 && {
|
||||
data: {
|
||||
tooltip: `Bucket ${bucket.id} is not active.`,
|
||||
},
|
||||
}),
|
||||
const adjustedDefaultBuckets = Array.from({ length: maxBuckets }, (_, i) => ({
|
||||
id: i + 1,
|
||||
maxThroughputPercentage: 100,
|
||||
}));
|
||||
|
||||
return [noDefaultThroughputBucketSelected, ...throughputBucketOptions];
|
||||
return adjustedDefaultBuckets.map(
|
||||
(defaultBucket) => buckets?.find((bucket) => bucket.id === defaultBucket.id) || defaultBucket,
|
||||
);
|
||||
};
|
||||
|
||||
const [throughputBuckets, setThroughputBuckets] = useState<ThroughputBucket[]>(getThroughputBuckets(currentBuckets));
|
||||
@@ -97,13 +54,7 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
||||
|
||||
const handleBucketChange = (id: number, newValue: number) => {
|
||||
const updatedBuckets = throughputBuckets.map((bucket) =>
|
||||
bucket.id === id
|
||||
? {
|
||||
...bucket,
|
||||
maxThroughputPercentage: newValue,
|
||||
isDefaultBucket: newValue === 100 ? false : bucket.isDefaultBucket,
|
||||
}
|
||||
: bucket,
|
||||
bucket.id === id ? { ...bucket, maxThroughputPercentage: newValue } : bucket,
|
||||
);
|
||||
setThroughputBuckets(updatedBuckets);
|
||||
const settingsChanged = isDirty(updatedBuckets, throughputBuckets);
|
||||
@@ -114,38 +65,11 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
||||
handleBucketChange(id, checked ? 50 : 100);
|
||||
};
|
||||
|
||||
const onDefaultBucketToggle = (id: number, checked: boolean): void => {
|
||||
const updatedBuckets: ThroughputBucket[] = throughputBuckets.map((bucket) =>
|
||||
bucket.id === id ? { ...bucket, isDefaultBucket: checked } : { ...bucket, isDefaultBucket: false },
|
||||
);
|
||||
setThroughputBuckets(updatedBuckets);
|
||||
const settingsChanged = isDirty(updatedBuckets, throughputBuckets);
|
||||
settingsChanged && onBucketsChange(updatedBuckets);
|
||||
};
|
||||
|
||||
const onRenderDefaultThroughputBucketLabel = (): JSX.Element => {
|
||||
const tooltipContent = (): JSX.Element => (
|
||||
<Text>
|
||||
The default throughput bucket is used for operations that do not specify a particular bucket.{" "}
|
||||
<Link href="https://aka.ms/cosmsodb-bucketing" target="_blank">
|
||||
Learn more.
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Label>Default Throughput Bucket</Label>
|
||||
<TooltipHost content={tooltipContent()}>
|
||||
<Icon iconName="Info" styles={{ root: { marginLeft: 4, marginTop: 5 } }} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
{throughputBuckets?.map((bucket) => (
|
||||
<Stack key={bucket.id} horizontal tokens={{ childrenGap: 8 }} verticalAlign="center">
|
||||
@@ -156,7 +80,9 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
||||
value={bucket.maxThroughputPercentage}
|
||||
onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
|
||||
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={{
|
||||
root: { flex: 2, maxWidth: 400 },
|
||||
titleLabel: {
|
||||
@@ -177,58 +103,20 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
||||
fieldGroup: { width: 80 },
|
||||
}}
|
||||
disabled={bucket.maxThroughputPercentage === 100}
|
||||
data-test={`bucket-${bucket.id}-percentage-input`}
|
||||
/>
|
||||
<Toggle
|
||||
onText="Active"
|
||||
offText="Inactive"
|
||||
onText={t(Keys.controls.settings.throughputBuckets.active)}
|
||||
offText={t(Keys.controls.settings.throughputBuckets.inactive)}
|
||||
checked={bucket.maxThroughputPercentage !== 100}
|
||||
onChange={(event, checked) => onToggle(bucket.id, checked)}
|
||||
styles={{
|
||||
root: { marginBottom: 0 },
|
||||
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
|
||||
}}
|
||||
data-test={`bucket-${bucket.id}-active-toggle`}
|
||||
></Toggle>
|
||||
{/* <Toggle
|
||||
onText="Default"
|
||||
offText="Not Default"
|
||||
checked={bucket.isDefaultBucket || false}
|
||||
onChange={(_, checked) => onDefaultBucketToggle(bucket.id, checked)}
|
||||
disabled={bucket.maxThroughputPercentage === 100}
|
||||
styles={{
|
||||
root: { marginBottom: 0 },
|
||||
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
|
||||
}}
|
||||
/> */}
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
<Dropdown
|
||||
placeholder="Select a default throughput bucket"
|
||||
label="Default Throughput Bucket"
|
||||
options={getThroughputBucketOptions()}
|
||||
selectedKey={
|
||||
throughputBuckets?.find((throughputbucket: ThroughputBucket) => throughputbucket.isDefaultBucket)?.id ||
|
||||
NoDefaultThroughputSelectedKey
|
||||
}
|
||||
onChange={(_, option) => onDefaultBucketToggle(option.key as number, true)}
|
||||
onRenderLabel={onRenderDefaultThroughputBucketLabel}
|
||||
onRenderOption={(option: IDropdownOption) => {
|
||||
const tooltip: string = option?.data?.tooltip;
|
||||
if (!tooltip) {
|
||||
return <>{option?.text}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipHost content={tooltip}>
|
||||
<span>{option?.text}</span>
|
||||
</TooltipHost>
|
||||
);
|
||||
}}
|
||||
styles={{ root: { width: "50%" } }}
|
||||
data-test="default-throughput-bucket-dropdown"
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import React from "react";
|
||||
import * as DataModels from "../../../../../Contracts/DataModels";
|
||||
import { Keys } from "../../../../../Localization/Keys.generated";
|
||||
import { t } from "../../../../../Localization/t";
|
||||
import * as SharedConstants from "../../../../../Shared/Constants";
|
||||
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
|
||||
@@ -97,8 +99,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
private throughputInputMaxValue: number;
|
||||
private autoPilotInputMaxValue: number;
|
||||
private options: IChoiceGroupOption[] = [
|
||||
{ key: "true", text: "Autoscale" },
|
||||
{ key: "false", text: "Manual" },
|
||||
{ key: "true", text: t(Keys.controls.settings.throughputInput.autoscale) },
|
||||
{ key: "false", text: t(Keys.controls.settings.throughputInput.manual) },
|
||||
];
|
||||
|
||||
// Style constants for theme-aware colors and layout
|
||||
@@ -244,7 +246,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
return (
|
||||
<div>
|
||||
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
|
||||
Updated cost per month
|
||||
{t(Keys.controls.settings.costEstimate.updatedCostPerMonth)}
|
||||
</Text>
|
||||
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
|
||||
<Text
|
||||
@@ -253,7 +255,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
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
|
||||
style={{
|
||||
@@ -261,7 +264,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
|
||||
}}
|
||||
>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} max
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}{" "}
|
||||
{t(Keys.controls.settings.throughputInput.max)}
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
@@ -274,7 +278,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
||||
{newThroughput && newThroughputCostElement()}
|
||||
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
|
||||
Current cost per month
|
||||
{t(Keys.controls.settings.costEstimate.currentCostPerMonth)}
|
||||
</Text>
|
||||
<Stack horizontal style={{ marginTop: 5, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
|
||||
<Text
|
||||
@@ -283,7 +287,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
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
|
||||
style={{
|
||||
@@ -291,7 +296,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
|
||||
}}
|
||||
>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} max
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}{" "}
|
||||
{t(Keys.controls.settings.throughputInput.max)}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -326,7 +332,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
return (
|
||||
<div>
|
||||
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
|
||||
Updated cost per month
|
||||
{t(Keys.controls.settings.costEstimate.updatedCostPerMonth)}
|
||||
</Text>
|
||||
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
|
||||
<Text style={this.settingsAndScaleStyle.root}>
|
||||
@@ -349,7 +355,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
||||
{newThroughput && newThroughputCostElement()}
|
||||
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
|
||||
Current cost per month
|
||||
{t(Keys.controls.settings.costEstimate.currentCostPerMonth)}
|
||||
</Text>
|
||||
<Stack horizontal style={{ marginTop: 5 }}>
|
||||
<Text style={this.settingsAndScaleStyle.root}>
|
||||
@@ -444,10 +450,14 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
this.setState({ spendAckChecked: checked });
|
||||
|
||||
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 (
|
||||
<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>
|
||||
</Stack>
|
||||
);
|
||||
@@ -558,10 +568,14 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
/>
|
||||
<Stack horizontal>
|
||||
<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 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>
|
||||
</Stack>
|
||||
@@ -641,7 +655,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
variant="small"
|
||||
style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }}
|
||||
>
|
||||
Minimum RU/s
|
||||
{t(Keys.controls.settings.throughputInput.minimumRuS)}
|
||||
</Text>
|
||||
<FontIcon iconName="Info" style={{ fontSize: 12, color: "var(--colorNeutralForeground2)" }} />
|
||||
</Stack>
|
||||
@@ -675,7 +689,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
}}
|
||||
>
|
||||
x 10 =
|
||||
{t(Keys.controls.settings.throughputInput.x10Equals)}
|
||||
</Text>
|
||||
|
||||
{/* Column 3: Maximum RU/s */}
|
||||
@@ -685,7 +699,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
variant="small"
|
||||
style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }}
|
||||
>
|
||||
Maximum RU/s
|
||||
{t(Keys.controls.settings.throughputInput.maximumRuS)}
|
||||
</Text>
|
||||
<FontIcon iconName="Info" style={{ fontSize: 12, color: "var(--colorNeutralForeground2)" }} />
|
||||
</Stack>
|
||||
@@ -726,7 +740,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
onGetErrorMessage={(value: string) => {
|
||||
const sanitizedValue = getSanitizedInputValue(value);
|
||||
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>;
|
||||
}}
|
||||
validateOnLoad={false}
|
||||
@@ -772,7 +788,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
)}
|
||||
{this.props.isAutoPilotSelected ? (
|
||||
<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>
|
||||
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)} RU/s (10% of max RU/s) -{" "}
|
||||
{this.props.maxAutoPilotThroughput} RU/s
|
||||
@@ -787,16 +805,19 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
styles={this.darkThemeMessageBarStyles}
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!this.overrideWithProvisionedThroughputSettings() && (
|
||||
<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/">
|
||||
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />
|
||||
{t(Keys.controls.settings.throughputInput.capacityCalculatorLink)}{" "}
|
||||
<FontIcon iconName="NavigateExternalInline" />
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
@@ -809,9 +830,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
onChange={this.onSpendAckChecked}
|
||||
/>
|
||||
)}
|
||||
{this.props.isFixed && (
|
||||
<p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>
|
||||
)}
|
||||
{this.props.isFixed && <p>{t(Keys.controls.settings.throughputInput.fixedStorageNote)}</p>}
|
||||
{this.props.collectionName && (
|
||||
<Stack.Item style={{ marginTop: "40px" }}>{this.getStorageCapacityTitle()}</Stack.Item>
|
||||
)}
|
||||
|
||||
@@ -561,9 +561,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
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.
|
||||
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.
|
||||
<StyledLinkBase
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
|
||||
target="_blank"
|
||||
@@ -581,9 +579,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
Based on usage, your
|
||||
container
|
||||
throughput will scale from
|
||||
Based on usage, your container throughput will scale from
|
||||
|
||||
<b>
|
||||
400
|
||||
@@ -692,7 +688,8 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
$
|
||||
|
||||
35.04
|
||||
min
|
||||
|
||||
min
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
@@ -705,7 +702,8 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
$
|
||||
|
||||
350.40
|
||||
max
|
||||
|
||||
max
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
@@ -739,7 +737,8 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
$
|
||||
|
||||
35.04
|
||||
min
|
||||
|
||||
min
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
@@ -752,7 +751,8 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
$
|
||||
|
||||
350.40
|
||||
max
|
||||
|
||||
max
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -1169,9 +1169,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
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.
|
||||
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.
|
||||
<StyledLinkBase
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
|
||||
target="_blank"
|
||||
@@ -1755,9 +1753,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
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.
|
||||
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.
|
||||
<StyledLinkBase
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
|
||||
target="_blank"
|
||||
|
||||
@@ -27,7 +27,8 @@ exports[`ComputedPropertiesComponent renders 1`] = `
|
||||
iconName="NavigateExternalInline"
|
||||
/>
|
||||
</StyledLinkBase>
|
||||
about how to define computed properties and how to use them.
|
||||
|
||||
about how to define computed properties and how to use them.
|
||||
</Text>
|
||||
<div
|
||||
className="settingsV2Editor"
|
||||
|
||||
@@ -33,8 +33,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
|
||||
}
|
||||
}
|
||||
>
|
||||
Change
|
||||
partition key
|
||||
Change partition key
|
||||
</Text>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
@@ -61,8 +60,7 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
|
||||
}
|
||||
}
|
||||
>
|
||||
Current
|
||||
partition key
|
||||
Current partition key
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
@@ -223,8 +221,7 @@ exports[`PartitionKeyComponent renders read-only component and matches snapshot
|
||||
}
|
||||
}
|
||||
>
|
||||
Current
|
||||
partition key
|
||||
Current partition key
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
|
||||
@@ -410,9 +410,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
<Text
|
||||
className="hintText-115"
|
||||
>
|
||||
Large
|
||||
partition key
|
||||
has been enabled.
|
||||
Large partition key has been enabled.
|
||||
</Text>
|
||||
<Text
|
||||
className="hintText-115"
|
||||
@@ -984,9 +982,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
<Text
|
||||
className="hintText-115"
|
||||
>
|
||||
Large
|
||||
partition key
|
||||
has been enabled.
|
||||
Large partition key has been enabled.
|
||||
</Text>
|
||||
<Text
|
||||
className="hintText-115"
|
||||
@@ -1522,9 +1518,7 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
|
||||
<Text
|
||||
className="hintText-115"
|
||||
>
|
||||
Large
|
||||
partition key
|
||||
has been enabled.
|
||||
Large partition key has been enabled.
|
||||
</Text>
|
||||
<Text
|
||||
className="hintText-115"
|
||||
@@ -2157,9 +2151,7 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
<Text
|
||||
className="hintText-115"
|
||||
>
|
||||
Large
|
||||
partition key
|
||||
has been enabled.
|
||||
Large partition key has been enabled.
|
||||
</Text>
|
||||
<Text
|
||||
className="hintText-115"
|
||||
@@ -2729,9 +2721,7 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
<Text
|
||||
className="hintText-115"
|
||||
>
|
||||
Large
|
||||
partition key
|
||||
has been enabled.
|
||||
Large partition key has been enabled.
|
||||
</Text>
|
||||
<Text
|
||||
className="hintText-115"
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { isCapabilityEnabled } from "../../../Utils/CapabilityUtils";
|
||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||
|
||||
const zeroValue = 0;
|
||||
@@ -88,6 +92,19 @@ export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection):
|
||||
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 => {
|
||||
// Backend can contain different casing as it does case-insensitive comparisson
|
||||
if (!modeFromBackend) {
|
||||
@@ -160,25 +177,27 @@ const getStringValue = (value: isDirtyTypes, type: string): string => {
|
||||
export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
||||
switch (tab) {
|
||||
case SettingsV2TabTypes.ScaleTab:
|
||||
return "Scale";
|
||||
return t(Keys.controls.settings.tabTitles.scale);
|
||||
case SettingsV2TabTypes.ConflictResolutionTab:
|
||||
return "Conflict Resolution";
|
||||
return t(Keys.controls.settings.tabTitles.conflictResolution);
|
||||
case SettingsV2TabTypes.SubSettingsTab:
|
||||
return "Settings";
|
||||
return t(Keys.controls.settings.tabTitles.settings);
|
||||
case SettingsV2TabTypes.IndexingPolicyTab:
|
||||
return "Indexing Policy";
|
||||
return t(Keys.controls.settings.tabTitles.indexingPolicy);
|
||||
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:
|
||||
return "Computed Properties";
|
||||
return t(Keys.controls.settings.tabTitles.computedProperties);
|
||||
case SettingsV2TabTypes.ContainerVectorPolicyTab:
|
||||
return "Container Policies";
|
||||
return t(Keys.controls.settings.tabTitles.containerPolicies);
|
||||
case SettingsV2TabTypes.ThroughputBucketsTab:
|
||||
return "Throughput Buckets";
|
||||
return t(Keys.controls.settings.tabTitles.throughputBuckets);
|
||||
case SettingsV2TabTypes.GlobalSecondaryIndexTab:
|
||||
return "Global Secondary Index (Preview)";
|
||||
return t(Keys.controls.settings.tabTitles.globalSecondaryIndexPreview);
|
||||
case SettingsV2TabTypes.DataMaskingTab:
|
||||
return "Masking Policy (preview)";
|
||||
return t(Keys.controls.settings.tabTitles.maskingPolicyPreview);
|
||||
default:
|
||||
throw new Error(`Unknown tab ${tab}`);
|
||||
}
|
||||
@@ -188,19 +207,19 @@ export const getMongoNotification = (description: string, type: MongoIndexTypes)
|
||||
if (description && !type) {
|
||||
return {
|
||||
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)) {
|
||||
return {
|
||||
type: MongoNotificationType.Error,
|
||||
message: "Please enter a field name.",
|
||||
message: t(Keys.controls.settings.mongoNotifications.enterFieldNameError),
|
||||
};
|
||||
} else if (type === MongoIndexTypes.Wildcard && description?.indexOf("$**") === -1) {
|
||||
return {
|
||||
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 +253,29 @@ export const isIndexTransforming = (indexTransformationProgress: number): boolea
|
||||
indexTransformationProgress !== undefined && indexTransformationProgress !== 100;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
export const getPartitionKeyTooltipText = (apiType: string): string => {
|
||||
if (apiType === "Mongo") {
|
||||
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’s critical to choose a field that will evenly distribute your data.";
|
||||
return t(Keys.controls.settings.partitionKey.shardKeyTooltip);
|
||||
}
|
||||
let tooltipText = `The ${getPartitionKeyName(
|
||||
apiType,
|
||||
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.`;
|
||||
let tooltipText = `The ${getPartitionKeyName(apiType, true)} ${t(
|
||||
Keys.controls.settings.partitionKey.partitionKeyTooltip,
|
||||
)}`;
|
||||
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;
|
||||
};
|
||||
|
||||
export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: string): string => {
|
||||
if (partitionKeyDefault && (apiType === "SQL" || apiType === "Mongo")) {
|
||||
const subtext = "For small workloads, the item ID is a suitable choice for the partition key.";
|
||||
return subtext;
|
||||
return t(Keys.controls.settings.partitionKey.partitionKeySubtext);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
@@ -263,18 +283,18 @@ export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: st
|
||||
export const getPartitionKeyPlaceHolder = (apiType: string, index?: number): string => {
|
||||
switch (apiType) {
|
||||
case "Mongo":
|
||||
return "e.g., categoryId";
|
||||
return t(Keys.controls.settings.partitionKey.mongoPlaceholder);
|
||||
case "Gremlin":
|
||||
return "e.g., /address";
|
||||
return t(Keys.controls.settings.partitionKey.gremlinPlaceholder);
|
||||
case "SQL":
|
||||
return `${
|
||||
index === undefined
|
||||
? "Required - first partition key e.g., /TenantId"
|
||||
? t(Keys.controls.settings.partitionKey.sqlFirstPartitionKey)
|
||||
: index === 0
|
||||
? "second partition key e.g., /UserId"
|
||||
: "third partition key e.g., /SessionId"
|
||||
? t(Keys.controls.settings.partitionKey.sqlSecondPartitionKey)
|
||||
: t(Keys.controls.settings.partitionKey.sqlThirdPartitionKey)
|
||||
}`;
|
||||
default:
|
||||
return "e.g., /address/zipCode";
|
||||
return t(Keys.controls.settings.partitionKey.defaultPlaceholder);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -68,7 +68,6 @@ export const collection = {
|
||||
dataMaskingPolicy: ko.observable<DataModels.DataMaskingPolicy>({
|
||||
includedPaths: [],
|
||||
excludedPaths: ["/excludedPath"],
|
||||
isPolicyEnabled: true,
|
||||
}),
|
||||
readSettings: () => {
|
||||
return;
|
||||
|
||||
@@ -604,6 +604,58 @@ exports[`SettingsComponent renders 1`] = `
|
||||
/>
|
||||
</Stack>
|
||||
</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
|
||||
headerButtonProps={
|
||||
{
|
||||
|
||||
@@ -127,9 +127,13 @@ 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.
|
||||
<br />
|
||||
Database:
|
||||
Database:
|
||||
|
||||
sampleDb
|
||||
, Container:
|
||||
,
|
||||
|
||||
Container:
|
||||
|
||||
sampleCollection
|
||||
|
||||
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
|
||||
@@ -309,7 +313,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
|
||||
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
|
||||
<StyledLinkBase
|
||||
onClick={[Function]}
|
||||
>
|
||||
|
||||
@@ -113,7 +113,7 @@ export class ContainerSampleGenerator {
|
||||
? await createMongoDocument(collection.databaseId, collection, shardKey, doc)
|
||||
: await createDocument(collection, doc);
|
||||
} catch (error) {
|
||||
NotificationConsoleUtils.logConsoleError(error);
|
||||
NotificationConsoleUtils.logConsoleError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -329,7 +329,10 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
}
|
||||
|
||||
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 => {
|
||||
|
||||
@@ -42,6 +42,8 @@ import {
|
||||
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { DEFAULT_FABRIC_NATIVE_CONTAINER_THROUGHPUT, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import React from "react";
|
||||
import { CollectionCreation } from "Shared/Constants";
|
||||
@@ -177,7 +179,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
messageType="info"
|
||||
showErrorDetails={false}
|
||||
link={Constants.Urls.freeTierInformation}
|
||||
linkText="Learn more"
|
||||
linkText={t(Keys.common.learnMore)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -274,17 +276,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()}.`}
|
||||
content={t(Keys.panes.addCollection.databaseTooltip, {
|
||||
collectionName: getCollectionName(true).toLocaleLowerCase(),
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()}.`}
|
||||
ariaLabel={t(Keys.panes.addCollection.databaseTooltip, {
|
||||
collectionName: getCollectionName(true).toLocaleLowerCase(),
|
||||
})}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
@@ -304,7 +306,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
tabIndex={0}
|
||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Create new</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.createNew)}</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
@@ -317,7 +319,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
tabIndex={0}
|
||||
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Use existing</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.useExisting)}</span>
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
@@ -347,7 +349,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
{!isServerlessAccount() && (
|
||||
<Stack horizontal>
|
||||
<Checkbox
|
||||
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
|
||||
label={t(Keys.panes.addCollection.shareThroughput, {
|
||||
collectionName: getCollectionName(true).toLocaleLowerCase(),
|
||||
})}
|
||||
checked={this.state.isSharedThroughputChecked}
|
||||
styles={{
|
||||
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
|
||||
@@ -365,17 +369,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
/>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()} within the database.`}
|
||||
content={t(Keys.panes.addCollection.shareThroughputTooltip, {
|
||||
collectionName: getCollectionName(true).toLocaleLowerCase(),
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||
true,
|
||||
).toLocaleLowerCase()} within the database.`}
|
||||
ariaLabel={t(Keys.panes.addCollection.shareThroughputTooltip, {
|
||||
collectionName: getCollectionName(true).toLocaleLowerCase(),
|
||||
})}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
@@ -424,14 +428,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Text>
|
||||
<TooltipHost
|
||||
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
|
||||
role="button"
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
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>
|
||||
</Stack>
|
||||
@@ -445,10 +453,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
autoComplete="off"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder={`e.g., ${getCollectionName()}1`}
|
||||
placeholder={t(Keys.panes.addCollection.collectionIdPlaceholder, { collectionName: getCollectionName() })}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
|
||||
aria-label={t(Keys.panes.addCollection.collectionIdAriaLabel, { collectionName: getCollectionName() })}
|
||||
value={this.state.collectionId}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
this.setState({ collectionId: event.target.value })
|
||||
@@ -462,7 +470,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Indexing
|
||||
{t(Keys.panes.addCollection.indexing)}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -470,32 +478,32 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.enableIndexing}
|
||||
aria-label="Turn on indexing"
|
||||
aria-label={t(Keys.panes.addCollection.turnOnIndexing)}
|
||||
aria-checked={this.state.enableIndexing}
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={this.onTurnOnIndexing.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Automatic</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.automatic)}</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.enableIndexing}
|
||||
aria-label="Turn off indexing"
|
||||
aria-label={t(Keys.panes.addCollection.turnOffIndexing)}
|
||||
aria-checked={!this.state.enableIndexing}
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={this.onTurnOffIndexing.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Off</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.off)}</span>
|
||||
</Stack>
|
||||
|
||||
<Text variant="small">
|
||||
{this.getFreeTierIndexingText()}{" "}
|
||||
<Link target="_blank" href="https://aka.ms/cosmos-indexing-policy">
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -508,21 +516,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Stack horizontal style={{ marginTop: -5, marginBottom: -4 }}>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Sharding
|
||||
{t(Keys.panes.addCollection.sharding)}
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={
|
||||
"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."
|
||||
}
|
||||
content={t(Keys.panes.addCollection.shardingTooltip)}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={
|
||||
"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."
|
||||
}
|
||||
ariaLabel={t(Keys.panes.addCollection.shardingTooltip)}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
@@ -531,7 +535,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.isSharded}
|
||||
aria-label="Unsharded"
|
||||
aria-label={t(Keys.panes.addCollection.unsharded)}
|
||||
aria-checked={!this.state.isSharded}
|
||||
name="unsharded"
|
||||
type="radio"
|
||||
@@ -540,12 +544,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
tabIndex={0}
|
||||
onChange={this.onUnshardedRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Unsharded (20GB limit)</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.unshardedLabel)}</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.isSharded}
|
||||
aria-label="Sharded"
|
||||
aria-label={t(Keys.panes.addCollection.sharded)}
|
||||
aria-checked={this.state.isSharded}
|
||||
name="sharded"
|
||||
type="radio"
|
||||
@@ -554,7 +558,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
tabIndex={0}
|
||||
onChange={this.onShardedRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Sharded</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.sharded)}</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
@@ -679,15 +683,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
disabled={this.state.subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
||||
onClick={() => this.setState({ subPartitionKeys: [...this.state.subPartitionKeys, ""] })}
|
||||
>
|
||||
Add hierarchical partition key
|
||||
{t(Keys.panes.addCollection.addPartitionKey)}
|
||||
</DefaultButton>
|
||||
{this.state.subPartitionKeys.length > 0 && (
|
||||
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
|
||||
partition your data with up to three levels of keys for better data distribution. Requires .NET
|
||||
V3, Java V4 SDK, or preview JavaScript V3 SDK.{" "}
|
||||
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} />{" "}
|
||||
{t(Keys.panes.addCollection.hierarchicalPartitionKeyInfo)}{" "}
|
||||
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
@@ -700,7 +703,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
{!isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && (
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Checkbox
|
||||
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
|
||||
label={t(Keys.panes.addCollection.provisionDedicatedThroughput, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
})}
|
||||
checked={this.state.enableDedicatedThroughput}
|
||||
styles={{
|
||||
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
|
||||
@@ -718,23 +723,19 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
/>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
|
||||
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
|
||||
true,
|
||||
).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.`}
|
||||
content={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
|
||||
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
|
||||
true,
|
||||
).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.`}
|
||||
ariaLabel={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
|
||||
})}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
@@ -769,8 +770,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
autoComplete="off"
|
||||
placeholder={
|
||||
userContext.apiType === "Mongo"
|
||||
? "Comma separated paths e.g. firstName,address.zipCode"
|
||||
: "Comma separated paths e.g. /firstName,/address/zipCode"
|
||||
? t(Keys.panes.addCollection.uniqueKeysPlaceholderMongo)
|
||||
: t(Keys.panes.addCollection.uniqueKeysPlaceholderSql)
|
||||
}
|
||||
className="panelTextField"
|
||||
value={uniqueKey}
|
||||
@@ -802,7 +803,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
styles={{ root: { padding: 0 }, label: { fontSize: 12, color: "var(--colorNeutralForeground1)" } }}
|
||||
onClick={() => this.setState({ uniqueKeys: [...this.state.uniqueKeys, ""] })}
|
||||
>
|
||||
Add unique key
|
||||
{t(Keys.panes.addCollection.addUniqueKey)}
|
||||
</ActionButton>
|
||||
</Stack>
|
||||
)}
|
||||
@@ -823,7 +824,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.enableAnalyticalStore}
|
||||
disabled={!isSynapseLinkEnabled()}
|
||||
aria-label="Enable analytical store"
|
||||
aria-label={t(Keys.panes.addCollection.enableAnalyticalStore)}
|
||||
aria-checked={this.state.enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
type="radio"
|
||||
@@ -832,13 +833,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
tabIndex={0}
|
||||
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">On</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.on)}</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.enableAnalyticalStore}
|
||||
disabled={!isSynapseLinkEnabled()}
|
||||
aria-label="Disable analytical store"
|
||||
aria-label={t(Keys.panes.addCollection.disableAnalyticalStore)}
|
||||
aria-checked={!this.state.enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
type="radio"
|
||||
@@ -847,26 +848,28 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
tabIndex={0}
|
||||
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Off</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.off)}</span>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
{!isSynapseLinkEnabled() && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
Azure Synapse Link is required for creating an analytical store{" "}
|
||||
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
|
||||
{t(Keys.panes.addCollection.analyticalStoreSynapseLinkRequired, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
})}{" "}
|
||||
<br />
|
||||
<Link
|
||||
href="https://aka.ms/cosmosdb-synapselink"
|
||||
target="_blank"
|
||||
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
|
||||
className="capacitycalculator-link"
|
||||
>
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
<DefaultButton
|
||||
text="Enable"
|
||||
text={t(Keys.panes.addCollection.enable)}
|
||||
onClick={() => this.props.explorer.openEnableSynapseLinkDialog()}
|
||||
style={{ height: 27, width: 80 }}
|
||||
styles={{ label: { fontSize: 12 } }}
|
||||
@@ -878,7 +881,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
{this.shouldShowVectorSearchParameters() && (
|
||||
<Stack>
|
||||
<CollapsibleSectionComponent
|
||||
title="Container Vector Policy"
|
||||
title={t(Keys.panes.addCollection.containerVectorPolicy)}
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
scrollToSection("collapsibleVectorPolicySectionContent");
|
||||
@@ -906,7 +909,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
{this.shouldShowFullTextSearchParameters() && (
|
||||
<Stack>
|
||||
<CollapsibleSectionComponent
|
||||
title="Container Full Text Search Policy"
|
||||
title={t(Keys.panes.addCollection.containerFullTextSearchPolicy)}
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
scrollToSection("collapsibleFullTextPolicySectionContent");
|
||||
@@ -935,7 +938,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
)}
|
||||
{!isFabricNative() && userContext.apiType !== "Tables" && (
|
||||
<CollapsibleSectionComponent
|
||||
title="Advanced"
|
||||
title={t(Keys.panes.addCollection.advanced)}
|
||||
isExpandedByDefault={false}
|
||||
onExpand={() => {
|
||||
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
|
||||
@@ -948,23 +951,23 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Indexing
|
||||
{t(Keys.panes.addCollection.indexing)}
|
||||
</Text>
|
||||
<TooltipHost
|
||||
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
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
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>
|
||||
</Stack>
|
||||
|
||||
<Checkbox
|
||||
label="Create a Wildcard Index on all fields"
|
||||
label={t(Keys.panes.addCollection.createWildcardIndex)}
|
||||
checked={this.state.createMongoWildCardIndex}
|
||||
styles={{
|
||||
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
|
||||
@@ -986,7 +989,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
{userContext.apiType === "SQL" && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<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}
|
||||
styles={{
|
||||
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
|
||||
@@ -1003,11 +1006,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
/>
|
||||
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
<Icon iconName="InfoSolid" className="removeIcon" /> 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.{" "}
|
||||
<Icon iconName="InfoSolid" className="removeIcon" /> {t(Keys.panes.addCollection.legacySdkInfo)}{" "}
|
||||
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -1018,7 +1019,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</div>
|
||||
|
||||
{!this.props.isCopyJobFlow && (
|
||||
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={this.state.isThroughputCapExceeded} />
|
||||
<PanelFooterComponent buttonLabel={t(Keys.common.ok)} isButtonDisabled={this.state.isThroughputCapExceeded} />
|
||||
)}
|
||||
|
||||
{this.state.isExecuting && (
|
||||
@@ -1044,7 +1045,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
progressTrack: { backgroundColor: "#A6A6A6" },
|
||||
progressBar: { background: "white" },
|
||||
}}
|
||||
label="Adding sample data set"
|
||||
label={t(Keys.panes.addCollection.addingSampleDataSet)}
|
||||
/>
|
||||
</TeachingBubble>
|
||||
)}
|
||||
@@ -1150,8 +1151,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
private getFreeTierIndexingText(): string {
|
||||
return this.state.enableIndexing
|
||||
? "All properties in your documents will be indexed by default for flexible and efficient queries."
|
||||
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.";
|
||||
? t(Keys.panes.addCollection.indexingOnInfo)
|
||||
: t(Keys.panes.addCollection.indexingOffInfo);
|
||||
}
|
||||
|
||||
private getPartitionKeySubtext(): string {
|
||||
@@ -1249,14 +1250,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
const throughput = this.state.createNewDatabase ? this.newDatabaseThroughput : this.collectionThroughput;
|
||||
if (throughput > CollectionCreation.DefaultCollectionRUs100K && !this.isCostAcknowledged) {
|
||||
const errorMessage = this.isNewDatabaseAutoscale
|
||||
? "Please acknowledge the estimated monthly spend."
|
||||
: "Please acknowledge the estimated daily spend.";
|
||||
? t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly)
|
||||
: t(Keys.panes.addCollection.acknowledgeSpendErrorDaily);
|
||||
this.setState({ errorMessage });
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1270,12 +1271,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
if (this.shouldShowVectorSearchParameters()) {
|
||||
if (!this.state.vectorPolicyValidated) {
|
||||
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
||||
this.setState({ errorMessage: t(Keys.panes.addCollection.vectorPolicyError) });
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,28 +3,33 @@ import * as Constants from "Common/Constants";
|
||||
import { configContext, Platform } from "ConfigContext";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/FullTextPoliciesComponent";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
|
||||
export function getPartitionKeyTooltipText(): string {
|
||||
if (userContext.apiType === "Mongo") {
|
||||
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’s critical to choose a field that will evenly distribute your data.";
|
||||
return t(Keys.panes.addCollectionUtility.shardKeyTooltip);
|
||||
}
|
||||
|
||||
let tooltipText = `The ${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.`;
|
||||
let tooltipText = t(Keys.panes.addCollectionUtility.partitionKeyTooltip, {
|
||||
partitionKeyName: getPartitionKeyName(true),
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -32,19 +37,19 @@ export function getPartitionKeyName(isLowerCase?: boolean): string {
|
||||
export function getPartitionKeyPlaceHolder(index?: number): string {
|
||||
switch (userContext.apiType) {
|
||||
case "Mongo":
|
||||
return "e.g., categoryId";
|
||||
return t(Keys.panes.addCollectionUtility.shardKeyPlaceholder);
|
||||
case "Gremlin":
|
||||
return "e.g., /address";
|
||||
return t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderDefault);
|
||||
case "SQL":
|
||||
return `${
|
||||
index === undefined
|
||||
? "Required - first partition key e.g., /TenantId"
|
||||
? t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderFirst)
|
||||
: index === 0
|
||||
? "second partition key e.g., /UserId"
|
||||
: "third partition key e.g., /SessionId"
|
||||
? t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderSecond)
|
||||
: t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderThird)
|
||||
}`;
|
||||
default:
|
||||
return "e.g., /address/zipCode";
|
||||
return t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderGraph);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,13 +74,12 @@ export function isFreeTierAccount(): boolean {
|
||||
}
|
||||
|
||||
export function UniqueKeysHeader(): JSX.Element {
|
||||
const tooltipContent =
|
||||
"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.";
|
||||
const tooltipContent = t(Keys.panes.addCollectionUtility.uniqueKeysTooltip);
|
||||
|
||||
return (
|
||||
<Stack horizontal style={{ marginBottom: -2 }}>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Unique keys
|
||||
{t(Keys.panes.addCollectionUtility.uniqueKeysLabel)}
|
||||
</Text>
|
||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
|
||||
@@ -99,12 +103,11 @@ export function shouldShowAnalyticalStoreOptions(): boolean {
|
||||
}
|
||||
|
||||
export function AnalyticalStoreHeader(): JSX.Element {
|
||||
const tooltipContent =
|
||||
"Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.";
|
||||
const tooltipContent = t(Keys.panes.addCollectionUtility.analyticalStoreTooltip);
|
||||
return (
|
||||
<Stack horizontal style={{ marginBottom: -2 }}>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Analytical Store
|
||||
{t(Keys.panes.addCollectionUtility.analyticalStoreLabel)}
|
||||
</Text>
|
||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
|
||||
@@ -116,14 +119,13 @@ export function AnalyticalStoreHeader(): JSX.Element {
|
||||
export function AnalyticalStorageContent(): JSX.Element {
|
||||
return (
|
||||
<Text variant="small">
|
||||
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting
|
||||
the performance of transactional workloads.{" "}
|
||||
{t(Keys.panes.addCollectionUtility.analyticalStoreDescription)}{" "}
|
||||
<Link
|
||||
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
|
||||
target="_blank"
|
||||
href="https://aka.ms/analytical-store-overview"
|
||||
>
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
@@ -155,10 +157,9 @@ export function scrollToSection(id: string): void {
|
||||
export function ContainerVectorPolicyTooltipContent(): JSX.Element {
|
||||
return (
|
||||
<Text variant="small">
|
||||
Describe any properties in your data that contain vectors, so that they can be made available for similarity
|
||||
queries.{" "}
|
||||
{t(Keys.panes.addCollectionUtility.vectorPolicyTooltip)}{" "}
|
||||
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
|
||||
@@ -482,10 +482,8 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
}
|
||||
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 />
|
||||
<StyledLinkBase
|
||||
aria-label="Learn more about Azure Synapse Link."
|
||||
@@ -608,7 +606,8 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
className="removeIcon"
|
||||
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
|
||||
href="https://aka.ms/cosmos-large-pk"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { getNewDatabaseSharedThroughputDefault } from "Common/DatabaseUtility";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
@@ -40,15 +42,18 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
|
||||
const databaseLabel: string = isCassandraAccount ? "keyspace" : "database";
|
||||
const collectionsLabel: string = isCassandraAccount ? "tables" : "collections";
|
||||
const databaseIdLabel: string = isCassandraAccount ? "Keyspace id" : "Database id";
|
||||
const databaseIdPlaceHolder: string = isCassandraAccount ? "Type a new keyspace id" : "Type a new database id";
|
||||
const databaseIdLabel: string = isCassandraAccount
|
||||
? 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 databaseIdTooltipText = `A ${
|
||||
isCassandraAccount ? "keyspace" : "database"
|
||||
} is a logical container of one or more ${isCassandraAccount ? "tables" : "collections"}`;
|
||||
const databaseIdTooltipText = t(Keys.panes.addDatabase.databaseTooltip, { databaseLabel, collectionsLabel });
|
||||
|
||||
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>(
|
||||
getNewDatabaseSharedThroughputDefault(),
|
||||
);
|
||||
@@ -144,15 +149,15 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
// TODO add feature flag that disables validation for customers with custom accounts
|
||||
if (isAutoscaleSelected) {
|
||||
if (!AutoPilotUtils.isValidAutoPilotThroughput(throughput)) {
|
||||
setFormErrors(
|
||||
`Please enter a value greater than ${AutoPilotUtils.autoPilotThroughput1K} for autopilot throughput`,
|
||||
);
|
||||
setFormErrors(t(Keys.panes.addDatabase.greaterThanError, { minValue: AutoPilotUtils.autoPilotThroughput1K }));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
|
||||
setFormErrors(`Please acknowledge the estimated ${isAutoscaleSelected ? "monthly" : "daily"} spend.`);
|
||||
setFormErrors(
|
||||
t(Keys.panes.addDatabase.acknowledgeSpendError, { period: isAutoscaleSelected ? "monthly" : "daily" }),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -169,7 +174,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
const props: RightPaneFormProps = {
|
||||
formError: formErrors,
|
||||
isExecuting,
|
||||
submitButtonText: "OK",
|
||||
submitButtonText: t(Keys.common.ok),
|
||||
isSubmitButtonDisabled: isThroughputCapExceeded,
|
||||
onSubmit,
|
||||
};
|
||||
@@ -187,7 +192,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
messageType="info"
|
||||
showErrorDetails={false}
|
||||
link={Constants.Urls.freeTierInformation}
|
||||
linkText="Learn more"
|
||||
linkText={t(Keys.common.learnMore)}
|
||||
/>
|
||||
)}
|
||||
<div className="panelMainContent">
|
||||
|
||||
@@ -40,6 +40,8 @@ import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent"
|
||||
import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
import { CollectionCreation } from "Shared/Constants";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
@@ -168,19 +170,19 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
}
|
||||
|
||||
if (globalSecondaryIndexThroughput > CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
|
||||
const errorMessage: string = "Please acknowledge the estimated monthly spend.";
|
||||
const errorMessage: string = t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly);
|
||||
setErrorMessage(errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (showVectorSearchParameters()) {
|
||||
if (!vectorPolicyValidated) {
|
||||
setErrorMessage("Please fix errors in container vector policy");
|
||||
setErrorMessage(t(Keys.panes.addCollection.vectorPolicyError));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fullTextPolicyValidated) {
|
||||
setErrorMessage("Please fix errors in container full text search policy");
|
||||
setErrorMessage(t(Keys.panes.addCollection.fullTextSearchPolicyError));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -307,7 +309,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Global secondary index container id
|
||||
{t(Keys.panes.addGlobalSecondaryIndex.globalSecondaryIndexId)}
|
||||
</Text>
|
||||
</Stack>
|
||||
<input
|
||||
@@ -318,7 +320,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
autoComplete="off"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder={`e.g., indexbyEmailId`}
|
||||
placeholder={t(Keys.panes.addGlobalSecondaryIndex.globalSecondaryIndexIdPlaceholder)}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
value={globalSecondaryIndexId}
|
||||
@@ -336,7 +338,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/materialized-views#defining-materialized-views"
|
||||
target="blank"
|
||||
>
|
||||
Learn more about defining global secondary indexes.
|
||||
{t(Keys.panes.addGlobalSecondaryIndex.projectionQueryTooltip)}
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
@@ -349,7 +351,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
aria-required
|
||||
required
|
||||
autoComplete="off"
|
||||
placeholder={"SELECT c.email, c.accountId FROM c"}
|
||||
placeholder={t(Keys.panes.addGlobalSecondaryIndex.projectionQueryPlaceholder)}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
value={definition || ""}
|
||||
@@ -393,7 +395,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
<AdvancedComponent {...{ useHashV1, setUseHashV1, setSubPartitionKeys }} />
|
||||
</Stack>
|
||||
</div>
|
||||
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={isThroughputCapExceeded} />
|
||||
<PanelFooterComponent buttonLabel={t(Keys.common.ok)} isButtonDisabled={isThroughputCapExceeded} />
|
||||
{isExecuting && <PanelLoadingScreen />}
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -2,14 +2,16 @@ import { Checkbox, Dropdown, IDropdownOption, Link, Stack, Text, TextField } fro
|
||||
import * as Constants from "Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import * as SharedConstants from "Shared/Constants";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { isServerlessAccount } from "Utils/CapabilityUtils";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||
import Explorer from "../../Explorer";
|
||||
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
|
||||
@@ -71,8 +73,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
|
||||
const errorMessage =
|
||||
isNewKeySpaceAutoscale || isTableAutoscale
|
||||
? "Please acknowledge the estimated monthly spend."
|
||||
: "Please acknowledge the estimated daily spend.";
|
||||
? t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly)
|
||||
: t(Keys.panes.addCollection.acknowledgeSpendErrorDaily);
|
||||
setFormError(errorMessage);
|
||||
return;
|
||||
}
|
||||
@@ -149,7 +151,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
const props: RightPaneFormProps = {
|
||||
formError,
|
||||
isExecuting,
|
||||
submitButtonText: "OK",
|
||||
submitButtonText: t(Keys.common.ok),
|
||||
isSubmitButtonDisabled: isThroughputCapExceeded,
|
||||
onSubmit,
|
||||
};
|
||||
@@ -161,7 +163,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<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>
|
||||
</Stack>
|
||||
|
||||
@@ -179,7 +182,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
setExistingKeyspaceId("");
|
||||
}}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Create new</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.createNew)}</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
@@ -193,7 +196,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
setIsKeyspaceShared(false);
|
||||
}}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Use existing</span>
|
||||
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.useExisting)}</span>
|
||||
</Stack>
|
||||
|
||||
{keyspaceCreateNew && (
|
||||
@@ -275,9 +278,9 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<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">
|
||||
Learn More
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
@@ -295,7 +298,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
autoComplete="off"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder="Enter table Id"
|
||||
placeholder={t(Keys.panes.cassandraAddCollection.enterTableId)}
|
||||
size={20}
|
||||
value={tableId}
|
||||
onChange={(e, newValue) => setTableId(newValue)}
|
||||
@@ -307,7 +310,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
multiline
|
||||
id="editor-area"
|
||||
rows={5}
|
||||
ariaLabel="Table schema"
|
||||
ariaLabel={t(Keys.panes.cassandraAddCollection.tableSchemaAriaLabel)}
|
||||
value={userTableQuery}
|
||||
onChange={(e, newValue) => setUserTableQuery(newValue)}
|
||||
/>
|
||||
@@ -318,17 +321,12 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
<input
|
||||
type="checkbox"
|
||||
id="tableSharedThroughput"
|
||||
title="Provision dedicated throughput for this table"
|
||||
title={t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughput)}
|
||||
checked={dedicateTableThroughput}
|
||||
onChange={(e) => setDedicateTableThroughput(e.target.checked)}
|
||||
/>
|
||||
<span>Provision dedicated throughput for this table</span>
|
||||
<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>
|
||||
<span>{t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughput)}</span>
|
||||
<InfoTooltip>{t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughputTooltip)}</InfoTooltip>
|
||||
</Stack>
|
||||
)}
|
||||
{!isServerlessAccount() && (!isKeyspaceShared || dedicateTableThroughput) && (
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { RightPaneForm } from "Explorer/Panes/RightPaneForm/RightPaneForm";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { userContext } from "UserContext";
|
||||
import { getCollectionName } from "Utils/APITypeUtils";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
@@ -72,7 +74,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
await createDataTransferJob();
|
||||
await onClose();
|
||||
} catch (error) {
|
||||
handleError(error, "ChangePartitionKey", "Failed to start data transfer job");
|
||||
handleError(error, "ChangePartitionKey", t(Keys.panes.changePartitionKey.failedToStartError));
|
||||
}
|
||||
setIsExecuting(false);
|
||||
useSidePanel.getState().closeSidePanel();
|
||||
@@ -133,17 +135,21 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
};
|
||||
|
||||
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">
|
||||
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
When changing a container’s partition key, you will need to create a destination container with the correct
|
||||
partition key. You may also select an existing destination container.
|
||||
{t(Keys.panes.changePartitionKey.description)}
|
||||
<Link
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy#container-copy-within-an-azure-cosmos-db-account"
|
||||
target="_blank"
|
||||
underline
|
||||
>
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
<Stack>
|
||||
@@ -218,14 +224,18 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
</Text>
|
||||
<TooltipHost
|
||||
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
|
||||
role="button"
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
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>
|
||||
</Stack>
|
||||
@@ -239,10 +249,14 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
autoComplete="off"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder={`e.g., ${getCollectionName()}1`}
|
||||
placeholder={t(Keys.panes.changePartitionKey.collectionIdPlaceholder, {
|
||||
collectionName: getCollectionName(),
|
||||
})}
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
|
||||
aria-label={t(Keys.panes.changePartitionKey.collectionIdAriaLabel, {
|
||||
collectionName: getCollectionName(),
|
||||
})}
|
||||
value={targetCollectionId}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setTargetCollectionId(event.target.value)}
|
||||
/>
|
||||
@@ -349,7 +363,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
disabled={subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
||||
onClick={() => setSubPartitionKeys([...subPartitionKeys, ""])}
|
||||
>
|
||||
Add hierarchical partition key
|
||||
{t(Keys.panes.addCollection.addPartitionKey)}
|
||||
</DefaultButton>
|
||||
{subPartitionKeys.length > 0 && (
|
||||
<Text
|
||||
@@ -357,11 +371,10 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
variant="small"
|
||||
style={{ color: "var(--colorNeutralForeground1)" }}
|
||||
>
|
||||
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
|
||||
partition your data with up to three levels of keys for better data distribution. Requires .NET V3,
|
||||
Java V4 SDK, or preview JavaScript V3 SDK.{" "}
|
||||
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} />{" "}
|
||||
{t(Keys.panes.addCollection.hierarchicalPartitionKeyInfo)}{" "}
|
||||
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
|
||||
Learn more
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
@@ -377,14 +390,18 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
</Text>
|
||||
<TooltipHost
|
||||
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
|
||||
role="button"
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
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>
|
||||
</Stack>
|
||||
@@ -400,7 +417,7 @@ export const ChangePartitionKeyPane: React.FC<ChangePartitionKeyPaneProps> = ({
|
||||
}}
|
||||
defaultSelectedKey={targetCollectionId}
|
||||
responsiveMode={999}
|
||||
ariaLabel="Existing Containers"
|
||||
ariaLabel={t(Keys.panes.changePartitionKey.existingContainers)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { HttpStatusCodes, PoolIdType } from "../../../Common/Constants";
|
||||
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
|
||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
||||
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
@@ -82,14 +84,14 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
||||
|
||||
const notebookContentItem = await copyNotebook(selectedLocation);
|
||||
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}`);
|
||||
closeSidePanel();
|
||||
} catch (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);
|
||||
} finally {
|
||||
clearMessage && clearMessage();
|
||||
@@ -136,7 +138,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
||||
const props: RightPaneFormProps = {
|
||||
formError,
|
||||
isExecuting: isExecuting,
|
||||
submitButtonText: "OK",
|
||||
submitButtonText: t(Keys.common.ok),
|
||||
onSubmit: () => submit(),
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
import { GitHubReposTitle } from "Explorer/Tree/ResourceTree";
|
||||
import React, { FormEvent, FunctionComponent } from "react";
|
||||
import { IPinnedRepo } from "../../../Juno/JunoClient";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
|
||||
@@ -96,8 +98,8 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
|
||||
return options;
|
||||
};
|
||||
const dropDownProps: IDropdownProps = {
|
||||
label: "Location",
|
||||
ariaLabel: "Location",
|
||||
label: t(Keys.panes.copyNotebook.location),
|
||||
ariaLabel: t(Keys.panes.copyNotebook.locationAriaLabel),
|
||||
placeholder: "Select an option",
|
||||
onRenderTitle: onRenderDropDownTitle,
|
||||
onRenderOption: onRenderDropDownOption,
|
||||
@@ -109,7 +111,7 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
|
||||
<div className="paneMainContent">
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack.Item>
|
||||
<Label htmlFor="notebookName">Name</Label>
|
||||
<Label htmlFor="notebookName">{t(Keys.panes.copyNotebook.name)}</Label>
|
||||
<Text id="notebookName">{name}</Text>
|
||||
</Stack.Item>
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import DeleteFeedback from "Common/DeleteFeedback";
|
||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||
import { deleteCollection } from "Common/dataAccess/deleteCollection";
|
||||
import { Collection } from "Contracts/ViewModels";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
|
||||
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||
@@ -34,12 +36,15 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
useDatabases.getState().isLastCollection() && !useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
|
||||
|
||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||
const paneTitle = "Delete " + collectionName;
|
||||
const paneTitle = t(Keys.panes.deleteCollection.panelTitle, { collectionName });
|
||||
|
||||
const onSubmit = async (): Promise<void> => {
|
||||
const collection = useSelectedNode.getState().findSelectedCollection();
|
||||
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);
|
||||
NotificationConsoleUtils.logConsoleError(
|
||||
`Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}`,
|
||||
@@ -106,18 +111,23 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
const props: RightPaneFormProps = {
|
||||
formError: formError,
|
||||
isExecuting,
|
||||
submitButtonText: "OK",
|
||||
submitButtonText: t(Keys.common.ok),
|
||||
onSubmit,
|
||||
};
|
||||
const confirmContainer = `Confirm by typing the ${collectionName.toLowerCase()} id`;
|
||||
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${collectionName}?`;
|
||||
const confirmContainer = t(Keys.panes.deleteCollection.confirmPrompt, {
|
||||
collectionName: collectionName.toLowerCase(),
|
||||
});
|
||||
const reasonInfo =
|
||||
t(Keys.panes.deleteCollection.feedbackTitle) +
|
||||
" " +
|
||||
t(Keys.panes.deleteCollection.feedbackReason, { collectionName });
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelFormWrapper">
|
||||
<div className="panelMainContent">
|
||||
<div className="confirmDeleteInput">
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text variant="small">Confirm by typing the {collectionName.toLowerCase()} id</Text>
|
||||
<Text variant="small">{confirmContainer}</Text>
|
||||
<TextField
|
||||
id="confirmCollectionId"
|
||||
autoFocus
|
||||
@@ -133,10 +143,10 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
{shouldRecordFeedback() && (
|
||||
<div className="deleteCollectionFeedback">
|
||||
<Text variant="small" block>
|
||||
Help us improve Azure Cosmos DB!
|
||||
{t(Keys.panes.deleteCollection.feedbackTitle)}
|
||||
</Text>
|
||||
<Text variant="small" block>
|
||||
What is the reason why you are deleting this {collectionName}?
|
||||
{t(Keys.panes.deleteCollection.feedbackReason, { collectionName })}
|
||||
</Text>
|
||||
<TextField
|
||||
id="deleteCollectionFeedbackInput"
|
||||
|
||||
@@ -34,9 +34,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
<span
|
||||
className="css-109"
|
||||
>
|
||||
Confirm by typing the
|
||||
container
|
||||
id
|
||||
Confirm by typing the container id
|
||||
</span>
|
||||
</Text>
|
||||
<StyledTextFieldBase
|
||||
|
||||
@@ -5,6 +5,8 @@ import DeleteFeedback from "Common/DeleteFeedback";
|
||||
import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
|
||||
import { deleteDatabase } from "Common/dataAccess/deleteDatabase";
|
||||
import { Collection, Database } from "Contracts/ViewModels";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { DefaultExperienceUtility } from "Shared/DefaultExperienceUtility";
|
||||
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||
@@ -38,11 +40,19 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
const submit = async (): Promise<void> => {
|
||||
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
|
||||
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(
|
||||
`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;
|
||||
}
|
||||
@@ -114,18 +124,20 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
const props: RightPaneFormProps = {
|
||||
formError,
|
||||
isExecuting: isLoading,
|
||||
submitButtonText: "OK",
|
||||
submitButtonText: t(Keys.common.ok),
|
||||
onSubmit: () => submit(),
|
||||
};
|
||||
|
||||
const errorProps: PanelInfoErrorProps = {
|
||||
messageType: "warning",
|
||||
showErrorDetails: false,
|
||||
message:
|
||||
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
||||
message: t(Keys.panes.deleteDatabase.warningMessage),
|
||||
};
|
||||
const confirmDatabase = `Confirm by typing the ${getDatabaseName()} id (name)`;
|
||||
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${getDatabaseName()}?`;
|
||||
const confirmDatabase = t(Keys.panes.deleteDatabase.confirmPrompt, { databaseName: getDatabaseName() });
|
||||
const reasonInfo =
|
||||
t(Keys.panes.deleteDatabase.feedbackTitle) +
|
||||
" " +
|
||||
t(Keys.panes.deleteDatabase.feedbackReason, { databaseName: getDatabaseName() });
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
{!formError && <PanelInfoErrorComponent {...errorProps} />}
|
||||
@@ -148,10 +160,10 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
{isLastNonEmptyDatabase() && (
|
||||
<div className="deleteDatabaseFeedback">
|
||||
<Text variant="small" block>
|
||||
Help us improve Azure Cosmos DB!
|
||||
{t(Keys.panes.deleteDatabase.feedbackTitle)}
|
||||
</Text>
|
||||
<Text variant="small" block>
|
||||
What is the reason why you are deleting this {getDatabaseName()}?
|
||||
{t(Keys.panes.deleteDatabase.feedbackReason, { databaseName: getDatabaseName() })}
|
||||
</Text>
|
||||
<TextField
|
||||
id="deleteDatabaseFeedbackInput"
|
||||
|
||||
@@ -3,6 +3,8 @@ import { useBoolean } from "@fluentui/react-hooks";
|
||||
import React, { FunctionComponent, useRef, useState } from "react";
|
||||
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||
import StoredProcedure from "../../Tree/StoredProcedure";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
@@ -45,8 +47,8 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
||||
};
|
||||
|
||||
const setInvalidParamError = (invalidParam: string): void => {
|
||||
setFormError(`Invalid param specified: ${invalidParam}`);
|
||||
logConsoleError(`Invalid param specified: ${invalidParam} is not a valid literal value`);
|
||||
setFormError(t(Keys.panes.executeStoredProcedure.invalidParamError, { invalidParam }));
|
||||
logConsoleError(t(Keys.panes.executeStoredProcedure.invalidParamConsoleError, { invalidParam }));
|
||||
};
|
||||
|
||||
const submit = (): void => {
|
||||
@@ -96,7 +98,7 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
||||
const props: RightPaneFormProps = {
|
||||
formError: formError,
|
||||
isExecuting: isLoading,
|
||||
submitButtonText: "Execute",
|
||||
submitButtonText: t(Keys.common.execute),
|
||||
onSubmit: () => submit(),
|
||||
};
|
||||
|
||||
@@ -107,9 +109,9 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
||||
inputParameters.push(
|
||||
<InputParameter
|
||||
key={paramKeyValue.text + i}
|
||||
dropdownLabel={i === 0 ? "Key" : ""}
|
||||
inputParameterTitle={i === 0 ? "Enter input parameters (if any)" : ""}
|
||||
inputLabel={i === 0 ? "Param" : ""}
|
||||
dropdownLabel={i === 0 ? t(Keys.panes.executeStoredProcedure.key) : ""}
|
||||
inputParameterTitle={i === 0 ? t(Keys.panes.executeStoredProcedure.enterInputParameters) : ""}
|
||||
inputLabel={i === 0 ? t(Keys.panes.executeStoredProcedure.param) : ""}
|
||||
isAddRemoveVisible={true}
|
||||
onDeleteParamKeyPress={() => deleteParamAtIndex(i)}
|
||||
onAddNewParamKeyPress={() => addNewParamAtIndex(i + 1)}
|
||||
@@ -130,9 +132,9 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelMainContent">
|
||||
<InputParameter
|
||||
dropdownLabel="Key"
|
||||
inputParameterTitle="Partition key value"
|
||||
inputLabel="Value"
|
||||
dropdownLabel={t(Keys.panes.executeStoredProcedure.key)}
|
||||
inputParameterTitle={t(Keys.panes.executeStoredProcedure.partitionKeyValue)}
|
||||
inputLabel={t(Keys.panes.executeStoredProcedure.value)}
|
||||
isAddRemoveVisible={false}
|
||||
onParamValueChange={(_event, newInput?: string) => (partitionValueRef.current = newInput)}
|
||||
onParamKeyChange={(_event: React.FormEvent<HTMLDivElement>, item: IDropdownOption) =>
|
||||
@@ -143,8 +145,8 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
||||
/>
|
||||
{getInputParameterComponent()}
|
||||
<Stack horizontal onClick={() => addNewParamAtLastIndex()} tabIndex={0}>
|
||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
|
||||
<Text className="addNewParamStyle">Add New Param</Text>
|
||||
<Image {...imageProps} src={AddPropertyIcon} alt={t(Keys.panes.executeStoredProcedure.addParam)} />
|
||||
<Text className="addNewParamStyle">{t(Keys.panes.executeStoredProcedure.addNewParam)}</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
</RightPaneForm>
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
import React, { FunctionComponent } from "react";
|
||||
import AddPropertyIcon from "../../../../images/Add-property.svg";
|
||||
import EntityCancelIcon from "../../../../images/Entity_cancel.svg";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
|
||||
const dropdownStyles: Partial<IDropdownStyles> = { dropdown: { width: 100 } };
|
||||
const options = [
|
||||
@@ -74,7 +76,7 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={EntityCancelIcon}
|
||||
alt="Delete param"
|
||||
alt={t(Keys.panes.executeStoredProcedure.deleteParam)}
|
||||
id="deleteparam"
|
||||
role="button"
|
||||
onClick={onDeleteParamKeyPress}
|
||||
@@ -84,7 +86,7 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={AddPropertyIcon}
|
||||
alt="Add param"
|
||||
alt={t(Keys.panes.executeStoredProcedure.addParam)}
|
||||
id="addparam"
|
||||
role="button"
|
||||
onClick={onAddNewParamKeyPress}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { GraphStyleComponent } from "../../Graph/GraphStyleComponent/GraphStyleComponent";
|
||||
import { IGraphConfig } from "../../Tabs/GraphTab";
|
||||
@@ -17,7 +19,7 @@ export const GraphStylingPanel: FunctionComponent<GraphStylingProps> = ({
|
||||
}: GraphStylingProps): JSX.Element => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
|
||||
const buttonLabel = "Ok";
|
||||
const buttonLabel = t(Keys.common.ok);
|
||||
|
||||
const submit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -5,6 +5,8 @@ import folderIcon from "../../../../images/folder_16x16.svg";
|
||||
import { logError } from "../../../Common/Logger";
|
||||
import { Collection } from "../../../Contracts/ViewModels";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
@@ -33,8 +35,8 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
|
||||
const submit = async (): Promise<void> => {
|
||||
setFormError("");
|
||||
if (!selectedFiles || selectedFiles.length === 0) {
|
||||
setFormError("No file specified");
|
||||
logConsoleError("Could not load query -- No file specified. Please input a file.");
|
||||
setFormError(t(Keys.panes.loadQuery.noFileSpecifiedError));
|
||||
logConsoleError(t(Keys.panes.loadQuery.noFileSpecifiedError));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,7 +50,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
|
||||
setLoadingFalse();
|
||||
} catch (error) {
|
||||
setLoadingFalse();
|
||||
setFormError("Failed to load query");
|
||||
setFormError(t(Keys.panes.loadQuery.failedToLoadQueryError));
|
||||
logConsoleError(`Failed to load query from file ${file.name}: ${error}`);
|
||||
}
|
||||
};
|
||||
@@ -71,7 +73,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
|
||||
};
|
||||
|
||||
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}`);
|
||||
};
|
||||
return reader.readAsText(file);
|
||||
@@ -79,7 +81,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
|
||||
const props: RightPaneFormProps = {
|
||||
formError: formError,
|
||||
isExecuting: isLoading,
|
||||
submitButtonText: "Load",
|
||||
submitButtonText: t(Keys.common.load),
|
||||
onSubmit: () => submit(),
|
||||
};
|
||||
|
||||
@@ -90,7 +92,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
|
||||
<Stack horizontal>
|
||||
<TextField
|
||||
id="confirmCollectionId"
|
||||
label="Select a query document"
|
||||
label={t(Keys.panes.loadQuery.selectFilesToOpen)}
|
||||
value={selectedFileName}
|
||||
autoFocus
|
||||
readOnly
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { NewVertexComponent } from "../../Graph/NewVertexComponent/NewVertexComponent";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
@@ -41,7 +43,7 @@ export const NewVertexPanel: FunctionComponent<INewVertexPanelProps> = ({
|
||||
const props: RightPaneFormProps = {
|
||||
formError: errorMessage,
|
||||
isExecuting: isLoading,
|
||||
submitButtonText: "OK",
|
||||
submitButtonText: t(Keys.common.ok),
|
||||
onSubmit: () => submit(),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Icon, Link, Stack, Text } from "@fluentui/react";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import React from "react";
|
||||
import { useNotificationConsole } from "../../hooks/useNotificationConsole";
|
||||
|
||||
@@ -20,13 +22,17 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
|
||||
}: PanelInfoErrorProps): JSX.Element => {
|
||||
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") {
|
||||
icon = <Icon iconName="StatusErrorFull" className="panelErrorIcon" aria-label="error" />;
|
||||
} else if (messageType === "warning") {
|
||||
icon = <Icon iconName="WarningSolid" className="panelWarningIcon" aria-label="warning" />;
|
||||
} 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 (
|
||||
@@ -43,7 +49,7 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
|
||||
</Text>
|
||||
{showErrorDetails && (
|
||||
<a className="paneErrorLink" role="button" onClick={expandConsole} tabIndex={0} onKeyPress={expandConsole}>
|
||||
More details
|
||||
{t(Keys.panes.panelInfo.moreDetails)}
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -5,6 +5,8 @@ import { getErrorMessage, getErrorStack, handleError } from "../../../Common/Err
|
||||
import { useNotebookSnapshotStore } from "../../../hooks/useNotebookSnapshotStore";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { JunoClient } from "../../../Juno/JunoClient";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
@@ -91,7 +93,7 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
|
||||
let startKey: number;
|
||||
|
||||
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");
|
||||
createFormError(formError, formErrorDetail, "PublishNotebookPaneAdapter/submit");
|
||||
setIsExecuting(false);
|
||||
@@ -143,7 +145,11 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
|
||||
);
|
||||
|
||||
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}`);
|
||||
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", formError);
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Dropdown, IDropdownProps, ITextFieldProps, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { ImmutableNotebook } from "@nteract/commutable";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
import { GalleryCardComponent } from "../../Controls/NotebookGallery/Cards/GalleryCardComponent";
|
||||
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
|
||||
import { SnapshotRequest } from "../../Notebook/NotebookComponent/types";
|
||||
@@ -57,13 +59,11 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
|
||||
const maxImageSizeInMib = 1.5;
|
||||
|
||||
const descriptionPara1 =
|
||||
"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 descriptionPara1 = t(Keys.panes.publishNotebook.publishDescription);
|
||||
|
||||
const descriptionPara2 = `Would you like to publish and share "${FileSystemUtil.stripExtension(
|
||||
notebookName,
|
||||
"ipynb",
|
||||
)}" to the gallery?`;
|
||||
const descriptionPara2 = t(Keys.panes.publishNotebook.publishPrompt, {
|
||||
name: FileSystemUtil.stripExtension(notebookName, "ipynb"),
|
||||
});
|
||||
|
||||
const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url];
|
||||
if (onTakeSnapshot) {
|
||||
@@ -74,9 +74,9 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
}
|
||||
|
||||
const thumbnailSelectorProps: IDropdownProps = {
|
||||
label: "Cover image",
|
||||
label: t(Keys.panes.publishNotebook.coverImage),
|
||||
selectedKey: type,
|
||||
ariaLabel: "Cover image",
|
||||
ariaLabel: t(Keys.panes.publishNotebook.coverImage),
|
||||
options: options.map((value: string) => ({ text: value, key: value })),
|
||||
onChange: async (event, options) => {
|
||||
setImageSrc("");
|
||||
@@ -99,7 +99,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
notebookContentRef,
|
||||
});
|
||||
} else {
|
||||
firstOutputErrorHandler(new Error("Output does not exist for any of the cells."));
|
||||
firstOutputErrorHandler(new Error(t(Keys.panes.publishNotebook.outputDoesNotExist)));
|
||||
}
|
||||
}
|
||||
setType(options.text);
|
||||
@@ -107,8 +107,8 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
};
|
||||
|
||||
const thumbnailUrlProps: ITextFieldProps = {
|
||||
label: "Cover image url",
|
||||
ariaLabel: "Cover image url",
|
||||
label: t(Keys.panes.publishNotebook.coverImageUrl),
|
||||
ariaLabel: t(Keys.panes.publishNotebook.coverImageUrl),
|
||||
required: true,
|
||||
onChange: (event, newValue) => {
|
||||
setImageSrc(newValue);
|
||||
@@ -116,7 +116,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
};
|
||||
|
||||
const firstOutputErrorHandler = (error: Error) => {
|
||||
const formError = "Failed to capture first output";
|
||||
const formError = t(Keys.panes.publishNotebook.failedToCaptureOutput);
|
||||
const formErrorDetail = `${error}`;
|
||||
const area = "PublishNotebookPaneComponent/UseFirstOutput";
|
||||
onError(formError, formErrorDetail, area);
|
||||
@@ -130,7 +130,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
};
|
||||
|
||||
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 area = "PublishNotebookPaneComponent/selectImageFile";
|
||||
onError(formError, formErrorDetail, area);
|
||||
@@ -151,7 +151,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
const file = event.target.files[0];
|
||||
if (file.size / 1024 ** 2 > maxImageSizeInMib) {
|
||||
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 area = "PublishNotebookPaneComponent/selectImageFile";
|
||||
|
||||
@@ -185,8 +185,8 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
|
||||
<Stack.Item>
|
||||
<TextField
|
||||
label="Name"
|
||||
ariaLabel="Name"
|
||||
label={t(Keys.panes.publishNotebook.name)}
|
||||
ariaLabel={t(Keys.panes.publishNotebook.name)}
|
||||
defaultValue={FileSystemUtil.stripExtension(notebookName, "ipynb")}
|
||||
required
|
||||
onChange={(event, newValue) => {
|
||||
@@ -198,8 +198,8 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
|
||||
<Stack.Item>
|
||||
<TextField
|
||||
label="Description"
|
||||
ariaLabel="Description"
|
||||
label={t(Keys.panes.publishNotebook.description)}
|
||||
ariaLabel={t(Keys.panes.publishNotebook.description)}
|
||||
multiline
|
||||
rows={3}
|
||||
required
|
||||
@@ -211,9 +211,9 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
|
||||
<Stack.Item>
|
||||
<TextField
|
||||
label="Tags"
|
||||
ariaLabel="Tags"
|
||||
placeholder="Optional tag 1, Optional tag 2"
|
||||
label={t(Keys.panes.publishNotebook.tags)}
|
||||
ariaLabel={t(Keys.panes.publishNotebook.tags)}
|
||||
placeholder={t(Keys.panes.publishNotebook.tagsPlaceholder)}
|
||||
onChange={(event, newValue) => {
|
||||
setNotebookTags(newValue);
|
||||
}}
|
||||
@@ -227,7 +227,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
|
||||
<Stack.Item>{renderThumbnailSelectors(type)}</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Text>Preview</Text>
|
||||
<Text>{t(Keys.panes.publishNotebook.preview)}</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<GalleryCardComponent
|
||||
|
||||
@@ -4,6 +4,8 @@ import React, { FunctionComponent, useState } from "react";
|
||||
import { Areas, SavedQueries } from "../../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import { Query } from "../../../Contracts/DataModels";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||
@@ -28,27 +30,27 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
|
||||
const [formError, setFormError] = 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 title = "Save Query";
|
||||
const setupSaveQueriesText = t(Keys.panes.saveQuery.setupCostMessage, { databaseName: SavedQueries.DatabaseName });
|
||||
const title = t(Keys.panes.saveQuery.panelTitle);
|
||||
const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled);
|
||||
|
||||
const submit = async (): Promise<void> => {
|
||||
setFormError("");
|
||||
if (!isSaveQueryEnabled()) {
|
||||
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 query: string = queryToSave || queryTab?.iTabAccessor.onSaveClickEvent();
|
||||
|
||||
if (!queryName || queryName.length === 0) {
|
||||
setFormError("No query name specified");
|
||||
logConsoleError("Could not save query -- No query name specified. Please specify a query name.");
|
||||
setFormError(t(Keys.panes.saveQuery.noQueryNameError));
|
||||
logConsoleError(t(Keys.panes.saveQuery.noQueryNameError));
|
||||
return;
|
||||
} else if (!query || query.length === 0) {
|
||||
setFormError("Invalid query content specified");
|
||||
logConsoleError("Could not save query -- Invalid query content specified. Please enter query content.");
|
||||
setFormError(t(Keys.panes.saveQuery.invalidQueryContentError));
|
||||
logConsoleError(t(Keys.panes.saveQuery.invalidQueryContentError));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,8 +82,8 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
|
||||
} catch (error) {
|
||||
setLoadingFalse();
|
||||
const errorMessage = getErrorMessage(error);
|
||||
setFormError("Failed to save query");
|
||||
logConsoleError(`Failed to save query: ${errorMessage}`);
|
||||
setFormError(t(Keys.panes.saveQuery.failedToSaveQueryError, { queryName }));
|
||||
logConsoleError(t(Keys.panes.saveQuery.failedToSaveQueryError, { queryName }) + ": " + errorMessage);
|
||||
traceFailure(
|
||||
Action.SaveQuery,
|
||||
{
|
||||
@@ -126,8 +128,8 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
|
||||
},
|
||||
startKey,
|
||||
);
|
||||
setFormError("Failed to setup a container for saved queries");
|
||||
logConsoleError(`Failed to setup a container for saved queries: ${errorMessage}`);
|
||||
setFormError(t(Keys.panes.saveQuery.failedToSetupContainerError));
|
||||
logConsoleError(t(Keys.panes.saveQuery.failedToSetupContainerError) + ": " + errorMessage);
|
||||
} finally {
|
||||
setLoadingFalse();
|
||||
}
|
||||
@@ -136,7 +138,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
|
||||
const props: RightPaneFormProps = {
|
||||
formError: formError,
|
||||
isExecuting: isLoading,
|
||||
submitButtonText: isSaveQueryEnabled() ? "Save" : "Complete setup",
|
||||
submitButtonText: isSaveQueryEnabled() ? t(Keys.common.save) : t(Keys.panes.saveQuery.completeSetup),
|
||||
onSubmit: () => {
|
||||
isSaveQueryEnabled() ? submit() : setupQueries();
|
||||
},
|
||||
@@ -160,7 +162,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
|
||||
) : (
|
||||
<TextField
|
||||
id="saveQueryInput"
|
||||
label="Name"
|
||||
label={t(Keys.panes.saveQuery.name)}
|
||||
autoFocus
|
||||
styles={{ fieldGroup: { width: 300 } }}
|
||||
onChange={(event, newInput?: string) => {
|
||||
|
||||
@@ -24,6 +24,8 @@ import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import { useDialog } from "Explorer/Controls/Dialog";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import {
|
||||
AppStateComponentNames,
|
||||
@@ -235,7 +237,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
const regionOptions: IDropdownOption[] = [];
|
||||
regionOptions.push({
|
||||
key: userContext?.databaseAccount?.properties?.documentEndpoint,
|
||||
text: `Global (Default)`,
|
||||
text: t(Keys.panes.settings.globalDefault),
|
||||
data: {
|
||||
isGlobal: true,
|
||||
writeEnabled: true,
|
||||
@@ -246,7 +248,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
uniqueAccountRegions.add(loc.locationName);
|
||||
regionOptions.push({
|
||||
key: loc.documentEndpoint,
|
||||
text: `${loc.locationName} (Read/Write)`,
|
||||
text: `${loc.locationName} ${t(Keys.panes.settings.readWrite)}`,
|
||||
data: {
|
||||
isGlobal: false,
|
||||
writeEnabled: true,
|
||||
@@ -259,7 +261,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
uniqueAccountRegions.add(loc.locationName);
|
||||
regionOptions.push({
|
||||
key: loc.documentEndpoint,
|
||||
text: `${loc.locationName} (Read)`,
|
||||
text: `${loc.locationName} ${t(Keys.panes.settings.read)}`,
|
||||
data: {
|
||||
isGlobal: false,
|
||||
writeEnabled: false,
|
||||
@@ -317,13 +319,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
authError instanceof msalAuthError &&
|
||||
authError.errorCode === msalBrowserAuthErrorMessage.popUpWindowError.code
|
||||
) {
|
||||
logConsoleError(
|
||||
`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`,
|
||||
);
|
||||
logConsoleError(t(Keys.panes.settings.popupsDisabledError));
|
||||
} else {
|
||||
logConsoleError(
|
||||
`"Failed to acquire authorization token automatically. Please click on "Login for Entra ID" button to enable Entra ID RBAC operations`,
|
||||
);
|
||||
logConsoleError(t(Keys.panes.settings.failedToAcquireTokenError));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -485,33 +483,33 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
const genericPaneProps: RightPaneFormProps = {
|
||||
formError: "",
|
||||
isExecuting,
|
||||
submitButtonText: "Apply",
|
||||
submitButtonText: t(Keys.common.apply),
|
||||
onSubmit: () => handlerOnSubmit(),
|
||||
};
|
||||
const pageOptionList: IChoiceGroupOption[] = [
|
||||
{ key: Constants.Queries.CustomPageOption, text: "Custom" },
|
||||
{ key: Constants.Queries.UnlimitedPageOption, text: "Unlimited" },
|
||||
{ key: Constants.Queries.CustomPageOption, text: t(Keys.panes.settings.custom) },
|
||||
{ key: Constants.Queries.UnlimitedPageOption, text: t(Keys.panes.settings.unlimited) },
|
||||
];
|
||||
|
||||
const graphAutoOptionList: IChoiceGroupOption[] = [
|
||||
{ key: "false", text: "Graph" },
|
||||
{ key: "true", text: "JSON" },
|
||||
{ key: "false", text: t(Keys.panes.settings.graph) },
|
||||
{ key: "true", text: t(Keys.panes.settings.json) },
|
||||
];
|
||||
|
||||
const priorityLevelOptionList: IChoiceGroupOption[] = [
|
||||
{ key: Constants.PriorityLevel.Low, text: "Low" },
|
||||
{ key: Constants.PriorityLevel.High, text: "High" },
|
||||
{ key: Constants.PriorityLevel.Low, text: t(Keys.panes.settings.low) },
|
||||
{ key: Constants.PriorityLevel.High, text: t(Keys.panes.settings.high) },
|
||||
];
|
||||
|
||||
const dataPlaneRBACOptionsList: IChoiceGroupOption[] = [
|
||||
{ key: Constants.RBACOptions.setAutomaticRBACOption, text: "Automatic" },
|
||||
{ key: Constants.RBACOptions.setTrueRBACOption, text: "True" },
|
||||
{ key: Constants.RBACOptions.setFalseRBACOption, text: "False" },
|
||||
{ key: Constants.RBACOptions.setAutomaticRBACOption, text: t(Keys.panes.settings.automatic) },
|
||||
{ key: Constants.RBACOptions.setTrueRBACOption, text: t(Keys.panes.settings["true"]) },
|
||||
{ key: Constants.RBACOptions.setFalseRBACOption, text: t(Keys.panes.settings["false"]) },
|
||||
];
|
||||
|
||||
const defaultQueryResultsViewOptionList: IChoiceGroupOption[] = [
|
||||
{ key: SplitterDirection.Vertical, text: "Vertical" },
|
||||
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
|
||||
{ key: SplitterDirection.Vertical, text: t(Keys.tabs.query.vertical) },
|
||||
{ key: SplitterDirection.Horizontal, text: t(Keys.tabs.query.horizontal) },
|
||||
];
|
||||
|
||||
const mongoGuidRepresentationDropdownOptions: IDropdownOption[] = [
|
||||
@@ -724,13 +722,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{shouldShowQueryPageOptions && (
|
||||
<AccordionItem value="1">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Page Options</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.pageOptions)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as
|
||||
many query results per page.
|
||||
{t(Keys.panes.settings.pageOptionsDescription)}
|
||||
</div>
|
||||
<ChoiceGroup
|
||||
ariaLabelledBy="pageOptions"
|
||||
@@ -744,14 +741,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{isCustomPageOptionSelected() && (
|
||||
<div className="tabcontent">
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Query results per page{" "}
|
||||
{t(Keys.panes.settings.queryResultsPerPage)}{" "}
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Enter the number of query results that should be shown per page.
|
||||
{t(Keys.panes.settings.queryResultsPerPageTooltip)}
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
|
||||
<SpinButton
|
||||
ariaLabel="Custom query items per page"
|
||||
ariaLabel={t(Keys.panes.settings.customQueryItemsPerPage)}
|
||||
value={"" + customItemPerPage}
|
||||
onIncrement={(newValue) => {
|
||||
setCustomItemPerPage(parseInt(newValue) + 1 || customItemPerPage);
|
||||
@@ -761,8 +758,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
min={1}
|
||||
step={1}
|
||||
className="textfontclr"
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
incrementButtonAriaLabel={t(Keys.common.increaseValueBy1)}
|
||||
decrementButtonAriaLabel={t(Keys.common.decreaseValueBy1)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
</div>
|
||||
@@ -774,20 +771,19 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{showEnableEntraIdRbac && (
|
||||
<AccordionItem value="2">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable Entra ID RBAC</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.entraIdRbac)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
|
||||
ID RBAC.
|
||||
{t(Keys.panes.settings.entraIdRbacDescription)}
|
||||
<a
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{" "}
|
||||
Learn more{" "}
|
||||
{t(Keys.common.learnMore)}{" "}
|
||||
</a>
|
||||
</div>
|
||||
<ChoiceGroup
|
||||
@@ -804,17 +800,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && !isFabric() && (
|
||||
<AccordionItem value="3">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Region Selection</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.regionSelection)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Changes region the Cosmos Client uses to access account.
|
||||
{t(Keys.panes.settings.regionSelectionDescription)}
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Select Region</span>
|
||||
<span className={styles.subHeader}>{t(Keys.panes.settings.selectRegion)}</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Changes the account endpoint used to perform client operations.
|
||||
{t(Keys.panes.settings.selectRegionTooltip)}
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<Dropdown
|
||||
@@ -865,17 +861,16 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
<>
|
||||
<AccordionItem value="4">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Query Timeout</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.queryTimeout)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
When a query reaches a specified time limit, a popup with an option to cancel the query will
|
||||
show unless automatic cancellation has been enabled.
|
||||
{t(Keys.panes.settings.queryTimeoutDescription)}
|
||||
</div>
|
||||
<Toggle
|
||||
styles={toggleStyles}
|
||||
label="Enable query timeout"
|
||||
label={t(Keys.panes.settings.enableQueryTimeout)}
|
||||
onChange={handleOnQueryTimeoutToggleChange}
|
||||
defaultChecked={queryTimeoutEnabled}
|
||||
/>
|
||||
@@ -883,18 +878,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{queryTimeoutEnabled && (
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<SpinButton
|
||||
label="Query timeout (ms)"
|
||||
label={t(Keys.panes.settings.queryTimeoutMs)}
|
||||
labelPosition={Position.top}
|
||||
defaultValue={(queryTimeout || 5000).toString()}
|
||||
min={100}
|
||||
step={1000}
|
||||
onChange={handleOnQueryTimeoutSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1000"
|
||||
decrementButtonAriaLabel="Decrease value by 1000"
|
||||
incrementButtonAriaLabel={t(Keys.panes.settings.increaseValueBy1000)}
|
||||
decrementButtonAriaLabel={t(Keys.panes.settings.decreaseValueBy1000)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
<Toggle
|
||||
label="Automatically cancel query after timeout"
|
||||
label={t(Keys.panes.settings.automaticallyCancelQuery)}
|
||||
styles={toggleStyles}
|
||||
onChange={handleOnAutomaticallyCancelQueryToggleChange}
|
||||
defaultChecked={automaticallyCancelQueryAfterTimeout}
|
||||
@@ -905,16 +900,16 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionItem>
|
||||
<AccordionItem value="5">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>RU Limit</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.ruLimit)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
If a query exceeds a configured RU limit, the query will be aborted.
|
||||
{t(Keys.panes.settings.ruLimitDescription)}
|
||||
</div>
|
||||
<Toggle
|
||||
styles={toggleStyles}
|
||||
label="Enable RU limit"
|
||||
label={t(Keys.panes.settings.enableRuLimit)}
|
||||
onChange={handleOnRUThresholdToggleChange}
|
||||
defaultChecked={ruThresholdEnabled}
|
||||
/>
|
||||
@@ -922,14 +917,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{ruThresholdEnabled && (
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<SpinButton
|
||||
label="RU Limit (RU)"
|
||||
label={t(Keys.panes.settings.ruLimitLabel)}
|
||||
labelPosition={Position.top}
|
||||
defaultValue={(ruThreshold || DefaultRUThreshold).toString()}
|
||||
min={1}
|
||||
step={1000}
|
||||
onChange={handleOnRUThresholdSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1000"
|
||||
decrementButtonAriaLabel="Decrease value by 1000"
|
||||
incrementButtonAriaLabel={t(Keys.panes.settings.increaseValueBy1000)}
|
||||
decrementButtonAriaLabel={t(Keys.panes.settings.decreaseValueBy1000)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
</div>
|
||||
@@ -939,12 +934,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
|
||||
<AccordionItem value="6">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Default Query Results View</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.defaultQueryResults)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Select the default view to use when displaying query results.
|
||||
{t(Keys.panes.settings.defaultQueryResultsDescription)}
|
||||
</div>
|
||||
<ChoiceGroup
|
||||
ariaLabelledBy="defaultQueryResultsView"
|
||||
@@ -962,17 +957,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{showRetrySettings && (
|
||||
<AccordionItem value="7">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Retry Settings</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.retrySettings)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Retry policy associated with throttled requests during CosmosDB queries.
|
||||
{t(Keys.panes.settings.retrySettingsDescription)}
|
||||
</div>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Max retry attempts</span>
|
||||
<span className={styles.subHeader}>{t(Keys.panes.settings.maxRetryAttempts)}</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Max number of retries to be performed for a request. Default value 9.
|
||||
{t(Keys.panes.settings.maxRetryAttemptsTooltip)}
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
@@ -981,18 +976,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
step={1}
|
||||
value={"" + retryAttempts}
|
||||
onChange={handleOnQueryRetryAttemptsSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
incrementButtonAriaLabel={t(Keys.common.increaseValueBy1)}
|
||||
decrementButtonAriaLabel={t(Keys.common.decreaseValueBy1)}
|
||||
onIncrement={(newValue) => setRetryAttempts(parseInt(newValue) + 1 || retryAttempts)}
|
||||
onDecrement={(newValue) => setRetryAttempts(parseInt(newValue) - 1 || retryAttempts)}
|
||||
onValidate={(newValue) => setRetryAttempts(parseInt(newValue) || retryAttempts)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Fixed retry interval (ms)</span>
|
||||
<span className={styles.subHeader}>{t(Keys.panes.settings.fixedRetryInterval)}</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned
|
||||
as part of the response. Default value is 0 milliseconds.
|
||||
{t(Keys.panes.settings.fixedRetryIntervalTooltip)}
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
@@ -1001,18 +995,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
step={1000}
|
||||
value={"" + retryInterval}
|
||||
onChange={handleOnRetryIntervalSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1000"
|
||||
decrementButtonAriaLabel="Decrease value by 1000"
|
||||
incrementButtonAriaLabel={t(Keys.panes.settings.increaseValueBy1000)}
|
||||
decrementButtonAriaLabel={t(Keys.panes.settings.decreaseValueBy1000)}
|
||||
onIncrement={(newValue) => setRetryInterval(parseInt(newValue) + 1000 || retryInterval)}
|
||||
onDecrement={(newValue) => setRetryInterval(parseInt(newValue) - 1000 || retryInterval)}
|
||||
onValidate={(newValue) => setRetryInterval(parseInt(newValue) || retryInterval)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
<div>
|
||||
<span className={styles.subHeader}>Max wait time (s)</span>
|
||||
<span className={styles.subHeader}>{t(Keys.panes.settings.maxWaitTime)}</span>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
|
||||
seconds.
|
||||
{t(Keys.panes.settings.maxWaitTimeTooltip)}
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<SpinButton
|
||||
@@ -1021,8 +1014,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
step={1}
|
||||
value={"" + MaxWaitTimeInSeconds}
|
||||
onChange={handleOnMaxWaitTimeSpinButtonChange}
|
||||
incrementButtonAriaLabel="Increase value by 1"
|
||||
decrementButtonAriaLabel="Decrease value by 1"
|
||||
incrementButtonAriaLabel={t(Keys.common.increaseValueBy1)}
|
||||
decrementButtonAriaLabel={t(Keys.common.decreaseValueBy1)}
|
||||
onIncrement={(newValue) =>
|
||||
setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)
|
||||
}
|
||||
@@ -1039,24 +1032,26 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{!isEmulator && (
|
||||
<AccordionItem value="8">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable container pagination</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.enableContainerPagination)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||
{t(Keys.panes.settings.enableContainerPaginationDescription)}
|
||||
</div>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Enable container pagination"
|
||||
ariaLabel={t(Keys.panes.settings.enableContainerPagination)}
|
||||
checked={containerPaginationEnabled}
|
||||
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
||||
label="Enable container pagination"
|
||||
label={t(Keys.panes.settings.enableContainerPagination)}
|
||||
onRenderLabel={() => (
|
||||
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable container pagination</span>
|
||||
<span style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.panes.settings.enableContainerPagination)}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -1066,24 +1061,25 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{shouldShowCrossPartitionOption && (
|
||||
<AccordionItem value="9">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable cross-partition query</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.enableCrossPartitionQuery)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
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.
|
||||
{t(Keys.panes.settings.enableCrossPartitionQueryDescription)}
|
||||
</div>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Enable cross partition query"
|
||||
ariaLabel={t(Keys.panes.settings.enableCrossPartitionQuery)}
|
||||
checked={crossPartitionQueryEnabled}
|
||||
onChange={() => setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)}
|
||||
onRenderLabel={() => (
|
||||
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable cross-partition query</span>
|
||||
<span style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.panes.settings.enableCrossPartitionQuery)}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -1093,19 +1089,19 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{shouldShowEnhancedQueryControl && (
|
||||
<AccordionItem value="10">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enhanced query control</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.enhancedQueryControl)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Query up to the max degree of parallelism.
|
||||
{t(Keys.panes.settings.maxDegreeOfParallelismQuery)}
|
||||
<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"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{" "}
|
||||
Learn more{" "}
|
||||
{t(Keys.common.learnMore)}{" "}
|
||||
</a>
|
||||
</div>
|
||||
<Checkbox
|
||||
@@ -1113,11 +1109,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="EnableQueryControl"
|
||||
ariaLabel={t(Keys.panes.settings.enableQueryControl)}
|
||||
checked={queryControlEnabled}
|
||||
onChange={() => setQueryControlEnabled(!queryControlEnabled)}
|
||||
onRenderLabel={() => (
|
||||
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable query control</span>
|
||||
<span style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.panes.settings.enableQueryControl)}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -1127,14 +1125,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{shouldShowParallelismOption && (
|
||||
<AccordionItem value="10">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Max degree of parallelism</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.maxDegreeOfParallelism)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Gets or sets the number of concurrent operations run client side during parallel query execution.
|
||||
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.
|
||||
{t(Keys.panes.settings.maxDegreeOfParallelismDescription)}
|
||||
</div>
|
||||
<SpinButton
|
||||
min={-1}
|
||||
@@ -1150,8 +1146,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism)
|
||||
}
|
||||
onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)}
|
||||
ariaLabel="Max degree of parallelism"
|
||||
label="Max degree of parallelism"
|
||||
ariaLabel={t(Keys.panes.settings.maxDegreeOfParallelism)}
|
||||
label={t(Keys.panes.settings.maxDegreeOfParallelism)}
|
||||
styles={spinButtonStyles}
|
||||
/>
|
||||
</div>
|
||||
@@ -1161,14 +1157,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{shouldShowPriorityLevelOption && (
|
||||
<AccordionItem value="11">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Priority Level</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.priorityLevel)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Sets the priority level for data-plane requests from Data Explorer when using Priority-Based
|
||||
Execution. If "None" is selected, Data Explorer will not specify priority level, and the
|
||||
server-side default priority level will be used.
|
||||
{t(Keys.panes.settings.priorityLevelDescription)}
|
||||
</div>
|
||||
<ChoiceGroup
|
||||
ariaLabelledBy="priorityLevel"
|
||||
@@ -1184,19 +1178,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{shouldShowGraphAutoVizOption && (
|
||||
<AccordionItem value="12">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Display Gremlin query results as: </div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.displayGremlinQueryResults)} </div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Select Graph to automatically visualize the query results as a Graph or JSON to display the
|
||||
results as JSON.
|
||||
{t(Keys.panes.settings.displayGremlinQueryResultsDescription)}
|
||||
</div>
|
||||
<ChoiceGroup
|
||||
selectedKey={graphAutoVizDisabled}
|
||||
options={graphAutoOptionList}
|
||||
onChange={handleOnGremlinChange}
|
||||
aria-label="Graph Auto-visualization"
|
||||
aria-label={t(Keys.panes.settings.graphAutoVisualization)}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
@@ -1205,25 +1198,25 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{shouldShowCopilotSampleDBOption && (
|
||||
<AccordionItem value="13">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enable sample database</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.enableSampleDatabase)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
This is a sample database and collection with synthetic product data you can use to explore using
|
||||
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.
|
||||
{t(Keys.panes.settings.enableSampleDatabaseDescription)}
|
||||
</div>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Enable sample db for query exploration"
|
||||
ariaLabel={t(Keys.panes.settings.enableSampleDbAriaLabel)}
|
||||
checked={copilotSampleDBEnabled}
|
||||
onChange={handleSampleDatabaseChange}
|
||||
onRenderLabel={() => (
|
||||
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable sample database</span>
|
||||
<span style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.panes.settings.enableSampleDatabase)}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -1233,13 +1226,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{shouldShowMongoGuidRepresentationOption && (
|
||||
<AccordionItem value="14">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Guid Representation</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.guidRepresentation)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
GuidRepresentation in MongoDB refers to how Globally Unique Identifiers (GUIDs) are serialized and
|
||||
deserialized when stored in BSON documents. This will apply to all document operations.
|
||||
{t(Keys.panes.settings.guidRepresentationDescription)}
|
||||
</div>
|
||||
<Dropdown
|
||||
aria-labelledby="mongoGuidRepresentation"
|
||||
@@ -1253,7 +1245,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
)}
|
||||
<AccordionItem value="15">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Advanced Settings</div>
|
||||
<div className={styles.header}>{t(Keys.panes.settings.advancedSettings)}</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
@@ -1283,14 +1275,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
},
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Ignore partition key on document update"
|
||||
ariaLabel={t(Keys.panes.settings.ignorePartitionKey)}
|
||||
checked={ignorePartitionKeyOnDocumentUpdate}
|
||||
onChange={handleOnIgnorePartitionKeyOnDocumentUpdateChange}
|
||||
label="Ignore partition key on document update"
|
||||
label={t(Keys.panes.settings.ignorePartitionKey)}
|
||||
/>
|
||||
<InfoTooltip className={styles.headerIcon}>
|
||||
If checked, the partition key value will not be used to locate the document during update
|
||||
operations. Only use this if document updates are failing due to an abnormal partition key.
|
||||
{t(Keys.panes.settings.ignorePartitionKeyTooltip)}
|
||||
</InfoTooltip>
|
||||
</Stack>
|
||||
</div>
|
||||
@@ -1320,9 +1311,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
}}
|
||||
onClick={() => {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
"Clear History",
|
||||
t(Keys.panes.settings.clearHistory),
|
||||
undefined,
|
||||
"Are you sure you want to proceed?",
|
||||
t(Keys.panes.settings.clearHistoryConfirm),
|
||||
() => {
|
||||
deleteAllStates();
|
||||
updateUserContext({
|
||||
@@ -1332,35 +1323,33 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
});
|
||||
useClientWriteEnabled.setState({ clientWriteEnabled: true });
|
||||
},
|
||||
"Cancel",
|
||||
t(Keys.common.cancel),
|
||||
undefined,
|
||||
<>
|
||||
<span>
|
||||
This action will clear the all customizations for this account in this browser, including:
|
||||
</span>
|
||||
<span>{t(Keys.panes.settings.clearHistoryDescription)}</span>
|
||||
<ul className={styles.bulletList}>
|
||||
<li>Reset your customized tab layout, including the splitter positions</li>
|
||||
<li>Erase your table column preferences, including any custom columns</li>
|
||||
<li>Clear your filter history</li>
|
||||
<li>Reset region selection to global</li>
|
||||
<li>{t(Keys.panes.settings.clearHistoryTabLayout)}</li>
|
||||
<li>{t(Keys.panes.settings.clearHistoryTableColumns)}</li>
|
||||
<li>{t(Keys.panes.settings.clearHistoryFilters)}</li>
|
||||
<li>{t(Keys.panes.settings.clearHistoryRegion)}</li>
|
||||
</ul>
|
||||
</>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Clear History
|
||||
{t(Keys.panes.settings.clearHistory)}
|
||||
</DefaultButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settingsSection">
|
||||
<div className={`settingsSectionPart ${styles.settingsSectionContainer}`}>
|
||||
<div className="settingsSectionLabel">Explorer Version</div>
|
||||
<div className="settingsSectionLabel">{t(Keys.panes.settings.explorerVersion)}</div>
|
||||
<div>{explorerVersion}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settingsSection">
|
||||
<div className="settingsSectionPart">
|
||||
<div className="settingsSectionLabel">Session ID</div>
|
||||
<div className="settingsSectionLabel">{t(Keys.panes.settings.sessionId)}</div>
|
||||
<div>{sessionId}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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.
|
||||
</div>
|
||||
<StyledCheckboxBase
|
||||
ariaLabel="Enable cross partition query"
|
||||
ariaLabel="Enable cross-partition query"
|
||||
checked={false}
|
||||
className="padding"
|
||||
onChange={[Function]}
|
||||
@@ -705,7 +705,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
</a>
|
||||
</div>
|
||||
<StyledCheckboxBase
|
||||
ariaLabel="EnableQueryControl"
|
||||
ariaLabel="Enable query control"
|
||||
checked={false}
|
||||
className="padding"
|
||||
onChange={[Function]}
|
||||
@@ -1190,7 +1190,8 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
<div
|
||||
className="___j7dlp70_0000000 fq02s40 f19n0e5"
|
||||
>
|
||||
Display Gremlin query results as:
|
||||
Display Gremlin query results as:
|
||||
|
||||
</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
import { configContext } from "ConfigContext";
|
||||
import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent";
|
||||
import { CosmosFluentProvider, getPlatformTheme } from "Explorer/Theme/ThemeUtil";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import React from "react";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
|
||||
@@ -113,13 +115,13 @@ export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> =
|
||||
<CosmosFluentProvider>
|
||||
<div className="panelFormWrapper">
|
||||
<div className="panelMainContent" style={{ display: "flex", flexDirection: "column" }}>
|
||||
<Text>Select which columns to display in your view of items in your container.</Text>
|
||||
<Text>{t(Keys.panes.tableColumnSelection.selectColumns)}</Text>
|
||||
<div /* Wrap <SearchBox> to avoid margin-bottom set by panelMainContent css */>
|
||||
<SearchBox
|
||||
className={styles.searchBox}
|
||||
value={columnSearchText}
|
||||
onChange={onSearchChange}
|
||||
placeholder="Search fields"
|
||||
placeholder={t(Keys.panes.tableColumnSelection.searchFields)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -130,7 +132,9 @@ export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> =
|
||||
key={columnDefinition.id}
|
||||
label={{
|
||||
className: styles.checkboxLabel,
|
||||
children: `${columnDefinition.label}${columnDefinition.isPartitionKey ? " (partition key)" : ""}`,
|
||||
children: `${columnDefinition.label}${
|
||||
columnDefinition.isPartitionKey ? t(Keys.panes.tableColumnSelection.partitionKeySuffix) : ""
|
||||
}`,
|
||||
}}
|
||||
checked={selectedColumnIdsSet.has(columnDefinition.id)}
|
||||
onChange={(_, data) => onCheckedValueChange(columnDefinition.id, data)}
|
||||
@@ -138,15 +142,15 @@ export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> =
|
||||
))}
|
||||
</div>
|
||||
<Button appearance="secondary" size="small" onClick={() => setNewSelectedColumnIds(defaultSelection)}>
|
||||
Reset
|
||||
{t(Keys.panes.tableColumnSelection.reset)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="panelFooter" style={{ display: "flex", gap: theme.spacingHorizontalS }}>
|
||||
<Button appearance="primary" onClick={onSave}>
|
||||
Save
|
||||
{t(Keys.common.save)}
|
||||
</Button>
|
||||
<Button appearance="secondary" onClick={closeSidePanel}>
|
||||
Cancel
|
||||
{t(Keys.common.cancel)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as _ from "underscore";
|
||||
@@ -100,8 +102,8 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
const { property, type, value } = entities[i];
|
||||
if ((property === "PartitionKey" && value === "") || (property === "RowKey" && value === "")) {
|
||||
logConsoleError(`${property} cannot be empty. Please input a value for ${property}`);
|
||||
setFormError(`${property} cannot be empty. Please input a value for ${property}`);
|
||||
logConsoleError(t(Keys.panes.tables.propertyEmptyError, { property }));
|
||||
setFormError(t(Keys.panes.tables.propertyEmptyError, { property }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -109,13 +111,13 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||
(property === "PartitionKey" && containsAnyWhiteSpace(value) === true) ||
|
||||
(property === "RowKey" && containsAnyWhiteSpace(value) === true)
|
||||
) {
|
||||
logConsoleError(`${property} cannot have whitespace. Please input a value for ${property} without whitespace`);
|
||||
setFormError(`${property} cannot have whitespace. Please input a value for ${property} without whitespace`);
|
||||
logConsoleError(t(Keys.panes.tables.whitespaceError, { property }));
|
||||
setFormError(t(Keys.panes.tables.whitespaceError, { property }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
|
||||
setFormError(t(Keys.panes.tables.propertyTypeEmptyError, { property }));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { useBoolean } from "@fluentui/react-hooks";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as _ from "underscore";
|
||||
@@ -198,7 +200,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
|
||||
setFormError(t(Keys.panes.tables.propertyTypeEmptyError, { property }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -208,8 +210,8 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
||||
(property === "RowKey" && value === "") ||
|
||||
(property === "RowKey" && value === undefined)
|
||||
) {
|
||||
logConsoleError(`${property} cannot be empty. Please input a value for ${property}`);
|
||||
setFormError(`${property} cannot be empty. Please input a value for ${property}`);
|
||||
logConsoleError(t(Keys.panes.tables.propertyEmptyError, { property }));
|
||||
setFormError(t(Keys.panes.tables.propertyEmptyError, { property }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -403,7 +405,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
|
||||
)}
|
||||
</div>
|
||||
<div className="panelNullWarning" style={{ padding: "20px", color: "red" }}>
|
||||
Warning: Null fields will not be displayed for editing.
|
||||
{t(Keys.panes.tables.nullFieldsWarning)}
|
||||
</div>
|
||||
</RightPaneForm>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Checkbox, Text } from "@fluentui/react";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import { userContext } from "../../../../UserContext";
|
||||
import { useSidePanel } from "../../../../hooks/useSidePanel";
|
||||
@@ -35,7 +37,7 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
|
||||
const props: RightPaneFormProps = {
|
||||
formError: "",
|
||||
isExecuting: false,
|
||||
submitButtonText: "OK",
|
||||
submitButtonText: t(Keys.common.ok),
|
||||
onSubmit,
|
||||
};
|
||||
|
||||
@@ -121,11 +123,11 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelFormWrapper">
|
||||
<div className="panelMainContent">
|
||||
<Text>Select the columns that you want to query.</Text>
|
||||
<Text>{t(Keys.panes.tableQuerySelect.selectColumns)}</Text>
|
||||
<div className="column-select-view">
|
||||
<Checkbox
|
||||
id="availableCheckbox"
|
||||
label="Available Columns"
|
||||
label={t(Keys.panes.tableQuerySelect.availableColumns)}
|
||||
checked={isAvailableColumnChecked}
|
||||
onChange={availableColumnsCheckboxClick}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import { Upload } from "Common/Upload/Upload";
|
||||
import { UploadDetailsRecord } from "Contracts/ViewModels";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { logConsoleError } from "Utils/NotificationConsoleUtils";
|
||||
import React, { ChangeEvent, FunctionComponent, useReducer, useState } from "react";
|
||||
import { getErrorMessage } from "../../Tables/Utilities";
|
||||
@@ -63,8 +65,8 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
|
||||
const onSubmit = () => {
|
||||
setFormError("");
|
||||
if (!files || files.length === 0) {
|
||||
setFormError("No files were specified. Please input at least one file.");
|
||||
logConsoleError("Could not upload items -- No files were specified. Please input at least one file.");
|
||||
setFormError(t(Keys.panes.uploadItems.noFilesSpecifiedError));
|
||||
logConsoleError(t(Keys.panes.uploadItems.noFilesSpecifiedError));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -150,7 +152,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
|
||||
},
|
||||
{
|
||||
key: "fileName",
|
||||
name: "FILE NAME",
|
||||
name: t(Keys.panes.uploadItems.fileNameColumn),
|
||||
fieldName: "fileName",
|
||||
minWidth: 120,
|
||||
maxWidth: 140,
|
||||
@@ -169,7 +171,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
name: "STATUS",
|
||||
name: t(Keys.panes.uploadItems.statusColumn),
|
||||
fieldName: "numSucceeded",
|
||||
minWidth: 120,
|
||||
maxWidth: 140,
|
||||
@@ -178,7 +180,11 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
|
||||
data: "string",
|
||||
isPadded: true,
|
||||
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
|
||||
const fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
|
||||
const fieldContent = t(Keys.panes.uploadItems.uploadStatus, {
|
||||
numSucceeded: item.numSucceeded,
|
||||
numThrottled: item.numThrottled,
|
||||
numFailed: item.numFailed,
|
||||
});
|
||||
return (
|
||||
<TooltipHost
|
||||
content={fieldContent}
|
||||
@@ -197,12 +203,12 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
|
||||
<div className="paneMainContent">
|
||||
<Upload
|
||||
key={reducer} // Force re-render on state change
|
||||
label="Select JSON Files"
|
||||
label={t(Keys.panes.uploadItems.selectJsonFiles)}
|
||||
onUpload={updateSelectedFiles}
|
||||
accept="application/json"
|
||||
multiple
|
||||
tabIndex={0}
|
||||
tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB. You can perform multiple upload operations for larger data sets."
|
||||
tooltip={t(Keys.panes.uploadItems.selectJsonFilesTooltip)}
|
||||
/>
|
||||
{uploadFileData?.length > 0 && (
|
||||
<div className="fileUploadSummaryContainer" data-test="file-upload-status">
|
||||
|
||||
@@ -714,9 +714,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
<span
|
||||
className="css-126"
|
||||
>
|
||||
What is the reason why you are deleting this
|
||||
Database
|
||||
?
|
||||
What is the reason why you are deleting this Database?
|
||||
</span>
|
||||
</Text>
|
||||
<StyledTextFieldBase
|
||||
|
||||
@@ -6,6 +6,8 @@ import { DocumentAddRegular, LinkMultipleRegular, OpenRegular } from "@fluentui/
|
||||
import { SampleDataConfiguration, SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
|
||||
import { SampleDataFile } from "Explorer/SplashScreen/SampleUtil";
|
||||
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
|
||||
import * as React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
@@ -159,8 +161,8 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
const getSplashScreenButtons = (): JSX.Element => {
|
||||
const buttons: FabricHomeScreenButtonProps[] = [
|
||||
{
|
||||
title: "New container",
|
||||
description: "Create a destination container to store your data",
|
||||
title: t(Keys.splashScreen.fabric.newContainer.title),
|
||||
description: t(Keys.splashScreen.fabric.newContainer.description),
|
||||
icon: <DocumentAddRegular />,
|
||||
onClick: () => {
|
||||
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
|
||||
@@ -168,8 +170,8 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Sample Data",
|
||||
description: "Load sample data in your database",
|
||||
title: t(Keys.splashScreen.fabric.sampleData.title),
|
||||
description: t(Keys.splashScreen.fabric.sampleData.description),
|
||||
icon: <img src={CosmosDbBlackIcon} alt={"Azure Cosmos DB icon"} aria-hidden="true" />,
|
||||
onClick: () => {
|
||||
setSelectedSampleDataConfiguration({
|
||||
@@ -181,8 +183,8 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Sample Vector Data",
|
||||
description: "Load sample vector data with text-embedding-ada-002",
|
||||
title: t(Keys.splashScreen.fabric.sampleVectorData.title),
|
||||
description: t(Keys.splashScreen.fabric.sampleVectorData.description),
|
||||
icon: <img src={AzureOpenAiIcon} alt={"Azure Open AI icon"} aria-hidden="true" />,
|
||||
onClick: () => {
|
||||
setSelectedSampleDataConfiguration({
|
||||
@@ -194,14 +196,14 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "App development",
|
||||
description: "Start here to use an SDK to build your apps",
|
||||
title: t(Keys.splashScreen.fabric.appDevelopment.title),
|
||||
description: t(Keys.splashScreen.fabric.appDevelopment.description),
|
||||
icon: <LinkMultipleRegular />,
|
||||
onClick: () => window.open("https://aka.ms/cosmosdbfabricsdk", "_blank"),
|
||||
},
|
||||
{
|
||||
title: "Sample Gallery",
|
||||
description: "Get real-world end-to-end samples",
|
||||
title: t(Keys.splashScreen.fabric.sampleGallery.title),
|
||||
description: t(Keys.splashScreen.fabric.sampleGallery.description),
|
||||
icon: <img src={GithubIcon} alt={"GitHub icon"} aria-hidden="true" />,
|
||||
onClick: () => window.open("https://aka.ms/CosmosFabricSamplesGallery", "_blank"),
|
||||
},
|
||||
@@ -222,7 +224,9 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
);
|
||||
};
|
||||
|
||||
const title = isFabricNativeReadOnly() ? "Use your database" : "Build your database";
|
||||
const title = isFabricNativeReadOnly()
|
||||
? t(Keys.splashScreen.fabric.useTitle)
|
||||
: t(Keys.splashScreen.fabric.buildTitle);
|
||||
return (
|
||||
<>
|
||||
<CosmosFluentProvider className={styles.homeContainer}>
|
||||
@@ -238,9 +242,9 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
{getSplashScreenButtons()}
|
||||
{
|
||||
<div className={styles.footer}>
|
||||
Need help?{" "}
|
||||
{t(Keys.splashScreen.sections.needHelp)}{" "}
|
||||
<Link href="https://learn.microsoft.com/fabric/database/cosmos-db/overview" target="_blank">
|
||||
Learn more <OpenRegular />
|
||||
{t(Keys.common.learnMore)} <OpenRegular />
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
} from "@fluentui/react-components";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { checkContainerExists, createContainer, importData, SampleDataFile } from "Explorer/SplashScreen/SampleUtil";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
|
||||
@@ -59,7 +61,7 @@ export const SampleDataImportDialog: React.FC<{
|
||||
setStatus("creating");
|
||||
const databaseName = props.sampleDataConfiguration.databaseName;
|
||||
if (checkContainerExists(databaseName, containerName)) {
|
||||
const msg = `The container "${containerName}" in database "${databaseName}" already exists. Please delete it and retry.`;
|
||||
const msg = t(Keys.splashScreen.sampleDataDialog.errorContainerExists, { containerName, databaseName });
|
||||
setStatus("error");
|
||||
setErrorMessage(msg);
|
||||
return;
|
||||
@@ -75,7 +77,11 @@ export const SampleDataImportDialog: React.FC<{
|
||||
);
|
||||
} catch (error) {
|
||||
setStatus("error");
|
||||
setErrorMessage(`Failed to create container: ${error instanceof Error ? error.message : String(error)}`);
|
||||
setErrorMessage(
|
||||
t(Keys.splashScreen.sampleDataDialog.errorCreateContainer, {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -86,7 +92,11 @@ export const SampleDataImportDialog: React.FC<{
|
||||
setStatus("completed");
|
||||
} catch (error) {
|
||||
setStatus("error");
|
||||
setErrorMessage(`Failed to import data: ${error instanceof Error ? error.message : String(error)}`);
|
||||
setErrorMessage(
|
||||
t(Keys.splashScreen.sampleDataDialog.errorImportData, {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -112,14 +122,26 @@ export const SampleDataImportDialog: React.FC<{
|
||||
const renderContent = () => {
|
||||
switch (status) {
|
||||
case "idle":
|
||||
return `Create a container "${containerName}" and import sample data into it. This may take a few minutes.`;
|
||||
return t(Keys.splashScreen.sampleDataDialog.createPrompt, { containerName });
|
||||
|
||||
case "creating":
|
||||
return <Spinner size="small" labelPosition="above" label={`Creating container "${containerName}"...`} />;
|
||||
return (
|
||||
<Spinner
|
||||
size="small"
|
||||
labelPosition="above"
|
||||
label={t(Keys.splashScreen.sampleDataDialog.creatingContainer, { containerName })}
|
||||
/>
|
||||
);
|
||||
case "importing":
|
||||
return <Spinner size="small" labelPosition="above" label={`Importing data into "${containerName}"...`} />;
|
||||
return (
|
||||
<Spinner
|
||||
size="small"
|
||||
labelPosition="above"
|
||||
label={t(Keys.splashScreen.sampleDataDialog.importingData, { containerName })}
|
||||
/>
|
||||
);
|
||||
case "completed":
|
||||
return `Successfully created "${containerName}" with sample data.`;
|
||||
return t(Keys.splashScreen.sampleDataDialog.success, { containerName });
|
||||
case "error":
|
||||
return (
|
||||
<div style={{ color: "red" }}>
|
||||
@@ -132,14 +154,14 @@ export const SampleDataImportDialog: React.FC<{
|
||||
const getButtonLabel = () => {
|
||||
switch (status) {
|
||||
case "idle":
|
||||
return "Start";
|
||||
return t(Keys.splashScreen.sampleDataDialog.startButton);
|
||||
case "creating":
|
||||
case "importing":
|
||||
return "Close";
|
||||
return t(Keys.common.close);
|
||||
case "completed":
|
||||
return "Close";
|
||||
return t(Keys.common.close);
|
||||
case "error":
|
||||
return "Close";
|
||||
return t(Keys.common.close);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -147,7 +169,7 @@ export const SampleDataImportDialog: React.FC<{
|
||||
<Dialog open={props.open} onOpenChange={(event, data) => props.setOpen(data.open)}>
|
||||
<DialogSurface>
|
||||
<DialogBody>
|
||||
<DialogTitle>Sample Data</DialogTitle>
|
||||
<DialogTitle>{t(Keys.splashScreen.sampleDataDialog.title)}</DialogTitle>
|
||||
<DialogContent>
|
||||
<div className={styles.dialogContent}>{renderContent()}</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -54,6 +54,6 @@
|
||||
.mainButtonsContainer {
|
||||
display: flex;
|
||||
gap: 0 16px;
|
||||
margin-bottom: 10px
|
||||
margin: 40px auto
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import { sendMessage } from "Common/MessageHandler";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { TerminalKind } from "Contracts/ViewModels";
|
||||
import { SplashScreenButton } from "Explorer/SplashScreen/SplashScreenButton";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
@@ -164,6 +166,23 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
const container = explorer;
|
||||
const subscriptions: Array<{ dispose: () => void }> = [];
|
||||
|
||||
let title: string;
|
||||
let subtitle: string;
|
||||
|
||||
switch (userContext.apiType) {
|
||||
case "Postgres":
|
||||
title = t(Keys.splashScreen.title.postgres);
|
||||
subtitle = t(Keys.splashScreen.subtitle.getStarted);
|
||||
break;
|
||||
case "VCoreMongo":
|
||||
title = t(Keys.splashScreen.title.vcoreMongo);
|
||||
subtitle = t(Keys.splashScreen.subtitle.getStarted);
|
||||
break;
|
||||
default:
|
||||
title = t(Keys.splashScreen.title.default);
|
||||
subtitle = t(Keys.splashScreen.subtitle.default);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
subscriptions.push(
|
||||
{
|
||||
@@ -232,8 +251,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
<Stack className="splashStackRow" horizontal>
|
||||
<SplashScreenButton
|
||||
imgSrc={QuickStartIcon}
|
||||
title={"Launch quick start"}
|
||||
description={"Launch a quick start tutorial to get started with sample data"}
|
||||
title={t(Keys.splashScreen.quickStart.title)}
|
||||
description={t(Keys.splashScreen.quickStart.description)}
|
||||
onClick={() => {
|
||||
container.onNewCollectionClicked({ isQuickstart: true });
|
||||
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
|
||||
@@ -241,8 +260,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
/>
|
||||
<SplashScreenButton
|
||||
imgSrc={ContainersIcon}
|
||||
title={`New ${getCollectionName()}`}
|
||||
description={"Create a new container for storage and throughput"}
|
||||
title={t(Keys.splashScreen.newCollection.title, { collectionName: getCollectionName() })}
|
||||
description={t(Keys.splashScreen.newCollection.description)}
|
||||
onClick={() => {
|
||||
container.onNewCollectionClicked();
|
||||
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
|
||||
@@ -253,10 +272,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
<SplashScreenButton
|
||||
imgSrc={CosmosDBIcon}
|
||||
imgSize={35}
|
||||
title={"Azure Cosmos DB Samples Gallery"}
|
||||
description={
|
||||
"Discover samples that showcase scalable, intelligent app patterns. Try one now to see how fast you can go from concept to code with Cosmos DB"
|
||||
}
|
||||
title={t(Keys.splashScreen.samplesGallery.title)}
|
||||
description={t(Keys.splashScreen.samplesGallery.description)}
|
||||
onClick={() => {
|
||||
window.open("https://azurecosmosdb.github.io/gallery/?tags=example", "_blank");
|
||||
traceOpen(Action.LearningResourcesClicked, { apiType: userContext.apiType });
|
||||
@@ -264,8 +281,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
/>
|
||||
<SplashScreenButton
|
||||
imgSrc={ConnectIcon}
|
||||
title={"Connect"}
|
||||
description={"Prefer using your own choice of tooling? Find the connection string you need to connect"}
|
||||
title={t(Keys.splashScreen.connectCard.title)}
|
||||
description={t(Keys.splashScreen.connectCard.description)}
|
||||
onClick={() => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect)}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -280,7 +297,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
usePostgres.getState().showPostgreTeachingBubble &&
|
||||
!usePostgres.getState().showResetPasswordBubble && (
|
||||
<TeachingBubble
|
||||
headline="New to Cosmos DB PGSQL?"
|
||||
headline={t(Keys.splashScreen.teachingBubble.newToPostgres.headline)}
|
||||
target={"#mainButton-quickstartDescription"}
|
||||
hasCloseButton
|
||||
onDismiss={() => usePostgres.getState().setShowPostgreTeachingBubble(false)}
|
||||
@@ -292,15 +309,14 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
preventDismissOnScroll: true,
|
||||
}}
|
||||
primaryButtonProps={{
|
||||
text: "Get started",
|
||||
text: t(Keys.common.getStarted),
|
||||
onClick: () => {
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart);
|
||||
usePostgres.getState().setShowPostgreTeachingBubble(false);
|
||||
},
|
||||
}}
|
||||
>
|
||||
Welcome! If you are new to Cosmos DB PGSQL and need help with getting started, here is where you can find
|
||||
sample data, query.
|
||||
{t(Keys.splashScreen.teachingBubble.newToPostgres.body)}
|
||||
</TeachingBubble>
|
||||
)}
|
||||
{/*TODO: convert below to use SplashScreenButton */}
|
||||
@@ -332,7 +348,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
))}
|
||||
{userContext.apiType === "Postgres" && usePostgres.getState().showResetPasswordBubble && (
|
||||
<TeachingBubble
|
||||
headline="Create your password"
|
||||
headline={t(Keys.splashScreen.teachingBubble.resetPassword.headline)}
|
||||
target={"#mainButton-quickstartDescription"}
|
||||
hasCloseButton
|
||||
onDismiss={() => {
|
||||
@@ -347,7 +363,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
preventDismissOnScroll: true,
|
||||
}}
|
||||
primaryButtonProps={{
|
||||
text: "Create",
|
||||
text: t(Keys.common.create),
|
||||
onClick: () => {
|
||||
localStorage.setItem(userContext.databaseAccount.id, "true");
|
||||
sendMessage({
|
||||
@@ -357,7 +373,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
If you haven't changed your password yet, change it now.
|
||||
{t(Keys.splashScreen.teachingBubble.resetPassword.body)}
|
||||
</TeachingBubble>
|
||||
)}
|
||||
</div>
|
||||
@@ -376,8 +392,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
const launchQuickstartBtn = {
|
||||
id: "quickstartDescription",
|
||||
iconSrc: QuickStartIcon,
|
||||
title: "Launch quick start",
|
||||
description: "Launch a quick start tutorial to get started with sample data",
|
||||
title: t(Keys.splashScreen.quickStart.title),
|
||||
description: t(Keys.splashScreen.quickStart.description),
|
||||
onClick: () => {
|
||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart);
|
||||
@@ -399,8 +415,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
if (userContext.apiType === "Postgres") {
|
||||
return {
|
||||
iconSrc: PowerShellIcon,
|
||||
title: "PostgreSQL Shell",
|
||||
description: "Create table and interact with data using PostgreSQL's shell interface",
|
||||
title: t(Keys.splashScreen.shell.postgres.title),
|
||||
description: t(Keys.splashScreen.shell.postgres.description),
|
||||
onClick: () => container.openNotebookTerminal(TerminalKind.Postgres),
|
||||
};
|
||||
}
|
||||
@@ -408,16 +424,16 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
if (userContext.apiType === "VCoreMongo") {
|
||||
return {
|
||||
iconSrc: PowerShellIcon,
|
||||
title: "Mongo Shell",
|
||||
description: "Create a collection and interact with data using MongoDB's shell interface",
|
||||
title: t(Keys.splashScreen.shell.vcoreMongo.title),
|
||||
description: t(Keys.splashScreen.shell.vcoreMongo.description),
|
||||
onClick: () => container.openNotebookTerminal(TerminalKind.VCoreMongo),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
iconSrc: ContainersIcon,
|
||||
title: `New ${getCollectionName()}`,
|
||||
description: "Create a new container for storage and throughput",
|
||||
title: t(Keys.splashScreen.newCollection.title, { collectionName: getCollectionName() }),
|
||||
description: t(Keys.splashScreen.newCollection.description),
|
||||
onClick: () => {
|
||||
container.onNewCollectionClicked();
|
||||
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
|
||||
@@ -427,19 +443,19 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
|
||||
const getThirdCard = (): SplashScreenItem => {
|
||||
let icon = ConnectIcon;
|
||||
let title = "Connect";
|
||||
let description = "Prefer using your own choice of tooling? Find the connection string you need to connect";
|
||||
let title = t(Keys.splashScreen.connectCard.title);
|
||||
let description = t(Keys.splashScreen.connectCard.description);
|
||||
let onClick = () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect);
|
||||
|
||||
if (userContext.apiType === "Postgres") {
|
||||
title = "Connect with pgAdmin";
|
||||
description = "Prefer pgAdmin? Find your connection strings here";
|
||||
title = t(Keys.splashScreen.connectCard.pgAdmin.title);
|
||||
description = t(Keys.splashScreen.connectCard.pgAdmin.description);
|
||||
}
|
||||
|
||||
if (userContext.apiType === "VCoreMongo") {
|
||||
icon = VisualStudioIcon;
|
||||
title = "Connect with VS Code";
|
||||
description = "Query and Manage your MongoDB and DocumentDB clusters in Visual Studio Code";
|
||||
title = t(Keys.splashScreen.connectCard.vsCode.title);
|
||||
description = t(Keys.splashScreen.connectCard.vsCode.description);
|
||||
onClick = () => container?.openInVsCode && container.openInVsCode();
|
||||
}
|
||||
|
||||
@@ -468,7 +484,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
info: activity.path,
|
||||
iconSrc: NotebookIcon,
|
||||
title: activity.name,
|
||||
description: "Notebook",
|
||||
description: t(Keys.splashScreen.sections.notebook),
|
||||
onClick: () => {
|
||||
const notebookItem = container.createNotebookContentItemFile(activity.name, activity.path);
|
||||
notebookItem && container.openNotebook(notebookItem);
|
||||
@@ -507,18 +523,18 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
items = [
|
||||
{
|
||||
link: "https://aka.ms/msl-modeling-partitioning-2",
|
||||
title: "Advanced Modeling Patterns",
|
||||
description: "Learn advanced strategies to optimize your database.",
|
||||
title: t(Keys.splashScreen.top3Items.sql.advancedModeling.title),
|
||||
description: t(Keys.splashScreen.top3Items.sql.advancedModeling.description),
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/msl-modeling-partitioning-1",
|
||||
title: "Partitioning Best Practices",
|
||||
description: "Learn to apply data model and partitioning strategies.",
|
||||
title: t(Keys.splashScreen.top3Items.sql.partitioning.title),
|
||||
description: t(Keys.splashScreen.top3Items.sql.partitioning.description),
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/msl-resource-planning",
|
||||
title: "Plan Your Resource Requirements",
|
||||
description: "Get to know the different configuration choices.",
|
||||
title: t(Keys.splashScreen.top3Items.sql.resourcePlanning.title),
|
||||
description: t(Keys.splashScreen.top3Items.sql.resourcePlanning.description),
|
||||
},
|
||||
];
|
||||
break;
|
||||
@@ -526,18 +542,18 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
items = [
|
||||
{
|
||||
link: "https://aka.ms/mongodbintro",
|
||||
title: "What is the MongoDB API?",
|
||||
description: "Understand Azure Cosmos DB for MongoDB and its features.",
|
||||
title: t(Keys.splashScreen.top3Items.mongo.whatIsMongo.title),
|
||||
description: t(Keys.splashScreen.top3Items.mongo.whatIsMongo.description),
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/mongodbfeaturesupport",
|
||||
title: "Features and Syntax",
|
||||
description: "Discover the advantages and features",
|
||||
title: t(Keys.splashScreen.top3Items.mongo.features.title),
|
||||
description: t(Keys.splashScreen.top3Items.mongo.features.description),
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/mongodbpremigration",
|
||||
title: "Migrate Your Data",
|
||||
description: "Pre-migration steps for moving data",
|
||||
title: t(Keys.splashScreen.top3Items.mongo.migrate.title),
|
||||
description: t(Keys.splashScreen.top3Items.mongo.migrate.description),
|
||||
},
|
||||
];
|
||||
break;
|
||||
@@ -545,18 +561,18 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
items = [
|
||||
{
|
||||
link: "https://aka.ms/cassandrajava",
|
||||
title: "Build a Java App",
|
||||
description: "Create a Java app using an SDK.",
|
||||
title: t(Keys.splashScreen.top3Items.cassandra.buildJavaApp.title),
|
||||
description: t(Keys.splashScreen.top3Items.cassandra.buildJavaApp.description),
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/cassandrapartitioning",
|
||||
title: "Partitioning Best Practices",
|
||||
description: "Learn how partitioning works.",
|
||||
title: t(Keys.splashScreen.top3Items.cassandra.partitioning.title),
|
||||
description: t(Keys.splashScreen.top3Items.cassandra.partitioning.description),
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/cassandraRu",
|
||||
title: "Request Units (RUs)",
|
||||
description: "Understand RU charges.",
|
||||
title: t(Keys.splashScreen.top3Items.cassandra.requestUnits.title),
|
||||
description: t(Keys.splashScreen.top3Items.cassandra.requestUnits.description),
|
||||
},
|
||||
];
|
||||
break;
|
||||
@@ -564,18 +580,18 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
items = [
|
||||
{
|
||||
link: "https://aka.ms/Graphdatamodeling",
|
||||
title: "Data Modeling",
|
||||
description: "Graph data modeling recommendations",
|
||||
title: t(Keys.splashScreen.top3Items.gremlin.dataModeling.title),
|
||||
description: t(Keys.splashScreen.top3Items.gremlin.dataModeling.description),
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/graphpartitioning",
|
||||
title: "Partitioning Best Practices",
|
||||
description: "Learn how partitioning works",
|
||||
title: t(Keys.splashScreen.top3Items.gremlin.partitioning.title),
|
||||
description: t(Keys.splashScreen.top3Items.gremlin.partitioning.description),
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/graphapiquery",
|
||||
title: "Query Data",
|
||||
description: "Querying data with Gremlin",
|
||||
title: t(Keys.splashScreen.top3Items.gremlin.queryData.title),
|
||||
description: t(Keys.splashScreen.top3Items.gremlin.queryData.description),
|
||||
},
|
||||
];
|
||||
break;
|
||||
@@ -583,18 +599,18 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
items = [
|
||||
{
|
||||
link: "https://aka.ms/tableintro",
|
||||
title: "What is the Table API?",
|
||||
description: "Understand Azure Cosmos DB for Table and its features",
|
||||
title: t(Keys.splashScreen.top3Items.tables.whatIsTable.title),
|
||||
description: t(Keys.splashScreen.top3Items.tables.whatIsTable.description),
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/tableimport",
|
||||
title: "Migrate your data",
|
||||
description: "Learn how to migrate your data",
|
||||
title: t(Keys.splashScreen.top3Items.tables.migrate.title),
|
||||
description: t(Keys.splashScreen.top3Items.tables.migrate.description),
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/tablefaq",
|
||||
title: "Azure Cosmos DB for Table FAQs",
|
||||
description: "Common questions about Azure Cosmos DB for Table",
|
||||
title: t(Keys.splashScreen.top3Items.tables.faq.title),
|
||||
description: t(Keys.splashScreen.top3Items.tables.faq.description),
|
||||
},
|
||||
];
|
||||
break;
|
||||
@@ -651,7 +667,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
</ul>
|
||||
{recentItems.length > 0 && (
|
||||
<Link onClick={() => clearMostRecent()} className={styles.listItemTitle}>
|
||||
Clear Recents
|
||||
{t(Keys.splashScreen.sections.clearRecents)}
|
||||
</Link>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -666,15 +682,15 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
}
|
||||
const cdbLiveTv: item = {
|
||||
link: "https://developer.azurecosmosdb.com/tv",
|
||||
title: "Learn the Fundamentals",
|
||||
description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.",
|
||||
title: t(Keys.splashScreen.learningResources.liveTv.title),
|
||||
description: t(Keys.splashScreen.learningResources.liveTv.description),
|
||||
};
|
||||
|
||||
const commonItems: item[] = [
|
||||
{
|
||||
link: "https://learn.microsoft.com/azure/cosmos-db/data-explorer-shortcuts",
|
||||
title: "Data Explorer keyboard shortcuts",
|
||||
description: "Learn keyboard shortcuts to navigate Data Explorer.",
|
||||
title: t(Keys.splashScreen.learningResources.shortcuts.title),
|
||||
description: t(Keys.splashScreen.learningResources.shortcuts.description),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -685,14 +701,14 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
apiItems = [
|
||||
{
|
||||
link: "https://aka.ms/msl-sdk-connect",
|
||||
title: "Get Started using an SDK",
|
||||
description: "Learn about the Azure Cosmos DB SDK.",
|
||||
title: t(Keys.splashScreen.learningResources.sql.sdk.title),
|
||||
description: t(Keys.splashScreen.learningResources.sql.sdk.description),
|
||||
},
|
||||
cdbLiveTv,
|
||||
{
|
||||
link: "https://aka.ms/msl-move-data",
|
||||
title: "Migrate Your Data",
|
||||
description: "Migrate data using Azure services and open-source solutions.",
|
||||
title: t(Keys.splashScreen.learningResources.sql.migrate.title),
|
||||
description: t(Keys.splashScreen.learningResources.sql.migrate.description),
|
||||
},
|
||||
];
|
||||
break;
|
||||
@@ -700,13 +716,13 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
apiItems = [
|
||||
{
|
||||
link: "https://aka.ms/mongonodejs",
|
||||
title: "Build an app with Node.js",
|
||||
description: "Create a Node.js app.",
|
||||
title: t(Keys.splashScreen.learningResources.mongo.nodejs.title),
|
||||
description: t(Keys.splashScreen.learningResources.mongo.nodejs.description),
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/mongopython",
|
||||
title: "Getting Started Guide",
|
||||
description: "Learn the basics to get started.",
|
||||
title: t(Keys.splashScreen.learningResources.mongo.gettingStarted.title),
|
||||
description: t(Keys.splashScreen.learningResources.mongo.gettingStarted.description),
|
||||
},
|
||||
cdbLiveTv,
|
||||
];
|
||||
@@ -715,14 +731,14 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
apiItems = [
|
||||
{
|
||||
link: "https://aka.ms/cassandracontainer",
|
||||
title: "Create a Container",
|
||||
description: "Get to know the create a container options.",
|
||||
title: t(Keys.splashScreen.learningResources.cassandra.createContainer.title),
|
||||
description: t(Keys.splashScreen.learningResources.cassandra.createContainer.description),
|
||||
},
|
||||
cdbLiveTv,
|
||||
{
|
||||
link: "https://aka.ms/Cassandrathroughput",
|
||||
title: "Provision Throughput",
|
||||
description: "Learn how to configure throughput.",
|
||||
title: t(Keys.splashScreen.learningResources.cassandra.throughput.title),
|
||||
description: t(Keys.splashScreen.learningResources.cassandra.throughput.description),
|
||||
},
|
||||
];
|
||||
break;
|
||||
@@ -730,13 +746,13 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
apiItems = [
|
||||
{
|
||||
link: "https://aka.ms/graphquickstart",
|
||||
title: "Get Started ",
|
||||
description: "Create, query, and traverse using the Gremlin console",
|
||||
title: t(Keys.splashScreen.learningResources.gremlin.getStarted.title),
|
||||
description: t(Keys.splashScreen.learningResources.gremlin.getStarted.description),
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/graphimport",
|
||||
title: "Import Graph Data",
|
||||
description: "Learn Bulk ingestion data using BulkExecutor",
|
||||
title: t(Keys.splashScreen.learningResources.gremlin.importData.title),
|
||||
description: t(Keys.splashScreen.learningResources.gremlin.importData.description),
|
||||
},
|
||||
cdbLiveTv,
|
||||
];
|
||||
@@ -745,13 +761,13 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
apiItems = [
|
||||
{
|
||||
link: "https://aka.ms/tabledotnet",
|
||||
title: "Build a .NET App",
|
||||
description: "How to access Azure Cosmos DB for Table from a .NET app.",
|
||||
title: t(Keys.splashScreen.learningResources.tables.dotnet.title),
|
||||
description: t(Keys.splashScreen.learningResources.tables.dotnet.description),
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/Tablejava",
|
||||
title: "Build a Java App",
|
||||
description: "Create a Azure Cosmos DB for Table app with Java SDK ",
|
||||
title: t(Keys.splashScreen.learningResources.tables.java.title),
|
||||
description: t(Keys.splashScreen.learningResources.tables.java.description),
|
||||
},
|
||||
cdbLiveTv,
|
||||
];
|
||||
@@ -790,17 +806,17 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
const postgresNextStepItems: { link: string; title: string; description: string }[] = [
|
||||
{
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2208312",
|
||||
title: "Data Modeling",
|
||||
title: t(Keys.splashScreen.nextStepItems.postgres.dataModeling),
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: " https://go.microsoft.com/fwlink/?linkid=2206941 ",
|
||||
title: "How to choose a Distribution Column",
|
||||
title: t(Keys.splashScreen.nextStepItems.postgres.distributionColumn),
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2207425",
|
||||
title: "Build Apps with Python/Java/Django",
|
||||
title: t(Keys.splashScreen.nextStepItems.postgres.buildApps),
|
||||
description: "",
|
||||
},
|
||||
];
|
||||
@@ -808,17 +824,17 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
const vcoreMongoNextStepItems: { link: string; title: string; description: string }[] = [
|
||||
{
|
||||
link: "https://learn.microsoft.com/azure/cosmos-db/mongodb/vcore/migration-options",
|
||||
title: "Migrate Data",
|
||||
title: t(Keys.splashScreen.nextStepItems.vcoreMongo.migrateData),
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/vector-search-ai",
|
||||
title: "Build AI apps with Vector Search",
|
||||
title: t(Keys.splashScreen.nextStepItems.vcoreMongo.vectorSearch),
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/tutorial-nodejs-web-app?tabs=github-codespaces",
|
||||
title: "Build Apps with Nodejs",
|
||||
title: t(Keys.splashScreen.nextStepItems.vcoreMongo.buildApps),
|
||||
description: "",
|
||||
},
|
||||
];
|
||||
@@ -846,17 +862,17 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
const postgresLearnMoreItems: { link: string; title: string; description: string }[] = [
|
||||
{
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2207226",
|
||||
title: "Performance Tuning",
|
||||
title: t(Keys.splashScreen.learnMoreItems.postgres.performanceTuning),
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2208037",
|
||||
title: "Useful Diagnostic Queries",
|
||||
title: t(Keys.splashScreen.learnMoreItems.postgres.diagnosticQueries),
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2205270",
|
||||
title: "Distributed SQL Reference",
|
||||
title: t(Keys.splashScreen.learnMoreItems.postgres.sqlReference),
|
||||
description: "",
|
||||
},
|
||||
];
|
||||
@@ -864,17 +880,17 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
const vcoreMongoLearnMoreItems: { link: string; title: string; description: string }[] = [
|
||||
{
|
||||
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/vector-search",
|
||||
title: "Vector Search",
|
||||
title: t(Keys.splashScreen.learnMoreItems.vcoreMongo.vectorSearch),
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/how-to-create-text-index",
|
||||
title: "Text Indexing",
|
||||
title: t(Keys.splashScreen.learnMoreItems.vcoreMongo.textIndexing),
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/troubleshoot-common-issues",
|
||||
title: "Troubleshoot common issues",
|
||||
title: t(Keys.splashScreen.learnMoreItems.vcoreMongo.troubleshoot),
|
||||
description: "",
|
||||
},
|
||||
];
|
||||
@@ -902,10 +918,11 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
return (
|
||||
<div className={styles.splashScreenContainer}>
|
||||
<div className={styles.splashScreen}>
|
||||
<h2 className={styles.title} role="heading" aria-label="Welcome to Azure Cosmos DB">
|
||||
Welcome to Azure Cosmos DB<span className="activePatch"></span>
|
||||
<h2 className={styles.title} role="heading" aria-label={title}>
|
||||
{title}
|
||||
<span className="activePatch"></span>
|
||||
</h2>
|
||||
<div className={styles.subtitle}>Globally distributed, multi-model database service for any scale</div>
|
||||
<div className={styles.subtitle}>{subtitle}</div>
|
||||
{getSplashScreenButtons()}
|
||||
{useCarousel.getState().showCoachMark && (
|
||||
<Coachmark
|
||||
@@ -914,24 +931,25 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
persistentBeak
|
||||
>
|
||||
<TeachingBubbleContent
|
||||
headline={`Start with sample ${getCollectionName().toLocaleLowerCase()}`}
|
||||
headline={t(Keys.splashScreen.teachingBubble.coachMark.headline, {
|
||||
collectionName: getCollectionName().toLocaleLowerCase(),
|
||||
})}
|
||||
hasCloseButton
|
||||
closeButtonAriaLabel="Close"
|
||||
closeButtonAriaLabel={t(Keys.common.close)}
|
||||
primaryButtonProps={{
|
||||
text: "Get started",
|
||||
text: t(Keys.common.getStarted),
|
||||
onClick: () => {
|
||||
useCarousel.getState().setShowCoachMark(false);
|
||||
container.onNewCollectionClicked({ isQuickstart: true });
|
||||
},
|
||||
}}
|
||||
secondaryButtonProps={{
|
||||
text: "Cancel",
|
||||
text: t(Keys.common.cancel),
|
||||
onClick: () => useCarousel.getState().setShowCoachMark(false),
|
||||
}}
|
||||
onDismiss={() => useCarousel.getState().setShowCoachMark(false)}
|
||||
>
|
||||
You will be guided to create a sample container with sample data, then we will give you a tour of data
|
||||
explorer. You can also cancel launching this tour and explore yourself
|
||||
{t(Keys.splashScreen.teachingBubble.coachMark.body)}
|
||||
</TeachingBubbleContent>
|
||||
</Coachmark>
|
||||
)}
|
||||
@@ -945,7 +963,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
fontFamily: '"Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif',
|
||||
}}
|
||||
>
|
||||
Next steps
|
||||
{t(Keys.splashScreen.sections.nextSteps)}
|
||||
</Text>
|
||||
{getNextStepItems()}
|
||||
</Stack>
|
||||
@@ -957,7 +975,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
fontFamily: '"Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif',
|
||||
}}
|
||||
>
|
||||
Tips & learn more
|
||||
{t(Keys.splashScreen.sections.tipsAndLearnMore)}
|
||||
</Text>
|
||||
{getTipsAndLearnMoreItems()}
|
||||
</Stack>
|
||||
@@ -966,15 +984,15 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
) : (
|
||||
<div className={styles.moreStuffContainer}>
|
||||
<div className={styles.moreStuffColumn}>
|
||||
<h2 className={styles.columnTitle}>Recents</h2>
|
||||
<h2 className={styles.columnTitle}>{t(Keys.splashScreen.sections.recents)}</h2>
|
||||
{getRecentItems()}
|
||||
</div>
|
||||
<div className={styles.moreStuffColumn}>
|
||||
<h2 className={styles.columnTitle}>Top 3 things you need to know</h2>
|
||||
<h2 className={styles.columnTitle}>{t(Keys.splashScreen.sections.top3)}</h2>
|
||||
{top3Items()}
|
||||
</div>
|
||||
<div className={styles.moreStuffColumn}>
|
||||
<h2 className={styles.columnTitle}>Learning Resources</h2>
|
||||
<h2 className={styles.columnTitle}>{t(Keys.splashScreen.sections.learningResources)}</h2>
|
||||
{getLearningResourceItems()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,8 @@ import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
|
||||
import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Keys } from "../../Localization/Keys.generated";
|
||||
import { t } from "../../Localization/t";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
@@ -57,7 +59,7 @@ export default class ConflictsTab extends TabsBase {
|
||||
|
||||
private _documentsIterator: MinimalQueryIterator;
|
||||
private _container: Explorer;
|
||||
private _acceptButtonLabel: ko.Observable<string> = ko.observable("Save");
|
||||
private _acceptButtonLabel: ko.Observable<string> = ko.observable(t(Keys.common.save));
|
||||
|
||||
constructor(options: ViewModels.ConflictsTabOptions) {
|
||||
super(options);
|
||||
@@ -213,9 +215,9 @@ export default class ConflictsTab extends TabsBase {
|
||||
this.selectedConflictContent.subscribe((newContent: string) => this._onEditorContentChange(newContent));
|
||||
|
||||
this.conflictOperation.subscribe((newOperationType: string) => {
|
||||
let operationLabel = "Save";
|
||||
let operationLabel = t(Keys.common.save);
|
||||
if (newOperationType === Constants.ConflictOperationType.Replace) {
|
||||
operationLabel = "Update";
|
||||
operationLabel = t(Keys.common.update);
|
||||
}
|
||||
|
||||
this._acceptButtonLabel(operationLabel);
|
||||
@@ -229,7 +231,7 @@ export default class ConflictsTab extends TabsBase {
|
||||
this._documentsIterator = this.createIterator();
|
||||
await this.loadNextPage();
|
||||
} catch (error) {
|
||||
useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error));
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.conflicts.refreshGridFailed), getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,11 +259,11 @@ export default class ConflictsTab extends TabsBase {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkCancelModalDialog(
|
||||
"Unsaved changes",
|
||||
"Changes will be lost. Do you want to continue?",
|
||||
"OK",
|
||||
t(Keys.tabs.conflicts.unsavedChanges),
|
||||
t(Keys.tabs.conflicts.changesWillBeLost),
|
||||
t(Keys.common.ok),
|
||||
async () => await this.resolveConflict(),
|
||||
"Cancel",
|
||||
t(Keys.common.cancel),
|
||||
undefined,
|
||||
);
|
||||
} else {
|
||||
@@ -332,7 +334,7 @@ export default class ConflictsTab extends TabsBase {
|
||||
} catch (error) {
|
||||
this.isExecutionError(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
useDialog.getState().showOkModalDialog("Resolve conflict failed", errorMessage);
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.conflicts.resolveConflictFailed), errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.ResolveConflict,
|
||||
{
|
||||
@@ -386,7 +388,7 @@ export default class ConflictsTab extends TabsBase {
|
||||
} catch (error) {
|
||||
this.isExecutionError(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
useDialog.getState().showOkModalDialog("Delete conflict failed", errorMessage);
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.conflicts.deleteConflictFailed), errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.DeleteConflict,
|
||||
{
|
||||
@@ -617,7 +619,7 @@ export default class ConflictsTab extends TabsBase {
|
||||
}
|
||||
|
||||
if (this.discardButton.visible()) {
|
||||
const label = "Discard";
|
||||
const label = t(Keys.common.discard);
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
@@ -630,7 +632,7 @@ export default class ConflictsTab extends TabsBase {
|
||||
}
|
||||
|
||||
if (this.deleteButton.visible()) {
|
||||
const label = "Delete";
|
||||
const label = t(Keys.common.delete);
|
||||
buttons.push({
|
||||
iconSrc: DeleteIcon,
|
||||
iconAlt: label,
|
||||
|
||||
@@ -41,6 +41,8 @@ import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { QueryConstants } from "Shared/Constants";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
@@ -349,7 +351,7 @@ export const getTabsButtons = ({
|
||||
}
|
||||
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
const label = !isPreferredApiMongoDB ? "New Item" : "New Document";
|
||||
const label = !isPreferredApiMongoDB ? t(Keys.tabs.documents.newItem) : t(Keys.tabs.documents.newDocument);
|
||||
if (getNewDocumentButtonState(editorState).visible) {
|
||||
buttons.push({
|
||||
iconSrc: NewDocumentIcon,
|
||||
@@ -368,7 +370,7 @@ export const getTabsButtons = ({
|
||||
}
|
||||
|
||||
if (getSaveNewDocumentButtonState(editorState).visible) {
|
||||
const label = "Save";
|
||||
const label = t(Keys.common.save);
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
@@ -386,7 +388,7 @@ export const getTabsButtons = ({
|
||||
}
|
||||
|
||||
if (getDiscardNewDocumentChangesButtonState(editorState).visible) {
|
||||
const label = "Discard";
|
||||
const label = t(Keys.common.discard);
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
@@ -403,7 +405,7 @@ export const getTabsButtons = ({
|
||||
}
|
||||
|
||||
if (getSaveExistingDocumentButtonState(editorState).visible) {
|
||||
const label = "Update";
|
||||
const label = t(Keys.common.update);
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
@@ -421,7 +423,7 @@ export const getTabsButtons = ({
|
||||
}
|
||||
|
||||
if (getDiscardExistingDocumentChangesButtonState(editorState).visible) {
|
||||
const label = "Discard";
|
||||
const label = t(Keys.common.discard);
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
@@ -438,7 +440,7 @@ export const getTabsButtons = ({
|
||||
}
|
||||
|
||||
if (selectedRows.size > 0) {
|
||||
const label = "Delete";
|
||||
const label = t(Keys.common.delete);
|
||||
buttons.push({
|
||||
iconSrc: DeleteDocumentIcon,
|
||||
iconAlt: label,
|
||||
@@ -453,7 +455,7 @@ export const getTabsButtons = ({
|
||||
}
|
||||
|
||||
if (!isPreferredApiMongoDB) {
|
||||
const label = "Upload Item";
|
||||
const label = t(Keys.tabs.documents.uploadItem);
|
||||
buttons.push({
|
||||
id: UPLOAD_BUTTON_ID,
|
||||
iconSrc: UploadIcon,
|
||||
@@ -737,17 +739,18 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
} else if (result.statusCode >= 400) {
|
||||
newFailed.push(result.documentId);
|
||||
logConsoleError(
|
||||
`Failed to delete document ${result.documentId.id()} with status code ${result.statusCode}`,
|
||||
t(Keys.tabs.documents.deleteDocumentFailedLog, {
|
||||
documentId: result.documentId.id(),
|
||||
statusCode: result.statusCode,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
logConsoleInfo(`Successfully deleted ${newSuccessful.length} document(s)`);
|
||||
logConsoleInfo(t(Keys.tabs.documents.deleteSuccessLog, { count: newSuccessful.length }));
|
||||
|
||||
if (newThrottled.length > 0) {
|
||||
logConsoleError(
|
||||
`Failed to delete ${newThrottled.length} document(s) due to "Request too large" (429) error. Retrying...`,
|
||||
);
|
||||
logConsoleError(t(Keys.tabs.documents.deleteThrottledLog, { count: newThrottled.length }));
|
||||
}
|
||||
|
||||
// Update result of the bulk delete: method is called again, because the state variables changed
|
||||
@@ -789,7 +792,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
);
|
||||
let partitionKeyProperties = useMemo(() => {
|
||||
return partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) =>
|
||||
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""),
|
||||
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, "").replace(/["]+/g, ""),
|
||||
);
|
||||
}, [partitionKeyPropertyHeaders]);
|
||||
|
||||
@@ -917,11 +920,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkCancelModalDialog(
|
||||
"Unsaved changes",
|
||||
"Your unsaved changes will be lost. Do you want to continue?",
|
||||
"OK",
|
||||
t(Keys.tabs.documents.unsavedChanges),
|
||||
t(Keys.tabs.documents.unsavedChangesMessage),
|
||||
t(Keys.common.ok),
|
||||
onDiscard,
|
||||
"Cancel",
|
||||
t(Keys.common.cancel),
|
||||
onCancelDiscard,
|
||||
);
|
||||
} else {
|
||||
@@ -1011,7 +1014,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
(error) => {
|
||||
onExecutionErrorChange(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
useDialog.getState().showOkModalDialog("Create document failed", errorMessage);
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.createDocumentFailed), errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.CreateDocument,
|
||||
{
|
||||
@@ -1097,7 +1100,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
selectedDocumentId.partitionKeyValue = originalPartitionKeyValue;
|
||||
onExecutionErrorChange(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
useDialog.getState().showOkModalDialog("Update document failed", errorMessage);
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.updateDocumentFailed), errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.UpdateDocument,
|
||||
{
|
||||
@@ -1174,7 +1177,12 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
// Remove the check for systemKey, remove call to deleteNoSqlDocument(). deleteNoSqlDocuments() should
|
||||
// always be called for NoSQL.
|
||||
deletePromise = deleteNoSqlDocument(_collection, toDeleteDocumentIds[0]).then(() => {
|
||||
useDialog.getState().showOkModalDialog("Delete document", "Document successfully deleted.");
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
t(Keys.tabs.documents.deleteDocumentDialogTitle),
|
||||
t(Keys.tabs.documents.documentDeleted),
|
||||
);
|
||||
return [toDeleteDocumentIds[0]];
|
||||
});
|
||||
// ----------------------------------------------------------------------------------------------------
|
||||
@@ -1251,17 +1259,20 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
"Delete documents",
|
||||
`Some documents failed to delete due to a rate limiting error. Please try again later. To prevent this in the future, consider increasing the throughput on your container or database.`,
|
||||
t(Keys.tabs.documents.deleteDocumentsDialogTitle),
|
||||
t(Keys.tabs.documents.throttlingError),
|
||||
{
|
||||
linkText: "Learn More",
|
||||
linkText: t(Keys.common.learnMore),
|
||||
linkUrl: MONGO_THROTTLING_DOC_URL,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog("Delete documents", `Deleting document(s) failed (${error.message})`);
|
||||
.showOkModalDialog(
|
||||
t(Keys.tabs.documents.deleteDocumentsDialogTitle),
|
||||
t(Keys.tabs.documents.deleteFailed, { error: error.message }),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -1275,21 +1286,21 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
const isPlural = selectedRows.size > 1;
|
||||
const documentName = !isPreferredApiMongoDB
|
||||
? isPlural
|
||||
? `the selected ${selectedRows.size} items`
|
||||
: "the selected item"
|
||||
? t(Keys.tabs.documents.selectedItems, { count: selectedRows.size })
|
||||
: t(Keys.tabs.documents.selectedItem)
|
||||
: isPlural
|
||||
? `the selected ${selectedRows.size} documents`
|
||||
: "the selected document";
|
||||
const msg = `Are you sure you want to delete ${documentName}?`;
|
||||
? t(Keys.tabs.documents.selectedDocuments, { count: selectedRows.size })
|
||||
: t(Keys.tabs.documents.selectedDocument);
|
||||
const msg = t(Keys.tabs.documents.confirmDelete, { documentName });
|
||||
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkCancelModalDialog(
|
||||
"Confirm delete",
|
||||
t(Keys.tabs.documents.confirmDeleteTitle),
|
||||
msg,
|
||||
"Delete",
|
||||
t(Keys.common.delete),
|
||||
() => deleteDocuments(Array.from(selectedRows).map((index) => documentIds[index as number])),
|
||||
"Cancel",
|
||||
t(Keys.common.cancel),
|
||||
undefined,
|
||||
);
|
||||
}, [deleteDocuments, documentIds, isPreferredApiMongoDB, selectedRows]);
|
||||
@@ -1470,7 +1481,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
const partitionKey = _partitionKey || (_collection && _collection.partitionKey);
|
||||
const partitionKeyPropertyHeaders = _collection?.partitionKeyPropertyHeaders || partitionKey?.paths;
|
||||
const partitionKeyProperties = partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) =>
|
||||
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""),
|
||||
partitionKeyPropertyHeader
|
||||
.replace(/[/]+/g, ".")
|
||||
.substring(1)
|
||||
.replace(/[']+/g, "")
|
||||
.replace(/["]+/g, ""),
|
||||
);
|
||||
|
||||
return newDocumentId(rawDocument, partitionKeyProperties, partitionKeyValue);
|
||||
@@ -1819,8 +1834,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
|
||||
const partitionKeyProperty = partitionKeyProperties?.[0];
|
||||
if (partitionKeyProperty !== "_id" && !_hasShardKeySpecified(documentContent)) {
|
||||
const message = `The document is lacking the shard property: ${partitionKeyProperty}`;
|
||||
useDialog.getState().showOkModalDialog("Create document failed", message);
|
||||
const message = t(Keys.tabs.documents.missingShardProperty, { partitionKeyProperty });
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.createDocumentFailed), message);
|
||||
onExecutionErrorChange(true);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.CreateDocument,
|
||||
@@ -1831,7 +1846,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
},
|
||||
startKey,
|
||||
);
|
||||
Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab");
|
||||
Logger.logError(t(Keys.tabs.documents.missingShardKeyLog), "MongoDocumentsTab");
|
||||
throw new Error("Document without shard key");
|
||||
}
|
||||
|
||||
@@ -1874,7 +1889,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
(error) => {
|
||||
onExecutionErrorChange(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
useDialog.getState().showOkModalDialog("Create document failed", errorMessage);
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.createDocumentFailed), errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.CreateDocument,
|
||||
{
|
||||
@@ -1945,7 +1960,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
(error) => {
|
||||
onExecutionErrorChange(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
useDialog.getState().showOkModalDialog("Update document failed", errorMessage);
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.updateDocumentFailed), errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.UpdateDocument,
|
||||
{
|
||||
@@ -2054,7 +2069,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error));
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.refreshGridFailed), getErrorMessage(error));
|
||||
}
|
||||
},
|
||||
[createIterator, filterContent],
|
||||
@@ -2066,18 +2081,17 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
* @returns 429 warning message
|
||||
*/
|
||||
const get429WarningMessageNoSql = (): string => {
|
||||
let message = 'Some delete requests failed due to a "Request too large" exception (429)';
|
||||
let message = t(Keys.tabs.documents.requestTooLargeBase);
|
||||
|
||||
if (bulkDeleteOperation.count === bulkDeleteProcess.successfulIds.length) {
|
||||
message += ", but were successfully retried.";
|
||||
message += ", " + t(Keys.tabs.documents.retriedSuccessfully);
|
||||
} else if (bulkDeleteMode === "inProgress" || bulkDeleteMode === "aborting") {
|
||||
message += ". Retrying now.";
|
||||
message += ". " + t(Keys.tabs.documents.retryingNow);
|
||||
} else {
|
||||
message += ".";
|
||||
}
|
||||
|
||||
return (message +=
|
||||
" To prevent this in the future, consider increasing the throughput on your container or database.");
|
||||
return (message += " " + t(Keys.tabs.documents.increaseThroughputTip));
|
||||
};
|
||||
|
||||
const onColumnSelectionChange = (newSelectedColumnIds: string[]): void => {
|
||||
@@ -2124,7 +2138,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
const nonBlankLastFilters = lastFilterContents.filter((filter) => filter.trim() !== "");
|
||||
if (nonBlankLastFilters.length > 0) {
|
||||
options.push({
|
||||
label: "Saved filters",
|
||||
label: t(Keys.tabs.documents.savedFilters),
|
||||
options: nonBlankLastFilters,
|
||||
});
|
||||
}
|
||||
@@ -2153,14 +2167,14 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
dropdownOptions={getFilterChoices()}
|
||||
placeholder={
|
||||
isPreferredApiMongoDB
|
||||
? "Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents."
|
||||
: "Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents."
|
||||
? t(Keys.tabs.documents.mongoFilterPlaceholder)
|
||||
: t(Keys.tabs.documents.sqlFilterPlaceholder)
|
||||
}
|
||||
title="Type a query predicate or choose one from the list."
|
||||
title={t(Keys.tabs.documents.filterTooltip)}
|
||||
value={filterContent}
|
||||
onChange={updateFilterContent}
|
||||
onKeyDown={onFilterKeyDown}
|
||||
bottomLink={{ text: "Learn more", url: DATA_EXPLORER_DOC_URL }}
|
||||
bottomLink={{ text: t(Keys.common.learnMore), url: DATA_EXPLORER_DOC_URL }}
|
||||
/>
|
||||
<Button
|
||||
appearance="primary"
|
||||
@@ -2176,10 +2190,12 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
}
|
||||
}}
|
||||
disabled={isExecuting && isPreferredApiMongoDB}
|
||||
aria-label={!isExecuting || isPreferredApiMongoDB ? "Apply filter" : "Cancel"}
|
||||
aria-label={
|
||||
!isExecuting || isPreferredApiMongoDB ? t(Keys.tabs.documents.applyFilter) : t(Keys.common.cancel)
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
{!isExecuting || isPreferredApiMongoDB ? "Apply Filter" : "Cancel"}
|
||||
{!isExecuting || isPreferredApiMongoDB ? t(Keys.tabs.documents.applyFilter) : t(Keys.common.cancel)}
|
||||
</Button>
|
||||
</div>
|
||||
<Allotment
|
||||
@@ -2223,14 +2239,14 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
</div>
|
||||
{tableContainerSizePx?.width >= calculateOffset(selectedColumnIds.length) + 200 && (
|
||||
<div
|
||||
title="Refresh"
|
||||
title={t(Keys.common.refresh)}
|
||||
className={styles.refreshBtn}
|
||||
role="button"
|
||||
onClick={() => refreshDocumentsGrid(false)}
|
||||
aria-label="Refresh"
|
||||
aria-label={t(Keys.common.refresh)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<img src={RefreshIcon} alt="Refresh" />
|
||||
<img src={RefreshIcon} alt={t(Keys.common.refresh)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -2243,7 +2259,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
onClick={() => loadNextPage(documentsIterator.iterator, false)}
|
||||
onKeyDown={onLoadMoreKeyInput}
|
||||
>
|
||||
Load more
|
||||
{t(Keys.tabs.documents.loadMore)}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
@@ -2255,7 +2271,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
language={"json"}
|
||||
content={selectedDocumentContent}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Document editor"}
|
||||
ariaLabel={t(Keys.tabs.documents.documentEditor)}
|
||||
lineNumbers={"on"}
|
||||
theme={"_theme"}
|
||||
onContentChanged={_onEditorContentChange}
|
||||
@@ -2263,7 +2279,9 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
/>
|
||||
)}
|
||||
{selectedRows.size > 1 && (
|
||||
<span style={{ margin: 10 }}>Number of selected documents: {selectedRows.size}</span>
|
||||
<span style={{ margin: 10 }}>
|
||||
{t(Keys.tabs.documents.numberOfSelectedDocuments, { count: selectedRows.size })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Allotment.Pane>
|
||||
@@ -2272,42 +2290,43 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
{bulkDeleteOperation && (
|
||||
<ProgressModalDialog
|
||||
isOpen={isBulkDeleteDialogOpen}
|
||||
dismissText="Abort"
|
||||
dismissText={t(Keys.tabs.documents.abort)}
|
||||
onDismiss={() => {
|
||||
setIsBulkDeleteDialogOpen(false);
|
||||
setBulkDeleteOperation(undefined);
|
||||
}}
|
||||
onCancel={() => setBulkDeleteMode("aborting")}
|
||||
title={`Deleting ${bulkDeleteOperation.count} document(s)`}
|
||||
message={`Successfully deleted ${bulkDeleteProcess.successfulIds.length} document(s).`}
|
||||
title={t(Keys.tabs.documents.deletingDocuments, { count: bulkDeleteOperation.count })}
|
||||
message={t(Keys.tabs.documents.deletedDocumentsSuccess, { count: bulkDeleteProcess.successfulIds.length })}
|
||||
maxValue={bulkDeleteOperation.count}
|
||||
value={bulkDeleteProcess.successfulIds.length}
|
||||
mode={bulkDeleteMode}
|
||||
>
|
||||
<div className={styles.deleteProgressContent}>
|
||||
{(bulkDeleteMode === "aborting" || bulkDeleteMode === "aborted") && (
|
||||
<div style={{ paddingBottom: tokens.spacingVerticalL }}>Deleting document(s) was aborted.</div>
|
||||
<div style={{ paddingBottom: tokens.spacingVerticalL }}>{t(Keys.tabs.documents.deleteAborted)}</div>
|
||||
)}
|
||||
{(bulkDeleteProcess.failedIds.length > 0 ||
|
||||
(bulkDeleteProcess.throttledIds.length > 0 && bulkDeleteMode !== "inProgress")) && (
|
||||
<MessageBar intent="error" style={{ marginBottom: tokens.spacingVerticalL }}>
|
||||
<MessageBarBody>
|
||||
<MessageBarTitle>Error</MessageBarTitle>
|
||||
Failed to delete{" "}
|
||||
{bulkDeleteMode === "inProgress"
|
||||
? bulkDeleteProcess.failedIds.length
|
||||
: bulkDeleteProcess.failedIds.length + bulkDeleteProcess.throttledIds.length}{" "}
|
||||
document(s).
|
||||
<MessageBarTitle>{t(Keys.tabs.documents.error)}</MessageBarTitle>
|
||||
{t(Keys.tabs.documents.failedToDeleteDocuments, {
|
||||
count:
|
||||
bulkDeleteMode === "inProgress"
|
||||
? bulkDeleteProcess.failedIds.length
|
||||
: bulkDeleteProcess.failedIds.length + bulkDeleteProcess.throttledIds.length,
|
||||
})}
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
)}
|
||||
{bulkDeleteProcess.hasBeenThrottled && (
|
||||
<MessageBar intent="warning">
|
||||
<MessageBarBody>
|
||||
<MessageBarTitle>Warning</MessageBarTitle>
|
||||
<MessageBarTitle>{t(Keys.tabs.documents.warning)}</MessageBarTitle>
|
||||
{get429WarningMessageNoSql()}{" "}
|
||||
<Link href={NO_SQL_THROTTLING_DOC_URL} target="_blank">
|
||||
Learn More
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
|
||||
@@ -44,13 +44,13 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
||||
}
|
||||
onChange={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents."
|
||||
placeholder="Type a query predicate (e.g., WHERE c.id="1"), or choose one from the drop down list, or leave empty to query all documents."
|
||||
title="Type a query predicate or choose one from the list."
|
||||
value=""
|
||||
/>
|
||||
<Button
|
||||
appearance="primary"
|
||||
aria-label="Apply filter"
|
||||
aria-label="Apply Filter"
|
||||
data-test="DocumentsTab/ApplyFilter"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
import { configContext } from "../../../ConfigContext";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../UserContext";
|
||||
@@ -212,7 +214,7 @@ export default class MongoShellTabComponent extends Component<
|
||||
src={this.state.url}
|
||||
id={this.props.tabsBaseInstance.tabId}
|
||||
onLoad={(event) => this.setContentFocus(event)}
|
||||
title="Mongo Shell"
|
||||
title={t(Keys.tabs.mongoShell.title)}
|
||||
role="tabpanel"
|
||||
></iframe>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,8 @@ import { QueryTabStyles, useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles
|
||||
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { QueryConstants } from "Shared/Constants";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
@@ -315,7 +317,9 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
};
|
||||
|
||||
public onSaveQueryClick = (): void => {
|
||||
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />);
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(t(Keys.tabs.query.saveQuery), <SaveQueryPane explorer={this.props.collection.container} />);
|
||||
};
|
||||
|
||||
public launchQueryCopilotChat = (): void => {
|
||||
@@ -325,7 +329,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
public onSavedQueriesClick = (): void => {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this.props.collection.container} />);
|
||||
.openSidePanel(
|
||||
t(Keys.tabs.query.openSavedQueries),
|
||||
<BrowseQueriesPane explorer={this.props.collection.container} />,
|
||||
);
|
||||
};
|
||||
|
||||
public toggleResult(): void {
|
||||
@@ -473,7 +480,8 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
if (this.executeQueryButton.visible) {
|
||||
const label = this.state.selectedContent?.length > 0 ? "Execute Selection" : "Execute Query";
|
||||
const label =
|
||||
this.state.selectedContent?.length > 0 ? t(Keys.tabs.query.executeSelection) : t(Keys.tabs.query.executeQuery);
|
||||
buttons.push({
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: label,
|
||||
@@ -490,7 +498,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
|
||||
if (this.saveQueryButton.visible) {
|
||||
if (configContext.platform !== Platform.Fabric) {
|
||||
const label = "Save Query";
|
||||
const label = t(Keys.tabs.query.saveQuery);
|
||||
buttons.push({
|
||||
iconSrc: SaveQueryIcon,
|
||||
iconAlt: label,
|
||||
@@ -507,11 +515,11 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
|
||||
buttons.push({
|
||||
iconSrc: DownloadQueryIcon,
|
||||
iconAlt: "Download Query",
|
||||
iconAlt: t(Keys.tabs.query.downloadQuery),
|
||||
keyboardAction: KeyboardAction.DOWNLOAD_ITEM,
|
||||
onCommandClick: this.onDownloadQueryClick,
|
||||
commandButtonLabel: "Download Query",
|
||||
ariaLabel: "Download Query",
|
||||
commandButtonLabel: t(Keys.tabs.query.downloadQuery),
|
||||
ariaLabel: t(Keys.tabs.query.downloadQuery),
|
||||
hasPopup: false,
|
||||
disabled: !this.saveQueryButton.enabled,
|
||||
});
|
||||
@@ -568,7 +576,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
// }
|
||||
|
||||
if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) {
|
||||
const label = "Cancel query";
|
||||
const label = t(Keys.tabs.query.cancelQuery);
|
||||
buttons.push({
|
||||
iconSrc: CancelQueryIcon,
|
||||
iconAlt: label,
|
||||
@@ -589,23 +597,23 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
const verticalButton: CommandButtonComponentProps = {
|
||||
isSelected: this.state.queryResultsView === SplitterDirection.Vertical,
|
||||
iconSrc: this.state.queryResultsView === SplitterDirection.Vertical ? CheckIcon : undefined,
|
||||
commandButtonLabel: "Vertical",
|
||||
ariaLabel: "Vertical",
|
||||
commandButtonLabel: t(Keys.tabs.query.vertical),
|
||||
ariaLabel: t(Keys.tabs.query.vertical),
|
||||
onCommandClick: () => this._setViewLayout(SplitterDirection.Vertical),
|
||||
hasPopup: false,
|
||||
};
|
||||
const horizontalButton: CommandButtonComponentProps = {
|
||||
isSelected: this.state.queryResultsView === SplitterDirection.Horizontal,
|
||||
iconSrc: this.state.queryResultsView === SplitterDirection.Horizontal ? CheckIcon : undefined,
|
||||
commandButtonLabel: "Horizontal",
|
||||
ariaLabel: "Horizontal",
|
||||
commandButtonLabel: t(Keys.tabs.query.horizontal),
|
||||
ariaLabel: t(Keys.tabs.query.horizontal),
|
||||
onCommandClick: () => this._setViewLayout(SplitterDirection.Horizontal),
|
||||
hasPopup: false,
|
||||
};
|
||||
|
||||
return {
|
||||
commandButtonLabel: "View",
|
||||
ariaLabel: "View",
|
||||
commandButtonLabel: t(Keys.tabs.query.view),
|
||||
ariaLabel: t(Keys.tabs.query.view),
|
||||
hasPopup: true,
|
||||
children: [verticalButton, horizontalButton],
|
||||
};
|
||||
@@ -782,7 +790,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
modelMarkers={this.state.modelMarkers}
|
||||
isReadOnly={false}
|
||||
wordWrap={"on"}
|
||||
ariaLabel={"Editing Query"}
|
||||
ariaLabel={t(Keys.tabs.query.editingQuery)}
|
||||
lineNumbers={"on"}
|
||||
theme={this.props.monacoTheme}
|
||||
onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
|
||||
|
||||
@@ -11,6 +11,8 @@ import { createStoredProcedure } from "../../../Common/dataAccess/createStoredPr
|
||||
import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProcedure";
|
||||
import { updateStoredProcedure } from "../../../Common/dataAccess/updateStoredProcedure";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
@@ -133,7 +135,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
this.collection = this.props.collection;
|
||||
this.executeResultsEditorId = `executestoredprocedureresults${this.props.scriptTabBaseInstance.tabId}`;
|
||||
this.executeLogsEditorId = `executestoredprocedurelogs${this.props.scriptTabBaseInstance.tabId}`;
|
||||
this.props.scriptTabBaseInstance.ariaLabel("Stored Procedure Body");
|
||||
this.props.scriptTabBaseInstance.ariaLabel(t(Keys.tabs.storedProcedure.body));
|
||||
|
||||
this.props.iStorProcTabComponentAccessor({
|
||||
onExecuteSprocsResultEvent: this.onExecuteSprocsResult.bind(this),
|
||||
@@ -318,7 +320,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
const label = "Save";
|
||||
const label = t(Keys.common.save);
|
||||
if (this.state.saveButton.visible) {
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
@@ -333,7 +335,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
}
|
||||
|
||||
if (this.state.updateButton.visible) {
|
||||
const label = "Update";
|
||||
const label = t(Keys.common.update);
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
@@ -347,7 +349,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
}
|
||||
|
||||
if (this.state.discardButton.visible) {
|
||||
const label = "Discard";
|
||||
const label = t(Keys.common.discard);
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
@@ -361,7 +363,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
}
|
||||
|
||||
if (this.state.executeButton.visible) {
|
||||
const label = "Execute";
|
||||
const label = t(Keys.common.execute);
|
||||
buttons.push({
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: label,
|
||||
@@ -519,7 +521,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
<div className="tab-pane flexContainer stored-procedure-tab" role="tabpanel">
|
||||
<div className="storedTabForm flexContainer">
|
||||
<div className="formTitleFirst">
|
||||
Stored Procedure Id
|
||||
{t(Keys.tabs.storedProcedure.id)}
|
||||
<span className="mandatoryStar" style={{ color: "#ff0707", fontSize: "14px", fontWeight: "bold" }}>
|
||||
*
|
||||
</span>
|
||||
@@ -531,28 +533,28 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
required
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
aria-label="Stored procedure id"
|
||||
placeholder="Enter the new stored procedure id"
|
||||
aria-label={t(Keys.tabs.storedProcedure.idAriaLabel)}
|
||||
placeholder={t(Keys.tabs.storedProcedure.idPlaceholder)}
|
||||
size={40}
|
||||
value={this.state.id}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => this.handleIdOnChange(event)}
|
||||
/>
|
||||
</span>
|
||||
<div className="spUdfTriggerHeader">Stored Procedure Body</div>
|
||||
<div className="spUdfTriggerHeader">{t(Keys.tabs.storedProcedure.body)}</div>
|
||||
<EditorReact
|
||||
language={"javascript"}
|
||||
content={this.state.sProcEditorContent}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Stored procedure body"}
|
||||
ariaLabel={t(Keys.tabs.storedProcedure.bodyAriaLabel)}
|
||||
lineNumbers={"on"}
|
||||
theme={"_theme"}
|
||||
onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
|
||||
/>
|
||||
{this.state.hasResults && (
|
||||
<div className="results-container">
|
||||
<Pivot aria-label="Successful execution of stored procedure" style={{ height: "100%" }}>
|
||||
<Pivot aria-label={t(Keys.tabs.storedProcedure.successfulExecution)} style={{ height: "100%" }}>
|
||||
<PivotItem
|
||||
headerText="Result"
|
||||
headerText={t(Keys.common.result)}
|
||||
headerButtonProps={{
|
||||
"data-order": 1,
|
||||
"data-title": "Result",
|
||||
@@ -563,11 +565,11 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
language={"javascript"}
|
||||
content={this.state.resultData}
|
||||
isReadOnly={true}
|
||||
ariaLabel={"Execute stored procedure result"}
|
||||
ariaLabel={t(Keys.tabs.storedProcedure.resultAriaLabel)}
|
||||
/>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="console.log"
|
||||
headerText={t(Keys.tabs.storedProcedure.consoleLogTab)}
|
||||
headerButtonProps={{
|
||||
"data-order": 2,
|
||||
"data-title": "console.log",
|
||||
@@ -578,7 +580,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
language={"javascript"}
|
||||
content={this.state.logsData}
|
||||
isReadOnly={true}
|
||||
ariaLabel={"Execute stored procedure logs"}
|
||||
ariaLabel={t(Keys.tabs.storedProcedure.logsAriaLabel)}
|
||||
/>
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
@@ -586,16 +588,16 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
)}
|
||||
{this.state.hasErrors && (
|
||||
<div className="errors-container">
|
||||
<div className="errors-header">Errors:</div>
|
||||
<div className="errors-header">{t(Keys.tabs.storedProcedure.errors)}</div>
|
||||
<div className="errorContent">
|
||||
<span className="errorMessage">{this.state.error}</span>
|
||||
<span className="errorDetailsLink">
|
||||
<a
|
||||
aria-label="Error details link"
|
||||
aria-label={t(Keys.tabs.storedProcedure.errorDetailsAriaLabel)}
|
||||
onClick={() => this.onErrorDetailsClick()}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) => this.onErrorDetailsKeyPress(event)}
|
||||
>
|
||||
More details
|
||||
{t(Keys.tabs.storedProcedure.moreDetails)}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,8 @@ import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"
|
||||
import { createTrigger } from "../../Common/dataAccess/createTrigger";
|
||||
import { updateTrigger } from "../../Common/dataAccess/updateTrigger";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Keys } from "../../Localization/Keys.generated";
|
||||
import { t } from "../../Localization/t";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
@@ -19,8 +21,8 @@ import { EditorReact } from "../Controls/Editor/EditorReact";
|
||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import TriggerTab from "./TriggerTab";
|
||||
const triggerTypeOptions: IDropdownOption[] = [
|
||||
{ key: "Pre", text: "Pre" },
|
||||
{ key: "Post", text: "Post" },
|
||||
{ key: "Pre", text: t(Keys.tabs.trigger.pre) },
|
||||
{ key: "Post", text: t(Keys.tabs.trigger.post) },
|
||||
];
|
||||
|
||||
const dropdownStyles: Partial<IDropdownStyles> = {
|
||||
@@ -147,10 +149,10 @@ const dropdownStyles: Partial<IDropdownStyles> = {
|
||||
};
|
||||
|
||||
const triggerOperationOptions: IDropdownOption[] = [
|
||||
{ key: "All", text: "All" },
|
||||
{ key: "Create", text: "Create" },
|
||||
{ key: "Delete", text: "Delete" },
|
||||
{ key: "Replace", text: "Replace" },
|
||||
{ key: "All", text: t(Keys.tabs.trigger.all) },
|
||||
{ key: "Create", text: t(Keys.tabs.trigger.operationCreate) },
|
||||
{ key: "Delete", text: t(Keys.tabs.trigger.operationDelete) },
|
||||
{ key: "Replace", text: t(Keys.tabs.trigger.operationReplace) },
|
||||
];
|
||||
|
||||
interface Ibutton {
|
||||
@@ -334,7 +336,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
const label = "Save";
|
||||
const label = t(Keys.common.save);
|
||||
if (this.saveButton.visible) {
|
||||
buttons.push({
|
||||
setState: this.setState,
|
||||
@@ -351,7 +353,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
}
|
||||
|
||||
if (this.updateButton.visible) {
|
||||
const label = "Update";
|
||||
const label = t(Keys.common.update);
|
||||
buttons.push({
|
||||
...this,
|
||||
iconSrc: SaveIcon,
|
||||
@@ -366,7 +368,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
}
|
||||
|
||||
if (this.discardButton.visible) {
|
||||
const label = "Discard";
|
||||
const label = t(Keys.common.discard);
|
||||
buttons.push({
|
||||
setState: this.setState,
|
||||
...this,
|
||||
@@ -415,14 +417,14 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
<div className="tab-pane flexContainer trigger-form" role="tabpanel">
|
||||
<TextField
|
||||
className="trigger-field"
|
||||
label="Trigger Id"
|
||||
label={t(Keys.tabs.trigger.id)}
|
||||
id="entityTimeId"
|
||||
autoFocus
|
||||
required
|
||||
type="text"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder="Enter the new trigger id"
|
||||
placeholder={t(Keys.tabs.trigger.idPlaceholder)}
|
||||
size={40}
|
||||
value={triggerId}
|
||||
readOnly={!isIdEditable}
|
||||
@@ -446,8 +448,8 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
}}
|
||||
/>
|
||||
<Dropdown
|
||||
placeholder="Trigger Type"
|
||||
label="Trigger Type"
|
||||
placeholder={t(Keys.tabs.trigger.type)}
|
||||
label={t(Keys.tabs.trigger.type)}
|
||||
options={triggerTypeOptions}
|
||||
selectedKey={triggerType}
|
||||
className="trigger-field"
|
||||
@@ -455,8 +457,8 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
<Dropdown
|
||||
placeholder="Trigger Operation"
|
||||
label="Trigger Operation"
|
||||
placeholder={t(Keys.tabs.trigger.operation)}
|
||||
label={t(Keys.tabs.trigger.operation)}
|
||||
selectedKey={triggerOperation}
|
||||
options={triggerOperationOptions}
|
||||
className="trigger-field"
|
||||
@@ -465,12 +467,12 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
}
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
<Label className="trigger-field">Trigger Body</Label>
|
||||
<Label className="trigger-field">{t(Keys.tabs.trigger.body)}</Label>
|
||||
<EditorReact
|
||||
language={"json"}
|
||||
content={triggerBody}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Graph JSON"}
|
||||
ariaLabel={t(Keys.tabs.trigger.bodyAriaLabel)}
|
||||
onContentChanged={this.handleTriggerBodyChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,8 @@ import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"
|
||||
import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
|
||||
import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Keys } from "../../Localization/Keys.generated";
|
||||
import { t } from "../../Localization/t";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
@@ -82,7 +84,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
const label = "Save";
|
||||
const label = t(Keys.common.save);
|
||||
if (this.saveButton.visible) {
|
||||
buttons.push({
|
||||
...this,
|
||||
@@ -99,7 +101,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
}
|
||||
|
||||
if (this.updateButton.visible) {
|
||||
const label = "Update";
|
||||
const label = t(Keys.common.update);
|
||||
buttons.push({
|
||||
...this,
|
||||
iconSrc: SaveIcon,
|
||||
@@ -114,7 +116,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
}
|
||||
|
||||
if (this.discardButton.visible) {
|
||||
const label = "Discard";
|
||||
const label = t(Keys.common.discard);
|
||||
buttons.push({
|
||||
setState: this.setState,
|
||||
...this,
|
||||
@@ -265,7 +267,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
<FluentProvider theme={currentTheme}>
|
||||
<TextField
|
||||
className="trigger-field"
|
||||
label="User Defined Function Id"
|
||||
label={t(Keys.tabs.udf.id)}
|
||||
id="entityTimeId"
|
||||
autoFocus
|
||||
required
|
||||
@@ -273,7 +275,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
type="text"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder="Enter the new user defined function id"
|
||||
placeholder={t(Keys.tabs.udf.idPlaceholder)}
|
||||
size={40}
|
||||
value={udfId}
|
||||
onChange={this.handleUdfIdChange}
|
||||
@@ -299,12 +301,12 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
}}
|
||||
/>{" "}
|
||||
</FluentProvider>
|
||||
<Label className="trigger-field">User Defined Function Body</Label>
|
||||
<Label className="trigger-field">{t(Keys.tabs.udf.body)}</Label>
|
||||
<EditorReact
|
||||
language={"javascript"}
|
||||
content={udfBody}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"User defined function body"}
|
||||
ariaLabel={t(Keys.tabs.udf.bodyAriaLabel)}
|
||||
onContentChanged={this.handleUdfBodyChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user