Compare commits

..

11 Commits

Author SHA1 Message Date
Sindhu Balasubramanian
79dbdbbe7f Merge branch 'users/sindhuba/playwrit-tests' of https://github.com/Azure/cosmos-explorer into users/sindhuba/sharedThroughput 2026-01-13 11:34:54 -08:00
Sindhu Balasubramanian
3f977df00d Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2026-01-13 10:25:55 -08:00
Sindhu Balasubramanian
865e9c906b Run npm format 2026-01-08 13:16:22 -08:00
Sindhu Balasubramanian
1c34425dd8 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2026-01-08 13:00:47 -08:00
Sindhu Balasubramanian
50a244e6f9 Add Mongo Pagination tests 2026-01-08 12:55:24 -08:00
Sindhu Balasubramanian
9dad75c2f9 Remove localhost 2025-12-30 23:21:56 -08:00
Sindhu Balasubramanian
876b531248 Add changes for Load more option to work 2025-12-30 22:54:42 -08:00
Sindhu Balasubramanian
28fe5846b3 Fix error with adding container to shared db test 2025-12-22 11:22:36 -08:00
Sindhu Balasubramanian
f8533abb64 Run npm format 2025-12-22 07:08:30 -08:00
Sindhu Balasubramanian
2921294a3d Run npm commands 2025-12-22 06:58:56 -08:00
Sindhu Balasubramanian
a03c289da0 Add sharedThroughput db tests 2025-12-21 22:03:12 -08:00
167 changed files with 13212 additions and 14473 deletions

View File

@@ -201,18 +201,18 @@ jobs:
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
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']}}

View File

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

1
.gitignore vendored
View File

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

14305
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"description": "Cosmos Explorer",
"main": "index.js",
"dependencies": {
"@azure/arm-cosmosdb": "16.4.0",
"@azure/arm-cosmosdb": "9.1.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.6.8",
"@jupyterlab/terminal": "3.0.3",
"@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.2.12",
"@nteract/data-explorer": "8.0.3",
"@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.4.12",
"@nteract/presentational-components": "3.0.7",
"@nteract/stateful-components": "1.7.0",
"@nteract/styles": "2.0.2",
"@nteract/transform-geojson": "5.1.8",
@@ -39,26 +39,25 @@
"@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.6.13",
"@types/node-fetch": "2.5.7",
"@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": "3.2.1",
"canvas": "2.11.2",
"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.9.0",
"d3": "7.8.5",
"datatables.net-colreorder-dt": "1.7.0",
"datatables.net-dt": "1.13.8",
"date-fns": "1.29.0",
@@ -71,8 +70,7 @@
"html2canvas": "1.0.0-rc.5",
"i18next": "23.11.5",
"i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "3.0.2",
"i18next-resources-to-backend": "1.2.1",
"i18next-http-backend": "1.0.23",
"iframe-resizer-react": "1.1.0",
"immer": "9.0.6",
"immutable": "4.0.0-rc.12",
@@ -81,15 +79,12 @@
"jquery-typeahead": "2.11.1",
"jquery-ui-dist": "1.13.2",
"knockout": "3.5.1",
"lodash": "4.17.23",
"lodash-es": "4.17.23",
"min-document": "2.19.1",
"loader-utils": "2.0.3",
"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.1",
"patch-package": "8.0.0",
"plotly.js-cartesian-dist-min": "1.52.3",
"post-robot": "10.0.42",
"q": "1.5.1",
@@ -108,6 +103,7 @@
"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",
@@ -115,30 +111,16 @@
"tinykeys": "2.1.0",
"underscore": "1.12.1",
"utility-types": "3.10.0",
"uuid": "9.0.0",
"web-vitals": "4.2.4",
"ws": "8.17.1",
"uuid": "9.0.0",
"zustand": "3.5.0"
},
"overrides": {
"d3-color": "3.1.0",
"cross-spawn": "7.0.6",
"less-vars-loader": {
"json5": "1.0.2"
},
"trim": "0.0.3",
"@octokit/plugin-paginate-rest": "9.2.2",
"@octokit/request-error": "5.1.1",
"@octokit/request": "8.4.1",
"prismjs": "1.30.0",
"sanitize-html": "2.17.0"
},
"devDependencies": {
"@babel/core": "7.29.0",
"@babel/core": "7.24.7",
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "7.24.7",
"@babel/preset-typescript": "7.24.7",
"@playwright/test": "1.55.1",
"@playwright/test": "1.49.1",
"@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56",
@@ -152,7 +134,7 @@
"@types/hasher": "0.0.31",
"@types/jest": "29.5.12",
"@types/jquery": "3.5.29",
"@types/node": "18.19.0",
"@types/node": "12.11.1",
"@types/post-robot": "10.0.1",
"@types/q": "1.5.1",
"@types/react": "17.0.44",
@@ -163,7 +145,7 @@
"@types/react-window": "1.8.8",
"@types/sanitize-html": "1.27.2",
"@types/sinon": "2.3.3",
"@types/styled-components": "5.1.32",
"@types/styled-components": "5.1.1",
"@types/underscore": "1.7.36",
"@types/youtube-player": "5.5.6",
"@typescript-eslint/eslint-plugin": "6.7.4",
@@ -171,7 +153,6 @@
"@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",
@@ -189,7 +170,6 @@
"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",
@@ -197,8 +177,7 @@
"jest-html-loader": "1.0.0",
"jest-react-hooks-shallow": "1.5.1",
"jest-trx-results-processor": "3.0.2",
"js-yaml": "3.14.2",
"less": "4.5.1",
"less": "3.8.1",
"less-loader": "11.1.3",
"less-vars-loader": "1.1.0",
"mini-css-extract-plugin": "2.1.0",
@@ -216,17 +195,14 @@
"typedoc": "0.26.2",
"typescript": "4.9.5",
"url-loader": "4.1.1",
"values-to-keys": "1.1.0",
"wait-on": "9.0.3",
"webpack": "5.104.1",
"webpack-bundle-analyzer": "5.2.0",
"wait-on": "4.0.2",
"webpack": "5.88.2",
"webpack-bundle-analyzer": "4.9.1",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.2.3",
"ws": "8.17.1"
"webpack-dev-server": "4.15.2"
},
"scripts": {
"postinstall": "patch-package && npm run generate:i18n-keys",
"prestart": "npm run generate:i18n-keys",
"postinstall": "patch-package",
"start": "webpack serve --mode development",
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
"build:dataExplorer:ci": "npm run build:ci",
@@ -253,7 +229,6 @@
"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": {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
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";
@@ -9,36 +7,19 @@ import { HttpStatusCodes } from "./Constants";
import { logError } from "./Logger";
import { sendMessage } from "./MessageHandler";
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 => {
export const handleError = (error: string | ARMError | Error, area: string, consoleErrorPrefix?: string): void => {
const errorMessage = getErrorMessage(error);
const errorCode = error instanceof ARMError ? error.code : undefined;
// logs error to data explorer console (always shows original, non-redacted message)
// logs error to data explorer console
const consoleErrorMessage = consoleErrorPrefix ? `${consoleErrorPrefix}:\n ${errorMessage}` : errorMessage;
logConsoleError(consoleErrorMessage);
// 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);
// logs error to both app insight and kusto
logError(errorMessage, 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 => {

View File

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

View File

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

View File

@@ -4,51 +4,6 @@ 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,
@@ -63,12 +18,7 @@ export const queryDocumentsPage = async (
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
return result;
} catch (error) {
// 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,
});
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`);
throw error;
} finally {
clearMessage();

View File

@@ -77,12 +77,6 @@ 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/",

View File

@@ -46,10 +46,6 @@ 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";

View File

@@ -275,7 +275,8 @@ export interface DataMaskingPolicy {
startPosition: number;
length: number;
}>;
excludedPaths?: string[];
excludedPaths: string[];
isPolicyEnabled: boolean;
}
export interface MaterializedView {

View File

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

View File

@@ -395,14 +395,6 @@ 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);

View File

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

View File

@@ -1,4 +1,3 @@
/* eslint-disable jest/no-conditional-expect */
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
import React from "react";
@@ -6,20 +5,6 @@ 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: {
@@ -33,11 +18,6 @@ jest.mock("../../ContainerCopyMessages", () => ({
cancel: "Cancel",
complete: "Complete",
},
dialog: {
heading: "Confirm Action",
confirmButtonText: "Confirm",
cancelButtonText: "Cancel",
},
},
},
}));
@@ -70,9 +50,6 @@ describe("CopyJobActionMenu", () => {
beforeEach(() => {
jest.clearAllMocks();
mockShowOkCancelModalDialog.mockClear();
mockCloseDialog.mockClear();
mockOpenDialog.mockClear();
});
describe("Component Rendering", () => {
@@ -289,29 +266,7 @@ describe("CopyJobActionMenu", () => {
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.pause, expect.any(Function));
});
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", () => {
it("should call handleClick when cancel action is clicked", () => {
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
@@ -322,9 +277,6 @@ 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));
});
@@ -342,33 +294,7 @@ describe("CopyJobActionMenu", () => {
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.resume, expect.any(Function));
});
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", () => {
it("should call handleClick when complete action is clicked", () => {
const job = createMockJob({
Status: CopyJobStatusType.InProgress,
Mode: CopyJobMigrationType.Online,
@@ -382,87 +308,10 @@ 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;
@@ -490,13 +339,8 @@ describe("CopyJobActionMenu", () => {
const pauseButton = screen.getByText("Pause");
fireEvent.click(pauseButton);
fireEvent.click(actionButton);
const pauseButtonAfterClick = screen.getByText("Pause").closest("button");
const pauseButtonAfterClick = screen.getByText("Pause");
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", () => {
@@ -516,7 +360,23 @@ describe("CopyJobActionMenu", () => {
expect(screen.getByText("Cancel")).toBeInTheDocument();
});
it("should disable complete action when job is being updated", () => {
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", () => {
const job = createMockJob({
Status: CopyJobStatusType.InProgress,
Mode: CopyJobMigrationType.Online,
@@ -530,34 +390,8 @@ 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);
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");
expect(screen.getByText("Complete")).toBeInTheDocument();
});
});
@@ -628,7 +462,6 @@ 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();
@@ -775,129 +608,4 @@ 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();
});
});
});

View File

@@ -1,6 +1,5 @@
import { DirectionalHint, IconButton, IContextualMenuProps, Stack } from "@fluentui/react";
import { IconButton, IContextualMenuProps } 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";
@@ -10,28 +9,6 @@ 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 (
@@ -45,22 +22,9 @@ 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 = [
{
@@ -68,21 +32,21 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
text: ContainerCopyMessages.MonitorJobs.Actions.pause,
iconProps: { iconName: "Pause" },
onClick: () => handleClick(job, CopyJobActions.pause, setUpdatingJobAction),
disabled: isThisJobUpdating,
disabled: isThisJobUpdating && updatingAction === CopyJobActions.pause,
},
{
key: CopyJobActions.cancel,
text: ContainerCopyMessages.MonitorJobs.Actions.cancel,
iconProps: { iconName: "Cancel" },
onClick: () => showActionConfirmationDialog(job, CopyJobActions.cancel),
disabled: isThisJobUpdating,
onClick: () => handleClick(job, CopyJobActions.cancel, setUpdatingJobAction),
disabled: isThisJobUpdating && updatingAction === CopyJobActions.cancel,
},
{
key: CopyJobActions.resume,
text: ContainerCopyMessages.MonitorJobs.Actions.resume,
iconProps: { iconName: "Play" },
onClick: () => handleClick(job, CopyJobActions.resume, setUpdatingJobAction),
disabled: isThisJobUpdating,
disabled: isThisJobUpdating && updatingAction === CopyJobActions.resume,
},
];
@@ -103,8 +67,8 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
key: CopyJobActions.complete,
text: ContainerCopyMessages.MonitorJobs.Actions.complete,
iconProps: { iconName: "CheckMark" },
onClick: () => showActionConfirmationDialog(job, CopyJobActions.complete),
disabled: isThisJobUpdating,
onClick: () => handleClick(job, CopyJobActions.complete, setUpdatingJobAction),
disabled: isThisJobUpdating && updatingAction === CopyJobActions.complete,
});
}
return filteredItems;
@@ -122,8 +86,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(), directionalHint: DirectionalHint.leftTopEdge, directionalHintFixed: false }}
menuIconProps={{ iconName: "", className: "hidden" }}
menuProps={{ items: getMenuItems() }}
menuIconProps={{ iconName: "" }}
ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions}
title={ContainerCopyMessages.MonitorJobs.Columns.actions}
/>

View File

@@ -11,17 +11,9 @@ 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: "Job name",
name: "Name",
fieldName: "Name",
minWidth: 140,
maxWidth: 300,
@@ -173,165 +165,6 @@ 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", () => {
@@ -509,7 +342,7 @@ describe("CopyJobsList", () => {
describe("Component Props", () => {
it("uses default page size when not provided", () => {
const manyJobs: CopyJobType[] = Array.from({ length: 20 }, (_, i) => ({
const manyJobs: CopyJobType[] = Array.from({ length: 12 }, (_, i) => ({
...mockJobs[0],
ID: `job-${i + 1}`,
Name: `Test Job ${i + 1}`,
@@ -518,7 +351,7 @@ describe("CopyJobsList", () => {
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} />);
expect(screen.getByLabelText("Go to next page")).toBeInTheDocument();
expect(screen.getByText("Showing 1 - 15 of 20 items")).toBeInTheDocument();
expect(screen.getByText("Showing 1 - 10 of 12 items")).toBeInTheDocument();
});
it("passes correct props to getColumns function", async () => {
@@ -607,33 +440,7 @@ describe("CopyJobsList", () => {
render(<CopyJobsList jobs={largeJobsList} handleActionClick={mockHandleActionClick} />);
}).not.toThrow();
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();
});
expect(screen.getByText("Showing 1 - 10 of 1000 items")).toBeInTheDocument();
});
});
});

View File

@@ -12,9 +12,8 @@ import {
Stack,
Sticky,
StickyPositionType,
TextField,
} from "@fluentui/react";
import React, { useEffect, useMemo } from "react";
import React, { useEffect } from "react";
import Pager from "../../../../Common/Pager";
import { useThemeStore } from "../../../../hooks/useTheme";
import { getThemeTokens } from "../../../Theme/ThemeUtil";
@@ -31,15 +30,9 @@ 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 = 15;
// Columns to search across
const searchableFields = ["Name", "Status", "LastUpdatedTime", "Mode"];
const PAGE_SIZE = 10;
const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => {
const isDarkMode = useThemeStore((state) => state.isDarkMode);
@@ -48,23 +41,6 @@ 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);
@@ -88,15 +64,7 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
setStartIndex(0);
};
const sortableColumns: IColumn[] = getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending);
const handleFilterTextChange = (
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string,
) => {
setFilterText(newValue || "");
setStartIndex(0);
};
const columns: IColumn[] = getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending);
const _handleRowClick = (job: CopyJobType) => {
openCopyJobDetailsPanel(job);
@@ -113,25 +81,14 @@ 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={sortableColumns}
items={filteredJobs.slice(startIndex, startIndex + pageSize)}
columns={columns}
items={sortedJobs.slice(startIndex, startIndex + pageSize)}
enableShimmer={false}
constrainMode={ConstrainMode.unconstrained}
layoutMode={DetailsListLayoutMode.justified}
@@ -160,12 +117,12 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
/>
</ScrollablePane>
</Stack.Item>
{filteredJobs.length > pageSize && (
{sortedJobs.length > pageSize && (
<Stack.Item>
<Pager
disabled={false}
startIndex={startIndex}
totalCount={filteredJobs.length}
totalCount={sortedJobs.length}
pageSize={pageSize}
onLoadPage={(startIdx /* pageSize */) => {
setStartIndex(startIdx);

View File

@@ -1,27 +1,5 @@
@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);
@@ -141,8 +119,25 @@
filter: invert(1);
}
.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);
}
}
.migrationTypeDescription {
p {
color: var(--colorNeutralForeground1);
@@ -178,11 +173,6 @@
width: 100%;
max-width: 100%;
margin: 0 auto;
body.isDarkMode & {
.themedTextFieldStyles();
}
.ms-DetailsList {
width: 100%;

View File

@@ -7,9 +7,7 @@ import {
AddGlobalSecondaryIndexPanelProps,
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
import { useDatabases } from "Explorer/useDatabases";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import { isFabric, isFabricNative, openRestoreContainerDialog } from "Platform/Fabric/FabricUtil";
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { ReactTabKind, useTabs } from "hooks/useTabs";
@@ -26,7 +24,6 @@ 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";
@@ -59,21 +56,10 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
{
iconSrc: AddCollectionIcon,
onClick: () => container.onNewCollectionClicked({ databaseId }),
label: t(Keys.contextMenu.newContainer, { containerName: getCollectionName() }),
label: `New ${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,
@@ -82,11 +68,11 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
useSidePanel
.getState()
.openSidePanel(
t(Keys.contextMenu.deleteDatabase, { databaseName: getDatabaseName() }),
"Delete " + getDatabaseName(),
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
);
},
label: t(Keys.contextMenu.deleteDatabase, { databaseName: getDatabaseName() }),
label: `Delete ${getDatabaseName()}`,
styleClass: "deleteDatabaseMenuItem",
});
}
@@ -102,7 +88,7 @@ export const createCollectionContextMenuButton = (
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, undefined),
label: t(Keys.contextMenu.newSqlQuery),
label: "New SQL Query",
});
}
@@ -110,7 +96,7 @@ export const createCollectionContextMenuButton = (
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, undefined),
label: t(Keys.contextMenu.newQuery),
label: "New Query",
});
items.push({
@@ -125,8 +111,8 @@ export const createCollectionContextMenuButton = (
},
label:
useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell
? t(Keys.contextMenu.openMongoShell)
: t(Keys.contextMenu.newShell),
? "Open Mongo Shell"
: "New Shell",
});
}
@@ -139,7 +125,7 @@ export const createCollectionContextMenuButton = (
onClick: () => {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
},
label: t(Keys.contextMenu.openCassandraShell),
label: "Open Cassandra Shell",
});
}
@@ -152,7 +138,7 @@ export const createCollectionContextMenuButton = (
onClick: () => {
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
},
label: t(Keys.contextMenu.newStoredProcedure),
label: "New Stored Procedure",
});
items.push({
@@ -160,7 +146,7 @@ export const createCollectionContextMenuButton = (
onClick: () => {
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
},
label: t(Keys.contextMenu.newUdf),
label: "New UDF",
});
items.push({
@@ -168,7 +154,7 @@ export const createCollectionContextMenuButton = (
onClick: () => {
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
},
label: t(Keys.contextMenu.newTrigger),
label: "New Trigger",
});
}
@@ -181,11 +167,11 @@ export const createCollectionContextMenuButton = (
useSidePanel
.getState()
.openSidePanel(
t(Keys.contextMenu.deleteContainer, { containerName: getCollectionName() }),
"Delete " + getCollectionName(),
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
);
},
label: t(Keys.contextMenu.deleteContainer, { containerName: getCollectionName() }),
label: `Delete ${getCollectionName()}`,
styleClass: "deleteCollectionMenuItem",
});
}
@@ -222,14 +208,14 @@ export const createSampleCollectionContextMenuButton = (): TreeNodeMenuItem[] =>
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
},
label: t(Keys.contextMenu.newSqlQuery),
label: "New SQL Query",
});
} else if (copilotVersion === "v2.0") {
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => sampleCollection && sampleCollection.onNewQueryClick(sampleCollection, undefined),
label: t(Keys.contextMenu.newSqlQuery),
label: "New SQL Query",
});
}
}
@@ -249,7 +235,7 @@ export const createStoreProcedureContextMenuItems = (
{
iconSrc: DeleteSprocIcon,
onClick: () => storedProcedure.delete(),
label: t(Keys.contextMenu.deleteStoredProcedure),
label: "Delete Stored Procedure",
},
];
};
@@ -263,7 +249,7 @@ export const createTriggerContextMenuItems = (container: Explorer, trigger: Trig
{
iconSrc: DeleteTriggerIcon,
onClick: () => trigger.delete(),
label: t(Keys.contextMenu.deleteTrigger),
label: "Delete Trigger",
},
];
};
@@ -280,7 +266,7 @@ export const createUserDefinedFunctionContextMenuItems = (
{
iconSrc: DeleteUDFIcon,
onClick: () => userDefinedFunction.delete(),
label: t(Keys.contextMenu.deleteUdf),
label: "Delete User Defined Function",
},
];
};

View File

@@ -17,8 +17,6 @@ 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;
@@ -90,7 +88,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
isModal: true,
title,
subText,
primaryButtonText: t(Keys.common.close),
primaryButtonText: "Close",
secondaryButtonText: undefined,
onPrimaryButtonClick: () => {
get().closeDialog();

View File

@@ -30,6 +30,7 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
dataMaskingPolicy: {
includedPaths: [],
excludedPaths: ["/excludedPath"],
isPolicyEnabled: true,
},
indexes: [],
}),
@@ -306,10 +307,12 @@ describe("SettingsComponent", () => {
dataMaskingContent: {
includedPaths: [],
excludedPaths: ["/excludedPath"],
isPolicyEnabled: true,
},
dataMaskingContentBaseline: {
includedPaths: [],
excludedPaths: [],
isPolicyEnabled: false,
},
isDataMaskingDirty: true,
});
@@ -323,6 +326,7 @@ describe("SettingsComponent", () => {
expect(wrapper.state("dataMaskingContentBaseline")).toEqual({
includedPaths: [],
excludedPaths: ["/excludedPath"],
isPolicyEnabled: true,
});
});
@@ -336,6 +340,7 @@ 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);
@@ -344,6 +349,7 @@ describe("SettingsComponent", () => {
expect(wrapper.state("dataMaskingContent")).toEqual({
includedPaths: "invalid",
excludedPaths: [],
isPolicyEnabled: false,
});
expect(wrapper.state("dataMaskingValidationErrors")).toEqual(["includedPaths must be an array"]);
@@ -358,6 +364,7 @@ describe("SettingsComponent", () => {
},
],
excludedPaths: ["/excludedPath"],
isPolicyEnabled: true,
};
settingsComponentInstance["onDataMaskingContentChange"](validPolicy);
@@ -381,6 +388,7 @@ describe("SettingsComponent", () => {
},
],
excludedPaths: ["/excludedPath1"],
isPolicyEnabled: false,
};
const modifiedPolicy = {
@@ -393,6 +401,7 @@ describe("SettingsComponent", () => {
},
],
excludedPaths: ["/excludedPath2"],
isPolicyEnabled: true,
};
// Set initial state

View File

@@ -16,7 +16,7 @@ import {
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
import { useDatabases } from "Explorer/useDatabases";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isCapabilityEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg";
@@ -44,8 +44,6 @@ 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,
@@ -72,7 +70,6 @@ import {
getMongoNotification,
getTabTitle,
hasDatabaseSharedThroughput,
isDataMaskingEnabled,
isDirty,
parseConflictResolutionMode,
parseConflictResolutionProcedure,
@@ -689,14 +686,22 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.setState({ isComputedPropertiesDirty: isComputedPropertiesDirty });
private onDataMaskingContentChange = (newDataMasking: DataModels.DataMaskingPolicy): void => {
const validationErrors = [];
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 (!newDataMasking.excludedPaths) {
newDataMasking.excludedPaths = [];
}
if (newDataMasking.excludedPaths !== undefined && !Array.isArray(newDataMasking.excludedPaths)) {
validationErrors.push(t(Keys.controls.settings.dataMasking.excludedPathsMustBeArray));
if (!newDataMasking.includedPaths) {
newDataMasking.includedPaths = [];
}
const validationErrors = [];
if (!Array.isArray(newDataMasking.includedPaths)) {
validationErrors.push("includedPaths must be an array");
}
if (!Array.isArray(newDataMasking.excludedPaths)) {
validationErrors.push("excludedPaths must be an array");
}
if (typeof newDataMasking.isPolicyEnabled !== "boolean") {
validationErrors.push("isPolicyEnabled must be a boolean");
}
this.setState({
@@ -837,6 +842,7 @@ 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();
@@ -898,7 +904,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const buttons: CommandButtonComponentProps[] = [];
const isExecuting = this.props.settingsTab.isExecuting();
if (this.saveSettingsButton.isVisible()) {
const label = t(Keys.common.save);
const label = "Save";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
@@ -911,7 +917,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}
if (this.discardSettingsChangesButton.isVisible()) {
const label = t(Keys.common.discard);
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
@@ -936,10 +942,9 @@ 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 = t(Keys.controls.settings.throughput.throughputCapError, {
throughputCap: String(throughputCap),
newTotalThroughput: String(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.`;
}
this.setState({ autoPilotThroughput: newThroughput, throughputError });
};
@@ -950,10 +955,9 @@ 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 = t(Keys.controls.settings.throughput.throughputCapError, {
throughputCap: String(throughputCap),
newTotalThroughput: String(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.`;
}
this.setState({ throughput: newThroughput, throughputError });
};
@@ -1069,8 +1073,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
newCollection.fullTextPolicy = this.state.fullTextPolicy;
// Only send data masking policy if it was modified (dirty) and data masking is enabled
if (this.state.isDataMaskingDirty && isDataMaskingEnabled(this.collection.dataMaskingPolicy?.())) {
// Only send data masking policy if it was modified (dirty)
if (this.state.isDataMaskingDirty && isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking)) {
newCollection.dataMaskingPolicy = this.state.dataMaskingContent;
}
@@ -1459,7 +1463,15 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
});
}
if (isDataMaskingEnabled(this.collection.dataMaskingPolicy?.())) {
// 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()) {
const dataMaskingComponentProps: DataMaskingComponentProps = {
shouldDiscardDataMasking: this.state.shouldDiscardDataMasking,
resetShouldDiscardDataMasking: this.resetShouldDiscardDataMasking,
@@ -1564,7 +1576,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}
>
{this.shouldShowKeyspaceSharedThroughputMessage() && (
<div>{t(Keys.controls.settings.scale.keyspaceSharedThroughput)}</div>
<div>This table shared throughput is configured at the keyspace</div>
)}
<div

View File

@@ -25,8 +25,6 @@ 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,
@@ -340,12 +338,10 @@ export const getEstimatedSpendingElement = (
const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : "";
return (
<Stack>
<Text style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.costEstimate.title)}
</Text>
<Text style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>Cost estimate*</Text>
{costElement}
<Text style={{ fontWeight: 600, marginTop: 15, color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.costEstimate.howWeCalculate)}
How we calculate this
</Text>
<Stack id="throughputSpendElement" style={{ marginTop: 5 }}>
<span>
@@ -357,8 +353,7 @@ export const getEstimatedSpendingElement = (
</span>
<span>
{priceBreakdown.currencySign}
{priceBreakdown.pricePerRu}
{t(Keys.controls.settings.costEstimate.perRu)}
{priceBreakdown.pricePerRu}/RU
</span>
</Stack>
<Text style={{ marginTop: 15, color: "var(--colorNeutralForeground1)" }}>
@@ -370,16 +365,18 @@ export const getEstimatedSpendingElement = (
export const manualToAutoscaleDisclaimerElement: JSX.Element = (
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
{t(Keys.controls.settings.throughput.manualToAutoscaleDisclaimer)}{" "}
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.{" "}
<Link href={Urls.autoscaleMigration}>Learn more</Link>
</Text>
);
export const ttlWarning: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
{t(Keys.controls.settings.throughput.ttlWarningText)}{" "}
The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete
operation explicitly issued by a client application. For more information see,{" "}
<Link target="_blank" href="https://aka.ms/cosmos-db-ttl">
{t(Keys.controls.settings.throughput.ttlWarningLinkText)}
Time to Live (TTL) in Azure Cosmos DB
</Link>
.
</Text>
@@ -387,28 +384,29 @@ export const ttlWarning: JSX.Element = (
export const unsavedEditorWarningMessage = (editor: editorType): JSX.Element => (
<Text styles={infoAndToolTipTextStyle}>
{t(Keys.controls.settings.throughput.unsavedEditorWarningPrefix)}{" "}
You have not saved the latest changes made to your{" "}
{editor === "indexPolicy"
? t(Keys.controls.settings.throughput.unsavedIndexingPolicy)
? "indexing policy"
: editor === "dataMasking"
? t(Keys.controls.settings.throughput.unsavedDataMaskingPolicy)
: t(Keys.controls.settings.throughput.unsavedComputedProperties)}
{t(Keys.controls.settings.throughput.unsavedEditorWarningSuffix)}
? "data masking policy"
: "computed properties"}
. Please click save to confirm the changes.
</Text>
);
export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
{t(Keys.controls.settings.throughput.updateDelayedApplyWarning)}
You are about to request an increase in throughput beyond the pre-allocated capacity. This operation will take some
time to complete.
</Text>
);
export const getUpdateThroughputBeyondInstantLimitMessage = (instantMaximumThroughput: number): JSX.Element => {
return (
<Text id="updateThroughputDelayedApplyWarningMessage">
{t(Keys.controls.settings.throughput.scalingUpDelayMessage, {
instantMaximumThroughput: String(instantMaximumThroughput),
})}
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.
</Text>
);
};
@@ -420,26 +418,22 @@ export const getUpdateThroughputBeyondSupportLimitMessage = (
return (
<>
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
{t(Keys.controls.settings.throughput.exceedPreAllocatedMessage)}
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:
</Text>
<ol style={{ fontSize: 14, color: "var(--colorNeutralForeground1)", marginTop: "5px" }}>
<li>
{t(Keys.controls.settings.throughput.instantScaleOption, {
instantMaximumThroughput: String(instantMaximumThroughput),
})}
</li>
<li>You can instantly scale up to {instantMaximumThroughput} RU/s.</li>
{instantMaximumThroughput < maximumThroughput && (
<li>
{t(Keys.controls.settings.throughput.asyncScaleOption, { maximumThroughput: String(maximumThroughput) })}
</li>
<li>You can asynchronously scale up to any value under {maximumThroughput} RU/s in 4-6 hours.</li>
)}
<li>
{t(Keys.controls.settings.throughput.quotaMaxOption, { maximumThroughput: String(maximumThroughput) })}
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.
<Link
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase"
target="_blank"
>
{t(Keys.common.learnMore)}
Learn more
</Link>
</li>
</ol>
@@ -450,19 +444,23 @@ export const getUpdateThroughputBeyondSupportLimitMessage = (
export const getUpdateThroughputBelowMinimumMessage = (minimum: number): JSX.Element => {
return (
<Text styles={infoAndToolTipTextStyle}>
{t(Keys.controls.settings.throughput.belowMinimumMessage, { minimum: String(minimum) })}
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.
<Link
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank"
>
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
);
};
export const saveThroughputWarningMessage: JSX.Element = (
<Text>{t(Keys.controls.settings.throughput.saveThroughputWarning)}</Text>
<Text>
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below
before saving your changes
</Text>
);
const getCurrentThroughput = (
@@ -474,29 +472,23 @@ const getCurrentThroughput = (
if (targetThroughput) {
if (throughput) {
return isAutoscale
? `, ${t(Keys.controls.settings.throughput.currentAutoscaleThroughput)} ${Math.round(
? `, Current autoscale throughput: ${Math.round(
throughput / 10,
)} - ${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
? `, ${t(Keys.controls.settings.throughput.targetAutoscaleThroughput)} ${Math.round(
)} - ${throughput} ${throughputUnit}, Target autoscale throughput: ${Math.round(
targetThroughput / 10,
)} - ${targetThroughput} ${throughputUnit}`
: `, ${t(Keys.controls.settings.throughput.targetManualThroughput)} ${targetThroughput} ${throughputUnit}`;
: `, Current manual throughput: ${throughput} ${throughputUnit}, Target manual throughput: ${targetThroughput}`;
} else {
return isAutoscale
? `, Target autoscale throughput: ${Math.round(targetThroughput / 10)} - ${targetThroughput} ${throughputUnit}`
: `, Target manual throughput: ${targetThroughput} ${throughputUnit}`;
}
}
if (!targetThroughput && throughput) {
return isAutoscale
? `, ${t(Keys.controls.settings.throughput.currentAutoscaleThroughput)} ${Math.round(
throughput / 10,
)} - ${throughput} ${throughputUnit}`
: `, ${t(Keys.controls.settings.throughput.currentManualThroughput)} ${throughput} ${throughputUnit}`;
? `, Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} ${throughputUnit}`
: `, Current manual throughput: ${throughput} ${throughputUnit}`;
}
return "";
@@ -511,10 +503,10 @@ export const getThroughputApplyDelayedMessage = (
requestedThroughput: number,
): JSX.Element => (
<Text styles={infoAndToolTipTextStyle}>
{t(Keys.controls.settings.throughput.applyDelayedMessage)}
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 />
{t(Keys.controls.settings.throughput.databaseLabel)} {databaseName},{" "}
{t(Keys.controls.settings.throughput.containerLabel)} {collectionName}{" "}
Database: {databaseName}, Container: {collectionName}{" "}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
</Text>
);
@@ -527,13 +519,9 @@ export const getThroughputApplyShortDelayMessage = (
collectionName: string,
): JSX.Element => (
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
{t(Keys.controls.settings.throughput.applyShortDelayMessage)}
A request to increase the throughput is currently in progress. This operation will take some time to complete.
<br />
{collectionName
? `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName}, ${t(
Keys.controls.settings.throughput.containerLabel,
)} ${collectionName} `
: `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName} `}
{collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
</Text>
);
@@ -547,13 +535,10 @@ export const getThroughputApplyLongDelayMessage = (
requestedThroughput: number,
): JSX.Element => (
<Text styles={infoAndToolTipTextStyle} id="throughputApplyLongDelayMessage">
{t(Keys.controls.settings.throughput.applyLongDelayMessage)}
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to
complete. View the latest status in Notifications.
<br />
{collectionName
? `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName}, ${t(
Keys.controls.settings.throughput.containerLabel,
)} ${collectionName} `
: `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName} `}
{collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
</Text>
);
@@ -562,49 +547,63 @@ export const getToolTipContainer = (content: string | JSX.Element): JSX.Element
content ? <Text styles={infoAndToolTipTextStyle}>{content}</Text> : undefined;
export const conflictResolutionLwwTooltip: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.conflictResolution.lwwTooltip)}</Text>
<Text styles={infoAndToolTipTextStyle}>
Gets or sets the name of a integer property in your documents which is used for the Last Write Wins (LWW) based
conflict resolution scheme. By default, the system uses the system defined timestamp property, _ts to decide the
winner for the conflicting versions of the document. Specify your own integer property if you want to override the
default timestamp based conflict resolution.
</Text>
);
export const conflictResolutionCustomToolTip: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>
{t(Keys.controls.settings.conflictResolution.customTooltip)}
Gets or sets the name of a stored procedure (aka merge procedure) for resolving the conflicts. You can write
application defined logic to determine the winner of the conflicting versions of a document. The stored procedure
will get executed transactionally, exactly once, on the server side. If you do not provide a stored procedure, the
conflicts will be populated in the
<Link className="linkDarkBackground" href="https://aka.ms/dataexplorerconflics" target="_blank">
{t(Keys.controls.settings.conflictResolution.customTooltipConflictsFeed)}
{` conflicts feed`}
</Link>
{t(Keys.controls.settings.conflictResolution.customTooltipSuffix)}
. You can update/re-register the stored procedure at any time.
</Text>
);
export const changeFeedPolicyToolTip: JSX.Element = (
<Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.changeFeed.tooltip)}</Text>
<Text styles={infoAndToolTipTextStyle}>
Enable change feed log retention policy to retain last 10 minutes of history for items in the container by default.
To support this, the request unit (RU) charge for this container will be multiplied by a factor of two for writes.
Reads are unaffected.
</Text>
);
export const mongoIndexingPolicyDisclaimer: JSX.Element = (
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.mongoIndexing.disclaimer)}
For queries that filter on multiple properties, create multiple single field indexes instead of a compound index.
<Link
href="https://docs.microsoft.com/azure/cosmos-db/mongodb-indexing#index-types"
target="_blank"
style={{ color: "var(--colorBrandForeground1)" }}
>
{t(Keys.controls.settings.mongoIndexing.disclaimerCompoundIndexesLink)}
{` Compound indexes `}
</Link>
{t(Keys.controls.settings.mongoIndexing.disclaimerSuffix)}
are only used for sorting query results. If you need to add a compound index, you can create one using the Mongo
shell.
</Text>
);
export const mongoCompoundIndexNotSupportedMessage: JSX.Element = (
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.mongoIndexing.compoundNotSupported)}
Collections with compound indexes are not yet supported in the indexing editor. To modify indexing policy for this
collection, use the Mongo Shell.
</Text>
);
export const mongoIndexingPolicyAADError: JSX.Element = (
<MessageBar messageBarType={MessageBarType.error}>
<Text>
{t(Keys.controls.settings.mongoIndexing.aadError)}
To use the indexing policy editor, please login to the
<Link target="_blank" href="https://portal.azure.com">
{t(Keys.controls.settings.mongoIndexing.aadErrorLink)}
{"azure portal."}
</Link>
</Text>
</MessageBar>
@@ -612,7 +611,7 @@ export const mongoIndexingPolicyAADError: JSX.Element = (
export const mongoIndexTransformationRefreshingMessage: JSX.Element = (
<Stack horizontal {...mongoWarningStackProps}>
<Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.mongoIndexing.refreshingProgress)}</Text>
<Text styles={infoAndToolTipTextStyle}>Refreshing index transformation progress</Text>
<Spinner size={SpinnerSize.small} />
</Stack>
);
@@ -624,18 +623,15 @@ export const renderMongoIndexTransformationRefreshMessage = (
if (progress === 0) {
return (
<Text styles={infoAndToolTipTextStyle}>
{t(Keys.controls.settings.mongoIndexing.canMakeMoreChangesZero)}
<Link onClick={performRefresh}>{t(Keys.controls.settings.mongoIndexing.refreshToCheck)}</Link>
{"You can make more indexing changes once the current index transformation is complete. "}
<Link onClick={performRefresh}>{"Refresh to check if it has completed."}</Link>
</Text>
);
} else {
return (
<Text styles={infoAndToolTipTextStyle}>
{`${t(Keys.controls.settings.mongoIndexing.canMakeMoreChangesProgress).replace(
"{{progress}}",
String(progress),
)} `}
<Link onClick={performRefresh}>{t(Keys.controls.settings.mongoIndexing.refreshToCheckProgress)}</Link>
{`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>
</Text>
);
}

View File

@@ -4,8 +4,6 @@ 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 {
@@ -109,7 +107,7 @@ export class ComputedPropertiesComponent extends React.Component<
this.computedPropertiesEditor = monaco.editor.create(this.computedPropertiesDiv.current, {
value: value,
language: "json",
ariaLabel: t(Keys.controls.settings.computedProperties.ariaLabel),
ariaLabel: "Computed properties",
theme: monacoTheme(),
});
if (this.computedPropertiesEditor) {
@@ -153,9 +151,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/">
{t(Keys.common.learnMore)} <FontIcon iconName="NavigateExternalInline" />
{"Learn more"} <FontIcon iconName="NavigateExternalInline" />
</Link>
&#160; {t(Keys.controls.settings.computedProperties.learnMorePrefix)}
&#160; about how to define computed properties and how to use them.
</Text>
<div
className="settingsV2Editor"

View File

@@ -2,8 +2,6 @@ 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,
@@ -34,12 +32,9 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
private conflictResolutionChoiceGroupOptions: IChoiceGroupOption[] = [
{
key: DataModels.ConflictResolutionMode.LastWriterWins,
text: t(Keys.controls.settings.conflictResolution.lwwDefault),
},
{
key: DataModels.ConflictResolutionMode.Custom,
text: t(Keys.controls.settings.conflictResolution.customMergeProcedure),
text: "Last Write Wins (default)",
},
{ key: DataModels.ConflictResolutionMode.Custom, text: "Merge Procedure (custom)" },
];
componentDidMount(): void {
@@ -90,7 +85,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
private getConflictResolutionModeComponent = (): JSX.Element => (
<ChoiceGroup
label={t(Keys.controls.settings.conflictResolution.mode)}
label="Mode"
selectedKey={this.props.conflictResolutionPolicyMode}
options={this.conflictResolutionChoiceGroupOptions}
onChange={this.onConflictResolutionPolicyModeChange}
@@ -108,7 +103,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
private getConflictResolutionLWWComponent = (): JSX.Element => (
<TextField
id="conflictResolutionLwwTextField"
label={t(Keys.controls.settings.conflictResolution.conflictResolverProperty)}
label={"Conflict Resolver Property"}
onRenderLabel={this.onRenderLwwComponentTextField}
styles={{
fieldGroup: {
@@ -163,7 +158,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
return (
<TextField
id="conflictResolutionCustomTextField"
label={t(Keys.controls.settings.conflictResolution.storedProcedure)}
label="Stored procedure"
onRenderLabel={this.onRenderCustomComponentTextField}
styles={{
fieldGroup: {

View File

@@ -7,8 +7,6 @@ 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 {
@@ -155,7 +153,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
<PivotItem
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.VectorPolicyTab]}
style={{ marginTop: 20, color: "var(--colorNeutralForeground1)" }}
headerText={t(Keys.controls.settings.containerPolicy.vectorPolicy)}
headerText="Vector Policy"
>
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
{vectorEmbeddings && (
@@ -177,7 +175,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
<PivotItem
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.FullTextPolicyTab]}
style={{ marginTop: 20, color: "var(--colorNeutralForeground1)" }}
headerText={t(Keys.controls.settings.containerPolicy.fullTextPolicy)}
headerText="Full Text Policy"
>
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
{fullTextSearchPolicy ? (
@@ -220,7 +218,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
});
}}
>
{t(Keys.controls.settings.containerPolicy.createFullTextPolicy)}
Create new full text search policy
</DefaultButton>
)}
</Stack>

View File

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

View File

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

View File

@@ -2,8 +2,6 @@ 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";
@@ -23,9 +21,7 @@ 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 } }}>
{t(Keys.controls.settings.globalSecondaryIndex.indexesDefined)}
</Text>
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following indexes defined for it.</Text>
)}
<Text>
<Link
@@ -35,7 +31,7 @@ export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexCompone
Learn more
<FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} />
</Link>{" "}
{t(Keys.controls.settings.globalSecondaryIndex.learnMoreSuffix)}
about how to define global secondary indexes and how to use them.
</Text>
</Stack>
{isSourceContainer && <GlobalSecondaryIndexSourceComponent collection={collection} explorer={explorer} />}

View File

@@ -9,8 +9,6 @@ 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;
@@ -69,7 +67,7 @@ export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexS
editorRef.current = monacoInstance.editor.create(editorContainerRef.current, {
value: jsonValue,
language: "json",
ariaLabel: t(Keys.controls.settings.globalSecondaryIndex.jsonAriaLabel),
ariaLabel: "Global Secondary Index JSON",
readOnly: true,
});
};
@@ -100,7 +98,7 @@ export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexS
}}
/>
<PrimaryButton
text={t(Keys.controls.settings.globalSecondaryIndex.addIndex)}
text="Add index"
styles={{ root: { width: "fit-content", marginTop: 12 } }}
onClick={() =>
useSidePanel

View File

@@ -1,8 +1,6 @@
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;
@@ -27,21 +25,17 @@ export const GlobalSecondaryIndexTargetComponent: React.FC<GlobalSecondaryIndexT
return (
<Stack tokens={{ childrenGap: 15 }} styles={{ root: { maxWidth: 600 } }}>
<Text styles={textHeadingStyle}>{t(Keys.controls.settings.globalSecondaryIndex.settingsTitle)}</Text>
<Text styles={textHeadingStyle}>Global Secondary Index Settings</Text>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={{ root: { fontWeight: "600" } }}>
{t(Keys.controls.settings.globalSecondaryIndex.sourceContainer)}
</Text>
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
<Stack styles={valueBoxStyle}>
<Text>{globalSecondaryIndexDefinition?.sourceCollectionId}</Text>
</Stack>
</Stack>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={{ root: { fontWeight: "600" } }}>
{t(Keys.controls.settings.globalSecondaryIndex.indexDefinition)}
</Text>
<Text styles={{ root: { fontWeight: "600" } }}>Global secondary index definition</Text>
<Stack styles={valueBoxStyle}>
<Text>{globalSecondaryIndexDefinition?.definition}</Text>
</Stack>

View File

@@ -3,8 +3,6 @@ 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";
@@ -121,7 +119,7 @@ export class IndexingPolicyComponent extends React.Component<
value: value,
language: "json",
readOnly: isIndexTransforming(this.props.indexTransformationProgress),
ariaLabel: t(Keys.controls.settings.indexingPolicy.ariaLabel),
ariaLabel: "Indexing Policy",
theme: monacoTheme(),
});
if (this.indexingPolicyEditor) {

View File

@@ -1,8 +1,6 @@
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,
@@ -48,11 +46,7 @@ export class IndexingPolicyRefreshComponent extends React.Component<
try {
await this.props.refreshIndexTransformationProgress();
} catch (error) {
handleError(
error,
"RefreshIndexTransformationProgress",
t(Keys.controls.settings.indexingPolicyRefresh.refreshFailed),
);
handleError(error, "RefreshIndexTransformationProgress", "Refreshing index transformation progress failed");
} finally {
this.setState({ isRefreshing: false });
}

View File

@@ -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]}
>

View File

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

View File

@@ -15,8 +15,6 @@ 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,
@@ -85,25 +83,11 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
};
private initialIndexesColumns: IColumn[] = [
{
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: "definition", name: "Definition", fieldName: "definition", minWidth: 100, maxWidth: 200, isResizable: true },
{ key: "type", name: "Type", fieldName: "type", minWidth: 100, maxWidth: 200, isResizable: true },
{
key: "actionButton",
name: t(Keys.controls.settings.mongoIndexing.dropIndexColumn),
name: "Drop Index",
fieldName: "actionButton",
minWidth: 100,
maxWidth: 200,
@@ -112,25 +96,11 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
];
private indexesToBeDroppedColumns: IColumn[] = [
{
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: "definition", name: "Definition", fieldName: "definition", minWidth: 100, maxWidth: 200, isResizable: true },
{ key: "type", name: "Type", fieldName: "type", minWidth: 100, maxWidth: 200, isResizable: true },
{
key: "actionButton",
name: t(Keys.controls.settings.mongoIndexing.addIndexBackColumn),
name: "Add index back",
fieldName: "actionButton",
minWidth: 100,
maxWidth: 200,
@@ -191,7 +161,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
private getActionButton = (arrayPosition: number, isCurrentIndex: boolean): JSX.Element => {
return isCurrentIndex ? (
<IconButton
ariaLabel={t(Keys.controls.settings.mongoIndexing.deleteIndexButton)}
ariaLabel="Delete index Button"
iconProps={{ iconName: "Delete" }}
disabled={isIndexTransforming(this.props.indexTransformationProgress)}
onClick={() => {
@@ -200,7 +170,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
/>
) : (
<IconButton
ariaLabel={t(Keys.controls.settings.mongoIndexing.addBackIndexButton)}
ariaLabel="Add back Index Button"
iconProps={{ iconName: "Add" }}
onClick={() => {
this.props.onRevertIndexDrop(arrayPosition);
@@ -288,10 +258,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
return (
<Stack {...createAndAddMongoIndexStackProps} styles={mediumWidthStackStyles}>
<CollapsibleSectionComponent
title={t(Keys.controls.settings.mongoIndexing.currentIndexes)}
isExpandedByDefault={true}
>
<CollapsibleSectionComponent title="Current index(es)" isExpandedByDefault={true}>
{
<>
<DetailsList
@@ -318,10 +285,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
return (
<Stack styles={mediumWidthStackStyles}>
<CollapsibleSectionComponent
title={t(Keys.controls.settings.mongoIndexing.indexesToBeDropped)}
isExpandedByDefault={true}
>
<CollapsibleSectionComponent title="Index(es) to be dropped" isExpandedByDefault={true}>
{indexesToBeDropped.length > 0 && (
<DetailsList
styles={customDetailsListStyles}

View File

@@ -18,8 +18,6 @@ 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,
@@ -82,7 +80,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
return (collection.partitionKeyProperties || []).map((property) => "/" + property).join(", ");
};
const partitionKeyName = t(Keys.controls.settings.partitionKey.partitionKey);
const partitionKeyName = "Partition key";
const partitionKeyValue = getPartitionKeyValue();
const textHeadingStyle = {
@@ -150,13 +148,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
const getProgressDescription = (): string => {
const processedCount = portalDataTransferJob?.properties?.processedCount;
const totalCount = portalDataTransferJob?.properties?.totalCount;
const processedCountString =
totalCount > 0
? t(Keys.controls.settings.partitionKeyEditor.documentsProcessed, {
processedCount: String(processedCount),
totalCount: String(totalCount),
})
: "";
const processedCountString = totalCount > 0 ? `(${processedCount} of ${totalCount} documents processed)` : "";
return `${portalDataTransferJob?.properties?.status} ${processedCountString}`;
};
@@ -189,28 +181,16 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
return (
<Stack tokens={{ childrenGap: 20 }} styles={{ root: { maxWidth: 600 } }}>
<Stack tokens={{ childrenGap: 10 }}>
{!isReadOnly && (
<Text styles={textHeadingStyle}>
{t(Keys.controls.settings.partitionKeyEditor.changePartitionKey, {
partitionKeyName: partitionKeyName.toLowerCase(),
})}
</Text>
)}
{!isReadOnly && <Text styles={textHeadingStyle}>Change {partitionKeyName.toLowerCase()}</Text>}
<Stack horizontal tokens={{ childrenGap: 20 }}>
<Stack tokens={{ childrenGap: 5 }}>
<Text styles={textSubHeadingStyle}>
{t(Keys.controls.settings.partitionKeyEditor.currentPartitionKey, {
partitionKeyName: partitionKeyName.toLowerCase(),
})}
</Text>
<Text styles={textSubHeadingStyle}>{t(Keys.controls.settings.partitionKeyEditor.partitioning)}</Text>
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
<Text styles={textSubHeadingStyle}>Partitioning</Text>
</Stack>
<Stack tokens={{ childrenGap: 5 }} data-test="partition-key-values">
<Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text>
<Text styles={textSubHeadingStyle1}>
{isHierarchicalPartitionedContainer()
? t(Keys.controls.settings.partitionKeyEditor.hierarchical)
: t(Keys.controls.settings.partitionKeyEditor.nonHierarchical)}
{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}
</Text>
</Stack>
</Stack>
@@ -224,33 +204,33 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
styles={darkThemeMessageBarStyles}
>
{t(Keys.controls.settings.partitionKeyEditor.safeguardWarning)}
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.
<Link
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
target="_blank"
underline
style={{ color: "var(--colorBrandForeground1)" }}
>
{t(Keys.common.learnMore)}
Learn more
</Link>
</MessageBar>
<Text styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>
{t(Keys.controls.settings.partitionKeyEditor.changeDescription)}
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.
</Text>
{configContext.platform !== Platform.Emulator && (
<PrimaryButton
data-test="change-partition-key-button"
styles={{ root: { width: "fit-content" } }}
text={t(Keys.controls.settings.partitionKeyEditor.changeButton)}
text="Change"
onClick={startPartitionkeyChangeWorkflow}
disabled={isCurrentJobInProgress(portalDataTransferJob)}
/>
)}
{portalDataTransferJob && (
<Stack>
<Text styles={textHeadingStyle}>
{t(Keys.controls.settings.partitionKeyEditor.changeJob, { partitionKeyName })}
</Text>
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
<Stack
horizontal
tokens={{ childrenGap: 20 }}
@@ -271,10 +251,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
}}
></ProgressIndicator>
{isCurrentJobInProgress(portalDataTransferJob) && (
<DefaultButton
text={t(Keys.controls.settings.partitionKeyEditor.cancelButton)}
onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)}
/>
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
)}
</Stack>
</Stack>

View File

@@ -4,8 +4,6 @@ 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";
@@ -158,12 +156,14 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
const freeTierLimits = SharedConstants.FreeTierLimits;
return (
<Text>
{t(Keys.controls.settings.scale.freeTierInfo, { ru: freeTierLimits.RU, storage: freeTierLimits.Storage })}
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.
<Link
href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts"
target="_blank"
>
{t(Keys.controls.settings.scale.freeTierLearnMore)}
Learn more.
</Link>
</Text>
);
@@ -188,9 +188,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
{/* TODO: Replace link with call to the Azure Support blade */}
{this.isAutoScaleEnabled() && (
<Stack {...titleAndInputStackProps}>
<Text>{t(Keys.controls.settings.scale.throughputRuS)}</Text>
<Text>Throughput (RU/s)</Text>
<TextField disabled styles={getTextFieldStyles(undefined, undefined)} />
<Text>{t(Keys.controls.settings.scale.autoScaleCustomSettings)}</Text>
<Text>
Your account has custom settings that prevents setting throughput at the container level. Please work with
your Cosmos DB engineering team point of contact to make changes.
</Text>
</Stack>
)}
</Stack>

View File

@@ -12,8 +12,6 @@ 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 {
@@ -87,12 +85,9 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
constructor(props: SubSettingsComponentProps) {
super(props);
this.geospatialVisible = userContext.apiType === "SQL";
this.partitionKeyName =
userContext.apiType === "Mongo"
? t(Keys.controls.settings.partitionKey.shardKey)
: t(Keys.controls.settings.partitionKey.partitionKey);
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
this.partitionKeyValue = this.getPartitionKeyValue();
this.uniqueKeyName = t(Keys.controls.settings.subSettings.uniqueKeys);
this.uniqueKeyName = "Unique keys";
this.uniqueKeyValue = this.getUniqueKeyValue();
}
@@ -148,13 +143,9 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
};
private ttlChoiceGroupOptions: IChoiceGroupOption[] = [
{ 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" },
{ 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" },
];
public getTtlValue = (value: string): TtlType => {
@@ -225,13 +216,13 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
}}
>
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.subSettings.mongoTtlMessage)}{" "}
To enable time-to-live (TTL) for your collection/documents,{" "}
<Link
href="https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-time-to-live"
target="_blank"
style={{ color: "var(--colorBrandForeground1)" }}
>
{t(Keys.controls.settings.subSettings.mongoTtlLinkText)}
create a TTL index
</Link>
.
</Text>
@@ -240,7 +231,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
<Stack {...titleAndInputStackProps}>
<ChoiceGroup
id="timeToLive"
label={t(Keys.controls.settings.subSettings.timeToLive)}
label="Time to Live"
selectedKey={this.props.timeToLive}
options={this.ttlChoiceGroupOptions}
onChange={this.onTtlChange}
@@ -264,8 +255,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
max={Int32.Max}
value={this.props.displayedTtlSeconds}
onChange={this.onTimeToLiveSecondsChange}
suffix={t(Keys.controls.settings.subSettings.seconds)}
ariaLabel={t(Keys.controls.settings.subSettings.timeToLiveInSeconds)}
suffix="second(s)"
ariaLabel={`Time to live in seconds`}
data-test="ttl-input"
/>
)}
@@ -273,16 +264,16 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
);
private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [
{ 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) },
{ key: TtlType.Off, text: "Off", disabled: true },
{ key: TtlType.OnNoDefault, text: "On (no default)" },
{ key: TtlType.On, text: "On" },
];
private getAnalyticalStorageTtlComponent = (): JSX.Element => (
<Stack {...titleAndInputStackProps}>
<ChoiceGroup
id="analyticalStorageTimeToLive"
label={t(Keys.controls.settings.subSettings.analyticalStorageTtl)}
label="Analytical Storage Time to Live"
selectedKey={this.props.analyticalStorageTtlSelection}
options={this.analyticalTtlChoiceGroupOptions}
onChange={this.onAnalyticalStorageTtlSelectionChange}
@@ -303,7 +294,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
min={1}
max={Int32.Max}
value={this.props.analyticalStorageTtlSeconds?.toString()}
suffix={t(Keys.controls.settings.subSettings.seconds)}
suffix="second(s)"
onChange={this.onAnalyticalStorageTtlSecondsChange}
/>
)}
@@ -311,22 +302,14 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
);
private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [
{
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",
},
{ key: GeospatialConfigType.Geography, text: "Geography", ariaLabel: "geography-option" },
{ key: GeospatialConfigType.Geometry, text: "Geometry", ariaLabel: "geometry-option" },
];
private getGeoSpatialComponent = (): JSX.Element => (
<ChoiceGroup
id="geoSpatialConfig"
label={t(Keys.controls.settings.subSettings.geospatialConfiguration)}
label="Geospatial Configuration"
selectedKey={this.props.geospatialConfigType}
options={this.geoSpatialConfigTypeChoiceGroupOptions}
onChange={this.onGeoSpatialConfigTypeChange}
@@ -335,8 +318,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
);
private changeFeedChoiceGroupOptions: IChoiceGroupOption[] = [
{ key: ChangeFeedPolicyState.Off, text: t(Keys.controls.settings.subSettings.ttlOff) },
{ key: ChangeFeedPolicyState.On, text: t(Keys.controls.settings.subSettings.ttlOn) },
{ key: ChangeFeedPolicyState.Off, text: "Off" },
{ key: ChangeFeedPolicyState.On, text: "On" },
];
private getChangeFeedComponent = (): JSX.Element => {
@@ -345,10 +328,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
return (
<Stack>
<Label id={labelId}>
<ToolTipLabelComponent
label={t(Keys.controls.settings.changeFeed.label)}
toolTipElement={changeFeedPolicyToolTip}
/>
<ToolTipLabelComponent label="Change feed log retention policy" toolTipElement={changeFeedPolicyToolTip} />
</Label>
<ChoiceGroup
id="changeFeedPolicy"
@@ -393,20 +373,14 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
)}
{userContext.apiType === "SQL" && this.isLargePartitionKeyEnabled() && (
<Text className={classNames.hintText}>
{t(Keys.controls.settings.subSettings.largePartitionKeyEnabled, {
partitionKeyName: this.partitionKeyName.toLowerCase(),
})}
</Text>
<Text className={classNames.hintText}>Large {this.partitionKeyName.toLowerCase()} has been enabled.</Text>
)}
{userContext.apiType === "SQL" &&
(this.isHierarchicalPartitionedContainer() ? (
<Text className={classNames.hintText}>{t(Keys.controls.settings.subSettings.hierarchicalPartitioned)}</Text>
<Text className={classNames.hintText}>Hierarchically partitioned container.</Text>
) : (
<Text className={classNames.hintText}>
{t(Keys.controls.settings.subSettings.nonHierarchicalPartitioned)}
</Text>
<Text className={classNames.hintText}>Non-hierarchically partitioned container.</Text>
))}
</Stack>
);

View File

@@ -1,7 +1,5 @@
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";
@@ -67,9 +65,7 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
return (
<Stack tokens={{ childrenGap: "m" }} styles={{ root: { width: "70%", maxWidth: 700 } }}>
<Label styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>
{t(Keys.controls.settings.throughputBuckets.label)}
</Label>
<Label styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>Throughput Buckets</Label>
<Stack>
{throughputBuckets?.map((bucket) => (
<Stack key={bucket.id} horizontal tokens={{ childrenGap: 8 }} verticalAlign="center">
@@ -80,9 +76,7 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
value={bucket.maxThroughputPercentage}
onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
showValue={false}
label={`${t(Keys.controls.settings.throughputBuckets.bucketLabel, { id: String(bucket.id) })}${
bucket.id === 1 ? t(Keys.controls.settings.throughputBuckets.dataExplorerQueryBucket) : ""
}`}
label={`Bucket ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
styles={{
root: { flex: 2, maxWidth: 400 },
titleLabel: {
@@ -105,8 +99,8 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
disabled={bucket.maxThroughputPercentage === 100}
/>
<Toggle
onText={t(Keys.controls.settings.throughputBuckets.active)}
offText={t(Keys.controls.settings.throughputBuckets.inactive)}
onText="Active"
offText="Inactive"
checked={bucket.maxThroughputPercentage !== 100}
onChange={(event, checked) => onToggle(bucket.id, checked)}
styles={{

View File

@@ -18,8 +18,6 @@ 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";
@@ -99,8 +97,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
private throughputInputMaxValue: number;
private autoPilotInputMaxValue: number;
private options: IChoiceGroupOption[] = [
{ key: "true", text: t(Keys.controls.settings.throughputInput.autoscale) },
{ key: "false", text: t(Keys.controls.settings.throughputInput.manual) },
{ key: "true", text: "Autoscale" },
{ key: "false", text: "Manual" },
];
// Style constants for theme-aware colors and layout
@@ -246,7 +244,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
return (
<div>
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
{t(Keys.controls.settings.costEstimate.updatedCostPerMonth)}
Updated cost per month
</Text>
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
<Text
@@ -255,8 +253,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
}}
>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)}{" "}
{t(Keys.controls.settings.throughputInput.min)}
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)} min
</Text>
<Text
style={{
@@ -264,8 +261,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
}}
>
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}{" "}
{t(Keys.controls.settings.throughputInput.max)}
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} max
</Text>
</Stack>
</div>
@@ -278,7 +274,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
{newThroughput && newThroughputCostElement()}
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
{t(Keys.controls.settings.costEstimate.currentCostPerMonth)}
Current cost per month
</Text>
<Stack horizontal style={{ marginTop: 5, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
<Text
@@ -287,8 +283,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
}}
>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)}{" "}
{t(Keys.controls.settings.throughputInput.min)}
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)} min
</Text>
<Text
style={{
@@ -296,8 +291,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
}}
>
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}{" "}
{t(Keys.controls.settings.throughputInput.max)}
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} max
</Text>
</Stack>
</Stack>
@@ -332,7 +326,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
return (
<div>
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
{t(Keys.controls.settings.costEstimate.updatedCostPerMonth)}
Updated cost per month
</Text>
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
<Text style={this.settingsAndScaleStyle.root}>
@@ -355,7 +349,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
{newThroughput && newThroughputCostElement()}
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
{t(Keys.controls.settings.costEstimate.currentCostPerMonth)}
Current cost per month
</Text>
<Stack horizontal style={{ marginTop: 5 }}>
<Text style={this.settingsAndScaleStyle.root}>
@@ -450,14 +444,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
this.setState({ spendAckChecked: checked });
private getStorageCapacityTitle = (): JSX.Element => {
const capacity: string = this.props.isFixed
? t(Keys.controls.settings.throughputInput.fixed)
: t(Keys.controls.settings.throughputInput.unlimited);
const capacity: string = this.props.isFixed ? "Fixed" : "Unlimited";
return (
<Stack {...titleAndInputStackProps}>
<Label style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.throughputInput.storageCapacity)}
</Label>
<Label style={{ color: "var(--colorNeutralForeground1)" }}>Storage capacity</Label>
<Text style={{ color: "var(--colorNeutralForeground1)" }}>{capacity}</Text>
</Stack>
);
@@ -568,14 +558,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
/>
<Stack horizontal>
<Stack.Item style={{ width: "34%", paddingRight: "5px" }}>
<Separator styles={this.thoughputRangeSeparatorStyles}>
{t(Keys.controls.settings.throughputInput.instant)}
</Separator>
<Separator styles={this.thoughputRangeSeparatorStyles}>Instant</Separator>
</Stack.Item>
<Stack.Item style={{ width: "66%", paddingLeft: "5px" }}>
<Separator styles={this.thoughputRangeSeparatorStyles}>
{t(Keys.controls.settings.throughputInput.fourToSixHrs)}
</Separator>
<Separator styles={this.thoughputRangeSeparatorStyles}>4-6 hrs</Separator>
</Stack.Item>
</Stack>
</Stack>
@@ -655,7 +641,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
variant="small"
style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }}
>
{t(Keys.controls.settings.throughputInput.minimumRuS)}
Minimum RU/s
</Text>
<FontIcon iconName="Info" style={{ fontSize: 12, color: "var(--colorNeutralForeground2)" }} />
</Stack>
@@ -689,7 +675,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
color: "var(--colorNeutralForeground1)",
}}
>
{t(Keys.controls.settings.throughputInput.x10Equals)}
x 10 =
</Text>
{/* Column 3: Maximum RU/s */}
@@ -699,7 +685,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
variant="small"
style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }}
>
{t(Keys.controls.settings.throughputInput.maximumRuS)}
Maximum RU/s
</Text>
<FontIcon iconName="Info" style={{ fontSize: 12, color: "var(--colorNeutralForeground2)" }} />
</Stack>
@@ -740,9 +726,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
onGetErrorMessage={(value: string) => {
const sanitizedValue = getSanitizedInputValue(value);
const errorMessage: string =
sanitizedValue % 1000
? t(Keys.controls.settings.throughput.throughputIncrementError)
: this.props.throughputError;
sanitizedValue % 1000 ? "Throughput value must be in increments of 1000" : this.props.throughputError;
return <span data-test="autopilot-throughput-input-error">{errorMessage}</span>;
}}
validateOnLoad={false}
@@ -788,9 +772,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
)}
{this.props.isAutoPilotSelected ? (
<Text style={{ marginTop: "40px", color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.throughputInput.autoscaleDescription, {
resourceType: this.props.collectionName ? "container" : "database",
})}{" "}
Based on usage, your {this.props.collectionName ? "container" : "database"} throughput will scale from{" "}
<b>
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)} RU/s (10% of max RU/s) -{" "}
{this.props.maxAutoPilotThroughput} RU/s
@@ -805,19 +787,16 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
styles={this.darkThemeMessageBarStyles}
style={{ marginTop: "40px" }}
>
{t(Keys.controls.settings.throughputInput.freeTierWarning, {
ru: String(SharedConstants.FreeTierLimits.RU),
})}
{`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.`}
</MessageBar>
)}
</>
)}
{!this.overrideWithProvisionedThroughputSettings() && (
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.controls.settings.throughputInput.capacityCalculator)}
Estimate your required RU/s with
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
{t(Keys.controls.settings.throughputInput.capacityCalculatorLink)}{" "}
<FontIcon iconName="NavigateExternalInline" />
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />
</Link>
</Text>
)}
@@ -830,7 +809,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
onChange={this.onSpendAckChecked}
/>
)}
{this.props.isFixed && <p>{t(Keys.controls.settings.throughputInput.fixedStorageNote)}</p>}
{this.props.isFixed && (
<p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>
)}
{this.props.collectionName && (
<Stack.Item style={{ marginTop: "40px" }}>{this.getStorageCapacityTitle()}</Stack.Item>
)}

View File

@@ -561,7 +561,9 @@ 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"
@@ -579,7 +581,9 @@ 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
@@ -688,8 +692,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$
35.04
min
min
</Text>
<Text
style={
@@ -702,8 +705,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$
350.40
max
max
</Text>
</Stack>
</div>
@@ -737,8 +739,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$
35.04
min
min
</Text>
<Text
style={
@@ -751,8 +752,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$
350.40
max
max
</Text>
</Stack>
</Stack>
@@ -1169,7 +1169,9 @@ 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"
@@ -1753,7 +1755,9 @@ 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"

View File

@@ -27,8 +27,7 @@ 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"

View File

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

View File

@@ -410,7 +410,9 @@ 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"
@@ -982,7 +984,9 @@ 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"
@@ -1518,7 +1522,9 @@ 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"
@@ -2151,7 +2157,9 @@ 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"
@@ -2721,7 +2729,9 @@ 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"

View File

@@ -1,11 +1,7 @@
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;
@@ -92,19 +88,6 @@ 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) {
@@ -177,27 +160,25 @@ const getStringValue = (value: isDirtyTypes, type: string): string => {
export const getTabTitle = (tab: SettingsV2TabTypes): string => {
switch (tab) {
case SettingsV2TabTypes.ScaleTab:
return t(Keys.controls.settings.tabTitles.scale);
return "Scale";
case SettingsV2TabTypes.ConflictResolutionTab:
return t(Keys.controls.settings.tabTitles.conflictResolution);
return "Conflict Resolution";
case SettingsV2TabTypes.SubSettingsTab:
return t(Keys.controls.settings.tabTitles.settings);
return "Settings";
case SettingsV2TabTypes.IndexingPolicyTab:
return t(Keys.controls.settings.tabTitles.indexingPolicy);
return "Indexing Policy";
case SettingsV2TabTypes.PartitionKeyTab:
return isFabricNative()
? t(Keys.controls.settings.tabTitles.partitionKeys)
: t(Keys.controls.settings.tabTitles.partitionKeysPreview);
return isFabricNative() ? "Partition Keys" : "Partition Keys (preview)";
case SettingsV2TabTypes.ComputedPropertiesTab:
return t(Keys.controls.settings.tabTitles.computedProperties);
return "Computed Properties";
case SettingsV2TabTypes.ContainerVectorPolicyTab:
return t(Keys.controls.settings.tabTitles.containerPolicies);
return "Container Policies";
case SettingsV2TabTypes.ThroughputBucketsTab:
return t(Keys.controls.settings.tabTitles.throughputBuckets);
return "Throughput Buckets";
case SettingsV2TabTypes.GlobalSecondaryIndexTab:
return t(Keys.controls.settings.tabTitles.globalSecondaryIndexPreview);
return "Global Secondary Index (Preview)";
case SettingsV2TabTypes.DataMaskingTab:
return t(Keys.controls.settings.tabTitles.maskingPolicyPreview);
return "Masking Policy (preview)";
default:
throw new Error(`Unknown tab ${tab}`);
}
@@ -207,19 +188,19 @@ export const getMongoNotification = (description: string, type: MongoIndexTypes)
if (description && !type) {
return {
type: MongoNotificationType.Warning,
message: t(Keys.controls.settings.mongoNotifications.selectTypeWarning),
message: "Please select a type for each index.",
};
}
if (type && (!description || description.trim().length === 0)) {
return {
type: MongoNotificationType.Error,
message: t(Keys.controls.settings.mongoNotifications.enterFieldNameError),
message: "Please enter a field name.",
};
} else if (type === MongoIndexTypes.Wildcard && description?.indexOf("$**") === -1) {
return {
type: MongoNotificationType.Error,
message: t(Keys.controls.settings.mongoNotifications.wildcardPathError) + MongoWildcardPlaceHolder,
message: "Wildcard path is not present in the field name. Use a pattern like " + MongoWildcardPlaceHolder,
};
}
@@ -253,29 +234,28 @@ export const isIndexTransforming = (indexTransformationProgress: number): boolea
indexTransformationProgress !== undefined && indexTransformationProgress !== 100;
export const getPartitionKeyName = (apiType: string, isLowerCase?: boolean): string => {
const partitionKeyName =
apiType === "Mongo"
? t(Keys.controls.settings.partitionKey.shardKey)
: t(Keys.controls.settings.partitionKey.partitionKey);
const partitionKeyName = apiType === "Mongo" ? "Shard key" : "Partition key";
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
};
export const getPartitionKeyTooltipText = (apiType: string): string => {
if (apiType === "Mongo") {
return t(Keys.controls.settings.partitionKey.shardKeyTooltip);
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. Its critical to choose a field that will evenly distribute your data.";
}
let tooltipText = `The ${getPartitionKeyName(apiType, true)} ${t(
Keys.controls.settings.partitionKey.partitionKeyTooltip,
)}`;
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.`;
if (apiType === "SQL") {
tooltipText += t(Keys.controls.settings.partitionKey.sqlPartitionKeyTooltipSuffix);
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
}
return tooltipText;
};
export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: string): string => {
if (partitionKeyDefault && (apiType === "SQL" || apiType === "Mongo")) {
return t(Keys.controls.settings.partitionKey.partitionKeySubtext);
const subtext = "For small workloads, the item ID is a suitable choice for the partition key.";
return subtext;
}
return "";
};
@@ -283,18 +263,18 @@ export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: st
export const getPartitionKeyPlaceHolder = (apiType: string, index?: number): string => {
switch (apiType) {
case "Mongo":
return t(Keys.controls.settings.partitionKey.mongoPlaceholder);
return "e.g., categoryId";
case "Gremlin":
return t(Keys.controls.settings.partitionKey.gremlinPlaceholder);
return "e.g., /address";
case "SQL":
return `${
index === undefined
? t(Keys.controls.settings.partitionKey.sqlFirstPartitionKey)
? "Required - first partition key e.g., /TenantId"
: index === 0
? t(Keys.controls.settings.partitionKey.sqlSecondPartitionKey)
: t(Keys.controls.settings.partitionKey.sqlThirdPartitionKey)
? "second partition key e.g., /UserId"
: "third partition key e.g., /SessionId"
}`;
default:
return t(Keys.controls.settings.partitionKey.defaultPlaceholder);
return "e.g., /address/zipCode";
}
};

View File

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

View File

@@ -604,58 +604,6 @@ 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={
{

View File

@@ -127,13 +127,9 @@ 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
@@ -313,7 +309,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]}
>

View File

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

View File

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

View File

@@ -42,8 +42,6 @@ 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";
@@ -179,7 +177,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
messageType="info"
showErrorDetails={false}
link={Constants.Urls.freeTierInformation}
linkText={t(Keys.common.learnMore)}
linkText="Learn more"
/>
)}
@@ -276,17 +274,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.databaseTooltip, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).toLocaleLowerCase()}.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.databaseTooltip, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).toLocaleLowerCase()}.`}
/>
</TooltipHost>
</Stack>
@@ -306,7 +304,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.createNew)}</span>
<span className="panelRadioBtnLabel">Create new</span>
<input
className="panelRadioBtn"
@@ -319,7 +317,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.useExisting)}</span>
<span className="panelRadioBtnLabel">Use existing</span>
</div>
</Stack>
)}
@@ -349,9 +347,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && (
<Stack horizontal>
<Checkbox
label={t(Keys.panes.addCollection.shareThroughput, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
checked={this.state.isSharedThroughputChecked}
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -369,17 +365,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.shareThroughputTooltip, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
true,
).toLocaleLowerCase()} within the database.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.shareThroughputTooltip, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
true,
).toLocaleLowerCase()} within the database.`}
/>
</TooltipHost>
</Stack>
@@ -414,6 +410,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
defaultSelectedKey={this.props.databaseId}
responsiveMode={999}
onRenderOption={this.onRenderDatabaseOption}
/>
)}
<Separator className="panelSeparator" style={{ marginTop: -4, marginBottom: -4 }} />
@@ -428,18 +425,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
>
<Icon
role="button"
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
/>
</TooltipHost>
</Stack>
@@ -453,10 +446,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={t(Keys.panes.addCollection.collectionIdPlaceholder, { collectionName: getCollectionName() })}
placeholder={`e.g., ${getCollectionName()}1`}
size={40}
className="panelTextField"
aria-label={t(Keys.panes.addCollection.collectionIdAriaLabel, { collectionName: getCollectionName() })}
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
value={this.state.collectionId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ collectionId: event.target.value })
@@ -470,7 +463,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addCollection.indexing)}
Indexing
</Text>
</Stack>
@@ -478,32 +471,32 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input
className="panelRadioBtn"
checked={this.state.enableIndexing}
aria-label={t(Keys.panes.addCollection.turnOnIndexing)}
aria-label="Turn on indexing"
aria-checked={this.state.enableIndexing}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onTurnOnIndexing.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.automatic)}</span>
<span className="panelRadioBtnLabel">Automatic</span>
<input
className="panelRadioBtn"
checked={!this.state.enableIndexing}
aria-label={t(Keys.panes.addCollection.turnOffIndexing)}
aria-label="Turn off indexing"
aria-checked={!this.state.enableIndexing}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onTurnOffIndexing.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.off)}</span>
<span className="panelRadioBtnLabel">Off</span>
</Stack>
<Text variant="small">
{this.getFreeTierIndexingText()}{" "}
<Link target="_blank" href="https://aka.ms/cosmos-indexing-policy">
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
</Stack>
@@ -516,17 +509,21 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal style={{ marginTop: -5, marginBottom: -4 }}>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addCollection.sharding)}
Sharding
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.shardingTooltip)}
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."
}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.shardingTooltip)}
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."
}
/>
</TooltipHost>
</Stack>
@@ -535,7 +532,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input
className="panelRadioBtn"
checked={!this.state.isSharded}
aria-label={t(Keys.panes.addCollection.unsharded)}
aria-label="Unsharded"
aria-checked={!this.state.isSharded}
name="unsharded"
type="radio"
@@ -544,12 +541,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onUnshardedRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.unshardedLabel)}</span>
<span className="panelRadioBtnLabel">Unsharded (20GB limit)</span>
<input
className="panelRadioBtn"
checked={this.state.isSharded}
aria-label={t(Keys.panes.addCollection.sharded)}
aria-label="Sharded"
aria-checked={this.state.isSharded}
name="sharded"
type="radio"
@@ -558,7 +555,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onShardedRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.sharded)}</span>
<span className="panelRadioBtnLabel">Sharded</span>
</Stack>
</Stack>
)}
@@ -683,14 +680,15 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
disabled={this.state.subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
onClick={() => this.setState({ subPartitionKeys: [...this.state.subPartitionKeys, ""] })}
>
{t(Keys.panes.addCollection.addPartitionKey)}
Add hierarchical partition key
</DefaultButton>
{this.state.subPartitionKeys.length > 0 && (
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} />{" "}
{t(Keys.panes.addCollection.hierarchicalPartitionKeyInfo)}{" "}
<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.{" "}
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
)}
@@ -703,9 +701,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && (
<Stack horizontal verticalAlign="center">
<Checkbox
label={t(Keys.panes.addCollection.provisionDedicatedThroughput, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
checked={this.state.enableDedicatedThroughput}
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -723,19 +719,23 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
})}
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.`}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
})}
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.`}
/>
</TooltipHost>
</Stack>
@@ -770,8 +770,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
autoComplete="off"
placeholder={
userContext.apiType === "Mongo"
? t(Keys.panes.addCollection.uniqueKeysPlaceholderMongo)
: t(Keys.panes.addCollection.uniqueKeysPlaceholderSql)
? "Comma separated paths e.g. firstName,address.zipCode"
: "Comma separated paths e.g. /firstName,/address/zipCode"
}
className="panelTextField"
value={uniqueKey}
@@ -803,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, ""] })}
>
{t(Keys.panes.addCollection.addUniqueKey)}
Add unique key
</ActionButton>
</Stack>
)}
@@ -824,7 +824,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
className="panelRadioBtn"
checked={this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label={t(Keys.panes.addCollection.enableAnalyticalStore)}
aria-label="Enable analytical store"
aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
@@ -833,13 +833,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.on)}</span>
<span className="panelRadioBtnLabel">On</span>
<input
className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label={t(Keys.panes.addCollection.disableAnalyticalStore)}
aria-label="Disable analytical store"
aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
@@ -848,28 +848,26 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.off)}</span>
<span className="panelRadioBtnLabel">Off</span>
</div>
</Stack>
{!isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing">
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.addCollection.analyticalStoreSynapseLinkRequired, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}{" "}
<br />
Azure Synapse Link is required for creating an analytical store{" "}
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
<Link
href="https://aka.ms/cosmosdb-synapselink"
target="_blank"
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
className="capacitycalculator-link"
>
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
<DefaultButton
text={t(Keys.panes.addCollection.enable)}
text="Enable"
onClick={() => this.props.explorer.openEnableSynapseLinkDialog()}
style={{ height: 27, width: 80 }}
styles={{ label: { fontSize: 12 } }}
@@ -881,7 +879,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowVectorSearchParameters() && (
<Stack>
<CollapsibleSectionComponent
title={t(Keys.panes.addCollection.containerVectorPolicy)}
title="Container Vector Policy"
isExpandedByDefault={false}
onExpand={() => {
scrollToSection("collapsibleVectorPolicySectionContent");
@@ -909,7 +907,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowFullTextSearchParameters() && (
<Stack>
<CollapsibleSectionComponent
title={t(Keys.panes.addCollection.containerFullTextSearchPolicy)}
title="Container Full Text Search Policy"
isExpandedByDefault={false}
onExpand={() => {
scrollToSection("collapsibleFullTextPolicySectionContent");
@@ -938,7 +936,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
{!isFabricNative() && userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent
title={t(Keys.panes.addCollection.advanced)}
title="Advanced"
isExpandedByDefault={false}
onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
@@ -951,23 +949,23 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addCollection.indexing)}
Indexing
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={t(Keys.panes.addCollection.mongoIndexingTooltip)}
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={t(Keys.panes.addCollection.mongoIndexingTooltip)}
ariaLabel="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
/>
</TooltipHost>
</Stack>
<Checkbox
label={t(Keys.panes.addCollection.createWildcardIndex)}
label="Create a Wildcard Index on all fields"
checked={this.state.createMongoWildCardIndex}
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -989,7 +987,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{userContext.apiType === "SQL" && (
<Stack className="panelGroupSpacing">
<Checkbox
label={t(Keys.panes.addCollection.legacySdkCheckbox)}
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
checked={this.state.useHashV1}
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -1006,9 +1004,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
/>
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
<Icon iconName="InfoSolid" className="removeIcon" /> {t(Keys.panes.addCollection.legacySdkInfo)}{" "}
<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.{" "}
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
</Stack>
@@ -1019,7 +1019,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</div>
{!this.props.isCopyJobFlow && (
<PanelFooterComponent buttonLabel={t(Keys.common.ok)} isButtonDisabled={this.state.isThroughputCapExceeded} />
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={this.state.isThroughputCapExceeded} />
)}
{this.state.isExecuting && (
@@ -1045,7 +1045,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
progressTrack: { backgroundColor: "#A6A6A6" },
progressBar: { background: "white" },
}}
label={t(Keys.panes.addCollection.addingSampleDataSet)}
label="Adding sample data set"
/>
</TeachingBubble>
)}
@@ -1151,8 +1151,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
private getFreeTierIndexingText(): string {
return this.state.enableIndexing
? t(Keys.panes.addCollection.indexingOnInfo)
: t(Keys.panes.addCollection.indexingOffInfo);
? "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.";
}
private getPartitionKeySubtext(): string {
@@ -1250,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
? t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly)
: t(Keys.panes.addCollection.acknowledgeSpendErrorDaily);
? "Please acknowledge the estimated monthly spend."
: "Please acknowledge the estimated daily spend.";
this.setState({ errorMessage });
return false;
}
if (throughput > CollectionCreation.MaxRUPerPartition && !this.state.isSharded) {
this.setState({ errorMessage: t(Keys.panes.addCollection.unshardedMaxRuError) });
this.setState({ errorMessage: "Unsharded collections support up to 10,000 RUs" });
return false;
}
@@ -1271,12 +1271,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
if (this.shouldShowVectorSearchParameters()) {
if (!this.state.vectorPolicyValidated) {
this.setState({ errorMessage: t(Keys.panes.addCollection.vectorPolicyError) });
this.setState({ errorMessage: "Please fix errors in container vector policy" });
return false;
}
if (!this.state.fullTextPolicyValidated) {
this.setState({ errorMessage: t(Keys.panes.addCollection.fullTextSearchPolicyError) });
this.setState({ errorMessage: "Please fix errors in container full text search polilcy" });
return false;
}
}
@@ -1474,4 +1474,19 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
TelemetryProcessor.traceFailure(Action.CreateCollection, failureTelemetryData, startKey);
}
}
private onRenderDatabaseOption = (
option?: IDropdownOption,
defaultRender?: (props?: IDropdownOption) => JSX.Element,
): JSX.Element | null => {
if (!option) {
return null;
}
return (
<div data-testid={`database-option-${option.key}`}>
{defaultRender ? defaultRender(option) : <span>{option.text}</span>}
</div>
);
};
}

View File

@@ -3,33 +3,28 @@ 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 t(Keys.panes.addCollectionUtility.shardKeyTooltip);
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. Its critical to choose a field that will evenly distribute your data.";
}
let tooltipText = t(Keys.panes.addCollectionUtility.partitionKeyTooltip, {
partitionKeyName: getPartitionKeyName(true),
});
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.`;
if (userContext.apiType === "SQL") {
tooltipText += t(Keys.panes.addCollectionUtility.partitionKeyTooltipSqlSuffix);
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
}
return tooltipText;
}
export function getPartitionKeyName(isLowerCase?: boolean): string {
const partitionKeyName =
userContext.apiType === "Mongo"
? t(Keys.panes.addCollectionUtility.shardKeyLabel)
: t(Keys.panes.addCollectionUtility.partitionKeyLabel);
const partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
}
@@ -37,19 +32,19 @@ export function getPartitionKeyName(isLowerCase?: boolean): string {
export function getPartitionKeyPlaceHolder(index?: number): string {
switch (userContext.apiType) {
case "Mongo":
return t(Keys.panes.addCollectionUtility.shardKeyPlaceholder);
return "e.g., categoryId";
case "Gremlin":
return t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderDefault);
return "e.g., /address";
case "SQL":
return `${
index === undefined
? t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderFirst)
? "Required - first partition key e.g., /TenantId"
: index === 0
? t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderSecond)
: t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderThird)
? "second partition key e.g., /UserId"
: "third partition key e.g., /SessionId"
}`;
default:
return t(Keys.panes.addCollectionUtility.partitionKeyPlaceholderGraph);
return "e.g., /address/zipCode";
}
}
@@ -74,12 +69,13 @@ export function isFreeTierAccount(): boolean {
}
export function UniqueKeysHeader(): JSX.Element {
const tooltipContent = t(Keys.panes.addCollectionUtility.uniqueKeysTooltip);
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.";
return (
<Stack horizontal style={{ marginBottom: -2 }}>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addCollectionUtility.uniqueKeysLabel)}
Unique keys
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
@@ -103,11 +99,12 @@ export function shouldShowAnalyticalStoreOptions(): boolean {
}
export function AnalyticalStoreHeader(): JSX.Element {
const tooltipContent = t(Keys.panes.addCollectionUtility.analyticalStoreTooltip);
const tooltipContent =
"Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.";
return (
<Stack horizontal style={{ marginBottom: -2 }}>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.addCollectionUtility.analyticalStoreLabel)}
Analytical Store
</Text>
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
@@ -119,13 +116,14 @@ export function AnalyticalStoreHeader(): JSX.Element {
export function AnalyticalStorageContent(): JSX.Element {
return (
<Text variant="small">
{t(Keys.panes.addCollectionUtility.analyticalStoreDescription)}{" "}
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting
the performance of transactional workloads.{" "}
<Link
aria-label={Constants.ariaLabelForLearnMoreLink.AnalyticalStore}
target="_blank"
href="https://aka.ms/analytical-store-overview"
>
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
);
@@ -157,9 +155,10 @@ export function scrollToSection(id: string): void {
export function ContainerVectorPolicyTooltipContent(): JSX.Element {
return (
<Text variant="small">
{t(Keys.panes.addCollectionUtility.vectorPolicyTooltip)}{" "}
Describe any properties in your data that contain vectors, so that they can be made available for similarity
queries.{" "}
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
{t(Keys.common.learnMore)}
Learn more
</Link>
</Text>
);

View File

@@ -482,8 +482,10 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
}
variant="small"
>
Azure Synapse Link is required for creating an analytical store container. Enable Synapse Link for this Cosmos DB account.
Azure Synapse Link is required for creating an analytical store
container
. Enable Synapse Link for this Cosmos DB account.
<br />
<StyledLinkBase
aria-label="Learn more about Azure Synapse Link."
@@ -606,8 +608,7 @@ 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"

View File

@@ -1,7 +1,5 @@
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";
@@ -42,18 +40,15 @@ 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
? t(Keys.panes.addDatabase.keyspaceIdLabel)
: t(Keys.panes.addDatabase.databaseIdLabel);
const databaseIdPlaceHolder: string = t(Keys.panes.addDatabase.databaseIdPlaceholder, { databaseLabel });
const databaseIdLabel: string = isCassandraAccount ? "Keyspace id" : "Database id";
const databaseIdPlaceHolder: string = isCassandraAccount ? "Type a new keyspace id" : "Type a new database id";
const [databaseId, setDatabaseId] = useState<string>("");
const databaseIdTooltipText = t(Keys.panes.addDatabase.databaseTooltip, { databaseLabel, collectionsLabel });
const databaseIdTooltipText = `A ${
isCassandraAccount ? "keyspace" : "database"
} is a logical container of one or more ${isCassandraAccount ? "tables" : "collections"}`;
const databaseLevelThroughputTooltipText = t(Keys.panes.addDatabase.shareThroughputTooltip, {
databaseLabel,
collectionsLabel,
});
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
getNewDatabaseSharedThroughputDefault(),
);
@@ -149,15 +144,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(t(Keys.panes.addDatabase.greaterThanError, { minValue: AutoPilotUtils.autoPilotThroughput1K }));
setFormErrors(
`Please enter a value greater than ${AutoPilotUtils.autoPilotThroughput1K} for autopilot throughput`,
);
return false;
}
}
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
setFormErrors(
t(Keys.panes.addDatabase.acknowledgeSpendError, { period: isAutoscaleSelected ? "monthly" : "daily" }),
);
setFormErrors(`Please acknowledge the estimated ${isAutoscaleSelected ? "monthly" : "daily"} spend.`);
return false;
}
@@ -174,7 +169,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
const props: RightPaneFormProps = {
formError: formErrors,
isExecuting,
submitButtonText: t(Keys.common.ok),
submitButtonText: "OK",
isSubmitButtonDisabled: isThroughputCapExceeded,
onSubmit,
};
@@ -192,7 +187,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
messageType="info"
showErrorDetails={false}
link={Constants.Urls.freeTierInformation}
linkText={t(Keys.common.learnMore)}
linkText="Learn more"
/>
)}
<div className="panelMainContent">

View File

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

View File

@@ -2,16 +2,14 @@ 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";
@@ -73,8 +71,8 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
const errorMessage =
isNewKeySpaceAutoscale || isTableAutoscale
? t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly)
: t(Keys.panes.addCollection.acknowledgeSpendErrorDaily);
? "Please acknowledge the estimated monthly spend."
: "Please acknowledge the estimated daily spend.";
setFormError(errorMessage);
return;
}
@@ -151,7 +149,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
const props: RightPaneFormProps = {
formError,
isExecuting,
submitButtonText: t(Keys.common.ok),
submitButtonText: "OK",
isSubmitButtonDisabled: isThroughputCapExceeded,
onSubmit,
};
@@ -163,8 +161,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.cassandraAddCollection.keyspaceLabel)}{" "}
<InfoTooltip>{t(Keys.panes.cassandraAddCollection.keyspaceTooltip)}</InfoTooltip>
Keyspace name <InfoTooltip>Select an existing keyspace or enter a new keyspace id.</InfoTooltip>
</Text>
</Stack>
@@ -182,7 +179,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
setExistingKeyspaceId("");
}}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.createNew)}</span>
<span className="panelRadioBtnLabel">Create new</span>
<input
className="panelRadioBtn"
@@ -196,7 +193,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
setIsKeyspaceShared(false);
}}
/>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.useExisting)}</span>
<span className="panelRadioBtnLabel">Use existing</span>
</Stack>
{keyspaceCreateNew && (
@@ -278,9 +275,9 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
{t(Keys.panes.cassandraAddCollection.tableIdLabel)}{" "}
Enter CQL command to create the table.{" "}
<Link className="underlinedLink" href="https://aka.ms/cassandra-create-table" target="_blank">
{t(Keys.common.learnMore)}
Learn More
</Link>
</Text>
</Stack>
@@ -298,7 +295,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={t(Keys.panes.cassandraAddCollection.enterTableId)}
placeholder="Enter table Id"
size={20}
value={tableId}
onChange={(e, newValue) => setTableId(newValue)}
@@ -310,7 +307,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
multiline
id="editor-area"
rows={5}
ariaLabel={t(Keys.panes.cassandraAddCollection.tableSchemaAriaLabel)}
ariaLabel="Table schema"
value={userTableQuery}
onChange={(e, newValue) => setUserTableQuery(newValue)}
/>
@@ -321,12 +318,17 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
<input
type="checkbox"
id="tableSharedThroughput"
title={t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughput)}
title="Provision dedicated throughput for this table"
checked={dedicateTableThroughput}
onChange={(e) => setDedicateTableThroughput(e.target.checked)}
/>
<span>{t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughput)}</span>
<InfoTooltip>{t(Keys.panes.cassandraAddCollection.provisionDedicatedThroughputTooltip)}</InfoTooltip>
<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>
</Stack>
)}
{!isServerlessAccount() && (!isKeyspaceShared || dedicateTableThroughput) && (

View File

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

View File

@@ -4,8 +4,6 @@ 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";
@@ -84,14 +82,14 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
const notebookContentItem = await copyNotebook(selectedLocation);
if (!notebookContentItem) {
throw new Error(t(Keys.panes.copyNotebook.uploadFailedError, { name }));
throw new Error(`Failed to upload ${name}`);
}
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${name} to ${destination}`);
closeSidePanel();
} catch (error) {
const errorMessage = getErrorMessage(error);
setFormError(t(Keys.panes.copyNotebook.copyFailedError, { name, destination }));
setFormError(`Failed to copy ${name} to ${destination}`);
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError);
} finally {
clearMessage && clearMessage();
@@ -138,7 +136,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
const props: RightPaneFormProps = {
formError,
isExecuting: isExecuting,
submitButtonText: t(Keys.common.ok),
submitButtonText: "OK",
onSubmit: () => submit(),
};

View File

@@ -12,8 +12,6 @@ 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";
@@ -98,8 +96,8 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
return options;
};
const dropDownProps: IDropdownProps = {
label: t(Keys.panes.copyNotebook.location),
ariaLabel: t(Keys.panes.copyNotebook.locationAriaLabel),
label: "Location",
ariaLabel: "Location",
placeholder: "Select an option",
onRenderTitle: onRenderDropDownTitle,
onRenderOption: onRenderDropDownOption,
@@ -111,7 +109,7 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
<div className="paneMainContent">
<Stack tokens={{ childrenGap: 10 }}>
<Stack.Item>
<Label htmlFor="notebookName">{t(Keys.panes.copyNotebook.name)}</Label>
<Label htmlFor="notebookName">Name</Label>
<Text id="notebookName">{name}</Text>
</Stack.Item>

View File

@@ -4,8 +4,6 @@ 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";
@@ -36,15 +34,12 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
useDatabases.getState().isLastCollection() && !useDatabases.getState().findSelectedDatabase()?.isDatabaseShared();
const collectionName = getCollectionName().toLocaleLowerCase();
const paneTitle = t(Keys.panes.deleteCollection.panelTitle, { collectionName });
const paneTitle = "Delete " + collectionName;
const onSubmit = async (): Promise<void> => {
const collection = useSelectedNode.getState().findSelectedCollection();
if (!collection || inputCollectionName !== collection.id()) {
const errorMessage = t(Keys.panes.deleteCollection.inputMismatch, {
input: inputCollectionName,
selectedId: collection.id(),
});
const errorMessage = "Input id " + inputCollectionName + " does not match the selected " + collection.id();
setFormError(errorMessage);
NotificationConsoleUtils.logConsoleError(
`Error while deleting ${collectionName} ${collection.id()}: ${errorMessage}`,
@@ -111,23 +106,18 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
const props: RightPaneFormProps = {
formError: formError,
isExecuting,
submitButtonText: t(Keys.common.ok),
submitButtonText: "OK",
onSubmit,
};
const confirmContainer = t(Keys.panes.deleteCollection.confirmPrompt, {
collectionName: collectionName.toLowerCase(),
});
const reasonInfo =
t(Keys.panes.deleteCollection.feedbackTitle) +
" " +
t(Keys.panes.deleteCollection.feedbackReason, { collectionName });
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}?`;
return (
<RightPaneForm {...props}>
<div className="panelFormWrapper">
<div className="panelMainContent">
<div className="confirmDeleteInput">
<span className="mandatoryStar">* </span>
<Text variant="small">{confirmContainer}</Text>
<Text variant="small">Confirm by typing the {collectionName.toLowerCase()} id</Text>
<TextField
id="confirmCollectionId"
autoFocus
@@ -143,10 +133,10 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
{shouldRecordFeedback() && (
<div className="deleteCollectionFeedback">
<Text variant="small" block>
{t(Keys.panes.deleteCollection.feedbackTitle)}
Help us improve Azure Cosmos DB!
</Text>
<Text variant="small" block>
{t(Keys.panes.deleteCollection.feedbackReason, { collectionName })}
What is the reason why you are deleting this {collectionName}?
</Text>
<TextField
id="deleteCollectionFeedbackInput"

View File

@@ -34,7 +34,9 @@ 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

View File

@@ -5,8 +5,6 @@ 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";
@@ -40,19 +38,11 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
const submit = async (): Promise<void> => {
if (selectedDatabase?.id() && databaseInput !== selectedDatabase.id()) {
setFormError(
t(Keys.panes.deleteDatabase.inputMismatch, {
databaseName: getDatabaseName(),
input: databaseInput,
selectedId: selectedDatabase.id(),
}),
`Input ${getDatabaseName()} name "${databaseInput}" does not match the selected ${getDatabaseName()} "${selectedDatabase.id()}"`,
);
logConsoleError(`Error while deleting ${getDatabaseName()} ${selectedDatabase && selectedDatabase.id()}`);
logConsoleError(
t(Keys.panes.deleteDatabase.inputMismatch, {
databaseName: getDatabaseName(),
input: databaseInput,
selectedId: selectedDatabase.id(),
}),
`Input ${getDatabaseName()} name "${databaseInput}" does not match the selected ${getDatabaseName()} "${selectedDatabase.id()}"`,
);
return;
}
@@ -124,20 +114,18 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
const props: RightPaneFormProps = {
formError,
isExecuting: isLoading,
submitButtonText: t(Keys.common.ok),
submitButtonText: "OK",
onSubmit: () => submit(),
};
const errorProps: PanelInfoErrorProps = {
messageType: "warning",
showErrorDetails: false,
message: t(Keys.panes.deleteDatabase.warningMessage),
message:
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
};
const confirmDatabase = t(Keys.panes.deleteDatabase.confirmPrompt, { databaseName: getDatabaseName() });
const reasonInfo =
t(Keys.panes.deleteDatabase.feedbackTitle) +
" " +
t(Keys.panes.deleteDatabase.feedbackReason, { databaseName: getDatabaseName() });
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()}?`;
return (
<RightPaneForm {...props}>
{!formError && <PanelInfoErrorComponent {...errorProps} />}
@@ -160,10 +148,10 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
{isLastNonEmptyDatabase() && (
<div className="deleteDatabaseFeedback">
<Text variant="small" block>
{t(Keys.panes.deleteDatabase.feedbackTitle)}
Help us improve Azure Cosmos DB!
</Text>
<Text variant="small" block>
{t(Keys.panes.deleteDatabase.feedbackReason, { databaseName: getDatabaseName() })}
What is the reason why you are deleting this {getDatabaseName()}?
</Text>
<TextField
id="deleteDatabaseFeedbackInput"

View File

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

View File

@@ -11,8 +11,6 @@ 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 = [
@@ -76,7 +74,7 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
<Image
{...imageProps}
src={EntityCancelIcon}
alt={t(Keys.panes.executeStoredProcedure.deleteParam)}
alt="Delete param"
id="deleteparam"
role="button"
onClick={onDeleteParamKeyPress}
@@ -86,7 +84,7 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
<Image
{...imageProps}
src={AddPropertyIcon}
alt={t(Keys.panes.executeStoredProcedure.addParam)}
alt="Add param"
id="addparam"
role="button"
onClick={onAddNewParamKeyPress}

View File

@@ -1,7 +1,5 @@
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";
@@ -19,7 +17,7 @@ export const GraphStylingPanel: FunctionComponent<GraphStylingProps> = ({
}: GraphStylingProps): JSX.Element => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const buttonLabel = t(Keys.common.ok);
const buttonLabel = "Ok";
const submit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();

View File

@@ -5,8 +5,6 @@ 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";
@@ -35,8 +33,8 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
const submit = async (): Promise<void> => {
setFormError("");
if (!selectedFiles || selectedFiles.length === 0) {
setFormError(t(Keys.panes.loadQuery.noFileSpecifiedError));
logConsoleError(t(Keys.panes.loadQuery.noFileSpecifiedError));
setFormError("No file specified");
logConsoleError("Could not load query -- No file specified. Please input a file.");
return;
}
@@ -50,7 +48,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
setLoadingFalse();
} catch (error) {
setLoadingFalse();
setFormError(t(Keys.panes.loadQuery.failedToLoadQueryError));
setFormError("Failed to load query");
logConsoleError(`Failed to load query from file ${file.name}: ${error}`);
}
};
@@ -73,7 +71,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
};
reader.onerror = (): void => {
setFormError(t(Keys.panes.loadQuery.failedToLoadQueryFromFileError, { fileName: file.name }));
setFormError("Failed to load query");
logConsoleError(`Failed to load query from file ${file.name}`);
};
return reader.readAsText(file);
@@ -81,7 +79,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
const props: RightPaneFormProps = {
formError: formError,
isExecuting: isLoading,
submitButtonText: t(Keys.common.load),
submitButtonText: "Load",
onSubmit: () => submit(),
};
@@ -92,7 +90,7 @@ export const LoadQueryPane: FunctionComponent = (): JSX.Element => {
<Stack horizontal>
<TextField
id="confirmCollectionId"
label={t(Keys.panes.loadQuery.selectFilesToOpen)}
label="Select a query document"
value={selectedFileName}
autoFocus
readOnly

View File

@@ -1,8 +1,6 @@
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";
@@ -43,7 +41,7 @@ export const NewVertexPanel: FunctionComponent<INewVertexPanelProps> = ({
const props: RightPaneFormProps = {
formError: errorMessage,
isExecuting: isLoading,
submitButtonText: t(Keys.common.ok),
submitButtonText: "OK",
onSubmit: () => submit(),
};

View File

@@ -1,6 +1,4 @@
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";
@@ -22,17 +20,13 @@ 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={t(Keys.panes.panelInfo.information)} />
);
let icon: JSX.Element = <Icon iconName="InfoSolid" className="panelLargeInfoIcon" aria-label="Infomation" />;
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={t(Keys.panes.panelInfo.information)} />
);
icon = <Icon iconName="InfoSolid" className="panelLargeInfoIcon" aria-label="Infomation" />;
}
return (
@@ -49,7 +43,7 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
</Text>
{showErrorDetails && (
<a className="paneErrorLink" role="button" onClick={expandConsole} tabIndex={0} onKeyPress={expandConsole}>
{t(Keys.panes.panelInfo.moreDetails)}
More details
</a>
)}
</span>

View File

@@ -5,8 +5,6 @@ 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";
@@ -93,7 +91,7 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
let startKey: number;
if (!notebookName || !notebookDescription || !author || !imageSrc) {
setFormError(t(Keys.panes.publishNotebook.publishFailedError, { notebookName }));
setFormError(`Failed to publish ${notebookName} to gallery`);
setFormErrorDetail("Name, description, author and cover image are required");
createFormError(formError, formErrorDetail, "PublishNotebookPaneAdapter/submit");
setIsExecuting(false);
@@ -145,11 +143,7 @@ export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> =
);
const errorMessage = getErrorMessage(error);
setFormError(
t(Keys.panes.publishNotebook.publishFailedError, {
notebookName: FileSystemUtil.stripExtension(notebookName, "ipynb"),
}),
);
setFormError(`Failed to publish ${FileSystemUtil.stripExtension(notebookName, "ipynb")} to gallery`);
setFormErrorDetail(`${errorMessage}`);
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", formError);
return;

View File

@@ -1,8 +1,6 @@
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";
@@ -59,11 +57,13 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
const maxImageSizeInMib = 1.5;
const descriptionPara1 = t(Keys.panes.publishNotebook.publishDescription);
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 descriptionPara2 = t(Keys.panes.publishNotebook.publishPrompt, {
name: FileSystemUtil.stripExtension(notebookName, "ipynb"),
});
const descriptionPara2 = `Would you like to publish and share "${FileSystemUtil.stripExtension(
notebookName,
"ipynb",
)}" to the gallery?`;
const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url];
if (onTakeSnapshot) {
@@ -74,9 +74,9 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
}
const thumbnailSelectorProps: IDropdownProps = {
label: t(Keys.panes.publishNotebook.coverImage),
label: "Cover image",
selectedKey: type,
ariaLabel: t(Keys.panes.publishNotebook.coverImage),
ariaLabel: "Cover image",
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(t(Keys.panes.publishNotebook.outputDoesNotExist)));
firstOutputErrorHandler(new Error("Output does not exist for any of the cells."));
}
}
setType(options.text);
@@ -107,8 +107,8 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
};
const thumbnailUrlProps: ITextFieldProps = {
label: t(Keys.panes.publishNotebook.coverImageUrl),
ariaLabel: t(Keys.panes.publishNotebook.coverImageUrl),
label: "Cover image url",
ariaLabel: "Cover image url",
required: true,
onChange: (event, newValue) => {
setImageSrc(newValue);
@@ -116,7 +116,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
};
const firstOutputErrorHandler = (error: Error) => {
const formError = t(Keys.panes.publishNotebook.failedToCaptureOutput);
const formError = "Failed to capture first output";
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 = t(Keys.panes.publishNotebook.failedToConvertError, { fileName: file.name });
const formError = `Failed to convert ${file.name} to base64 format`;
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 = t(Keys.panes.publishNotebook.failedToUploadError, { fileName: file.name });
const formError = `Failed to upload ${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={t(Keys.panes.publishNotebook.name)}
ariaLabel={t(Keys.panes.publishNotebook.name)}
label="Name"
ariaLabel="Name"
defaultValue={FileSystemUtil.stripExtension(notebookName, "ipynb")}
required
onChange={(event, newValue) => {
@@ -198,8 +198,8 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
<Stack.Item>
<TextField
label={t(Keys.panes.publishNotebook.description)}
ariaLabel={t(Keys.panes.publishNotebook.description)}
label="Description"
ariaLabel="Description"
multiline
rows={3}
required
@@ -211,9 +211,9 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
<Stack.Item>
<TextField
label={t(Keys.panes.publishNotebook.tags)}
ariaLabel={t(Keys.panes.publishNotebook.tags)}
placeholder={t(Keys.panes.publishNotebook.tagsPlaceholder)}
label="Tags"
ariaLabel="Tags"
placeholder="Optional tag 1, Optional tag 2"
onChange={(event, newValue) => {
setNotebookTags(newValue);
}}
@@ -227,7 +227,7 @@ export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPane
<Stack.Item>{renderThumbnailSelectors(type)}</Stack.Item>
<Stack.Item>
<Text>{t(Keys.panes.publishNotebook.preview)}</Text>
<Text>Preview</Text>
</Stack.Item>
<Stack.Item>
<GalleryCardComponent

View File

@@ -4,8 +4,6 @@ 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";
@@ -30,27 +28,27 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
const [formError, setFormError] = useState<string>("");
const [queryName, setQueryName] = useState<string>("");
const setupSaveQueriesText = t(Keys.panes.saveQuery.setupCostMessage, { databaseName: SavedQueries.DatabaseName });
const title = t(Keys.panes.saveQuery.panelTitle);
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 isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled);
const submit = async (): Promise<void> => {
setFormError("");
if (!isSaveQueryEnabled()) {
setFormError("Cannot save query");
logConsoleError(t(Keys.panes.saveQuery.accountNotSetupError));
logConsoleError("Failed to save query: account not setup to save queries");
}
const queryTab = useTabs.getState().activeTab as NewQueryTab;
const query: string = queryToSave || queryTab?.iTabAccessor.onSaveClickEvent();
if (!queryName || queryName.length === 0) {
setFormError(t(Keys.panes.saveQuery.noQueryNameError));
logConsoleError(t(Keys.panes.saveQuery.noQueryNameError));
setFormError("No query name specified");
logConsoleError("Could not save query -- No query name specified. Please specify a query name.");
return;
} else if (!query || query.length === 0) {
setFormError(t(Keys.panes.saveQuery.invalidQueryContentError));
logConsoleError(t(Keys.panes.saveQuery.invalidQueryContentError));
setFormError("Invalid query content specified");
logConsoleError("Could not save query -- Invalid query content specified. Please enter query content.");
return;
}
@@ -82,8 +80,8 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
} catch (error) {
setLoadingFalse();
const errorMessage = getErrorMessage(error);
setFormError(t(Keys.panes.saveQuery.failedToSaveQueryError, { queryName }));
logConsoleError(t(Keys.panes.saveQuery.failedToSaveQueryError, { queryName }) + ": " + errorMessage);
setFormError("Failed to save query");
logConsoleError(`Failed to save query: ${errorMessage}`);
traceFailure(
Action.SaveQuery,
{
@@ -128,8 +126,8 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
},
startKey,
);
setFormError(t(Keys.panes.saveQuery.failedToSetupContainerError));
logConsoleError(t(Keys.panes.saveQuery.failedToSetupContainerError) + ": " + errorMessage);
setFormError("Failed to setup a container for saved queries");
logConsoleError(`Failed to setup a container for saved queries: ${errorMessage}`);
} finally {
setLoadingFalse();
}
@@ -138,7 +136,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
const props: RightPaneFormProps = {
formError: formError,
isExecuting: isLoading,
submitButtonText: isSaveQueryEnabled() ? t(Keys.common.save) : t(Keys.panes.saveQuery.completeSetup),
submitButtonText: isSaveQueryEnabled() ? "Save" : "Complete setup",
onSubmit: () => {
isSaveQueryEnabled() ? submit() : setupQueries();
},
@@ -162,7 +160,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
) : (
<TextField
id="saveQueryInput"
label={t(Keys.panes.saveQuery.name)}
label="Name"
autoFocus
styles={{ fieldGroup: { width: 300 } }}
onChange={(event, newInput?: string) => {

View File

@@ -24,8 +24,6 @@ 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,
@@ -237,7 +235,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
const regionOptions: IDropdownOption[] = [];
regionOptions.push({
key: userContext?.databaseAccount?.properties?.documentEndpoint,
text: t(Keys.panes.settings.globalDefault),
text: `Global (Default)`,
data: {
isGlobal: true,
writeEnabled: true,
@@ -248,7 +246,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
uniqueAccountRegions.add(loc.locationName);
regionOptions.push({
key: loc.documentEndpoint,
text: `${loc.locationName} ${t(Keys.panes.settings.readWrite)}`,
text: `${loc.locationName} (Read/Write)`,
data: {
isGlobal: false,
writeEnabled: true,
@@ -261,7 +259,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
uniqueAccountRegions.add(loc.locationName);
regionOptions.push({
key: loc.documentEndpoint,
text: `${loc.locationName} ${t(Keys.panes.settings.read)}`,
text: `${loc.locationName} (Read)`,
data: {
isGlobal: false,
writeEnabled: false,
@@ -319,9 +317,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
authError instanceof msalAuthError &&
authError.errorCode === msalBrowserAuthErrorMessage.popUpWindowError.code
) {
logConsoleError(t(Keys.panes.settings.popupsDisabledError));
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`,
);
} else {
logConsoleError(t(Keys.panes.settings.failedToAcquireTokenError));
logConsoleError(
`"Failed to acquire authorization token automatically. Please click on "Login for Entra ID" button to enable Entra ID RBAC operations`,
);
}
}
} else {
@@ -483,33 +485,33 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
const genericPaneProps: RightPaneFormProps = {
formError: "",
isExecuting,
submitButtonText: t(Keys.common.apply),
submitButtonText: "Apply",
onSubmit: () => handlerOnSubmit(),
};
const pageOptionList: IChoiceGroupOption[] = [
{ key: Constants.Queries.CustomPageOption, text: t(Keys.panes.settings.custom) },
{ key: Constants.Queries.UnlimitedPageOption, text: t(Keys.panes.settings.unlimited) },
{ key: Constants.Queries.CustomPageOption, text: "Custom" },
{ key: Constants.Queries.UnlimitedPageOption, text: "Unlimited" },
];
const graphAutoOptionList: IChoiceGroupOption[] = [
{ key: "false", text: t(Keys.panes.settings.graph) },
{ key: "true", text: t(Keys.panes.settings.json) },
{ key: "false", text: "Graph" },
{ key: "true", text: "JSON" },
];
const priorityLevelOptionList: IChoiceGroupOption[] = [
{ key: Constants.PriorityLevel.Low, text: t(Keys.panes.settings.low) },
{ key: Constants.PriorityLevel.High, text: t(Keys.panes.settings.high) },
{ key: Constants.PriorityLevel.Low, text: "Low" },
{ key: Constants.PriorityLevel.High, text: "High" },
];
const dataPlaneRBACOptionsList: IChoiceGroupOption[] = [
{ 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"]) },
{ key: Constants.RBACOptions.setAutomaticRBACOption, text: "Automatic" },
{ key: Constants.RBACOptions.setTrueRBACOption, text: "True" },
{ key: Constants.RBACOptions.setFalseRBACOption, text: "False" },
];
const defaultQueryResultsViewOptionList: IChoiceGroupOption[] = [
{ key: SplitterDirection.Vertical, text: t(Keys.tabs.query.vertical) },
{ key: SplitterDirection.Horizontal, text: t(Keys.tabs.query.horizontal) },
{ key: SplitterDirection.Vertical, text: "Vertical" },
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
];
const mongoGuidRepresentationDropdownOptions: IDropdownOption[] = [
@@ -722,12 +724,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowQueryPageOptions && (
<AccordionItem value="1">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.pageOptions)}</div>
<div className={styles.header}>Page Options</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.pageOptionsDescription)}
Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as
many query results per page.
</div>
<ChoiceGroup
ariaLabelledBy="pageOptions"
@@ -741,14 +744,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{isCustomPageOptionSelected() && (
<div className="tabcontent">
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.queryResultsPerPage)}{" "}
Query results per page{" "}
<InfoTooltip className={styles.headerIcon}>
{t(Keys.panes.settings.queryResultsPerPageTooltip)}
Enter the number of query results that should be shown per page.
</InfoTooltip>
</div>
<SpinButton
ariaLabel={t(Keys.panes.settings.customQueryItemsPerPage)}
ariaLabel="Custom query items per page"
value={"" + customItemPerPage}
onIncrement={(newValue) => {
setCustomItemPerPage(parseInt(newValue) + 1 || customItemPerPage);
@@ -758,8 +761,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
min={1}
step={1}
className="textfontclr"
incrementButtonAriaLabel={t(Keys.common.increaseValueBy1)}
decrementButtonAriaLabel={t(Keys.common.decreaseValueBy1)}
incrementButtonAriaLabel="Increase value by 1"
decrementButtonAriaLabel="Decrease value by 1"
styles={spinButtonStyles}
/>
</div>
@@ -771,19 +774,20 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{showEnableEntraIdRbac && (
<AccordionItem value="2">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.entraIdRbac)}</div>
<div className={styles.header}>Enable Entra ID RBAC</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.entraIdRbacDescription)}
Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra
ID RBAC.
<a
href="https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#use-data-explorer"
target="_blank"
rel="noopener noreferrer"
>
{" "}
{t(Keys.common.learnMore)}{" "}
Learn more{" "}
</a>
</div>
<ChoiceGroup
@@ -800,17 +804,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{userContext.apiType === "SQL" && userContext.authType === AuthType.AAD && !isFabric() && (
<AccordionItem value="3">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.regionSelection)}</div>
<div className={styles.header}>Region Selection</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.regionSelectionDescription)}
Changes region the Cosmos Client uses to access account.
</div>
<div>
<span className={styles.subHeader}>{t(Keys.panes.settings.selectRegion)}</span>
<span className={styles.subHeader}>Select Region</span>
<InfoTooltip className={styles.headerIcon}>
{t(Keys.panes.settings.selectRegionTooltip)}
Changes the account endpoint used to perform client operations.
</InfoTooltip>
</div>
<Dropdown
@@ -861,16 +865,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
<>
<AccordionItem value="4">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.queryTimeout)}</div>
<div className={styles.header}>Query Timeout</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.queryTimeoutDescription)}
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.
</div>
<Toggle
styles={toggleStyles}
label={t(Keys.panes.settings.enableQueryTimeout)}
label="Enable query timeout"
onChange={handleOnQueryTimeoutToggleChange}
defaultChecked={queryTimeoutEnabled}
/>
@@ -878,18 +883,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{queryTimeoutEnabled && (
<div className={styles.settingsSectionContainer}>
<SpinButton
label={t(Keys.panes.settings.queryTimeoutMs)}
label="Query timeout (ms)"
labelPosition={Position.top}
defaultValue={(queryTimeout || 5000).toString()}
min={100}
step={1000}
onChange={handleOnQueryTimeoutSpinButtonChange}
incrementButtonAriaLabel={t(Keys.panes.settings.increaseValueBy1000)}
decrementButtonAriaLabel={t(Keys.panes.settings.decreaseValueBy1000)}
incrementButtonAriaLabel="Increase value by 1000"
decrementButtonAriaLabel="Decrease value by 1000"
styles={spinButtonStyles}
/>
<Toggle
label={t(Keys.panes.settings.automaticallyCancelQuery)}
label="Automatically cancel query after timeout"
styles={toggleStyles}
onChange={handleOnAutomaticallyCancelQueryToggleChange}
defaultChecked={automaticallyCancelQueryAfterTimeout}
@@ -900,16 +905,16 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionItem>
<AccordionItem value="5">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.ruLimit)}</div>
<div className={styles.header}>RU Limit</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.ruLimitDescription)}
If a query exceeds a configured RU limit, the query will be aborted.
</div>
<Toggle
styles={toggleStyles}
label={t(Keys.panes.settings.enableRuLimit)}
label="Enable RU limit"
onChange={handleOnRUThresholdToggleChange}
defaultChecked={ruThresholdEnabled}
/>
@@ -917,14 +922,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{ruThresholdEnabled && (
<div className={styles.settingsSectionContainer}>
<SpinButton
label={t(Keys.panes.settings.ruLimitLabel)}
label="RU Limit (RU)"
labelPosition={Position.top}
defaultValue={(ruThreshold || DefaultRUThreshold).toString()}
min={1}
step={1000}
onChange={handleOnRUThresholdSpinButtonChange}
incrementButtonAriaLabel={t(Keys.panes.settings.increaseValueBy1000)}
decrementButtonAriaLabel={t(Keys.panes.settings.decreaseValueBy1000)}
incrementButtonAriaLabel="Increase value by 1000"
decrementButtonAriaLabel="Decrease value by 1000"
styles={spinButtonStyles}
/>
</div>
@@ -934,12 +939,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
<AccordionItem value="6">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.defaultQueryResults)}</div>
<div className={styles.header}>Default Query Results View</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.defaultQueryResultsDescription)}
Select the default view to use when displaying query results.
</div>
<ChoiceGroup
ariaLabelledBy="defaultQueryResultsView"
@@ -957,17 +962,17 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{showRetrySettings && (
<AccordionItem value="7">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.retrySettings)}</div>
<div className={styles.header}>Retry Settings</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.retrySettingsDescription)}
Retry policy associated with throttled requests during CosmosDB queries.
</div>
<div>
<span className={styles.subHeader}>{t(Keys.panes.settings.maxRetryAttempts)}</span>
<span className={styles.subHeader}>Max retry attempts</span>
<InfoTooltip className={styles.headerIcon}>
{t(Keys.panes.settings.maxRetryAttemptsTooltip)}
Max number of retries to be performed for a request. Default value 9.
</InfoTooltip>
</div>
<SpinButton
@@ -976,17 +981,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
step={1}
value={"" + retryAttempts}
onChange={handleOnQueryRetryAttemptsSpinButtonChange}
incrementButtonAriaLabel={t(Keys.common.increaseValueBy1)}
decrementButtonAriaLabel={t(Keys.common.decreaseValueBy1)}
incrementButtonAriaLabel="Increase value by 1"
decrementButtonAriaLabel="Decrease value by 1"
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}>{t(Keys.panes.settings.fixedRetryInterval)}</span>
<span className={styles.subHeader}>Fixed retry interval (ms)</span>
<InfoTooltip className={styles.headerIcon}>
{t(Keys.panes.settings.fixedRetryIntervalTooltip)}
Fixed retry interval in milliseconds to wait between each retry ignoring the retryAfter returned
as part of the response. Default value is 0 milliseconds.
</InfoTooltip>
</div>
<SpinButton
@@ -995,17 +1001,18 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
step={1000}
value={"" + retryInterval}
onChange={handleOnRetryIntervalSpinButtonChange}
incrementButtonAriaLabel={t(Keys.panes.settings.increaseValueBy1000)}
decrementButtonAriaLabel={t(Keys.panes.settings.decreaseValueBy1000)}
incrementButtonAriaLabel="Increase value by 1000"
decrementButtonAriaLabel="Decrease value by 1000"
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}>{t(Keys.panes.settings.maxWaitTime)}</span>
<span className={styles.subHeader}>Max wait time (s)</span>
<InfoTooltip className={styles.headerIcon}>
{t(Keys.panes.settings.maxWaitTimeTooltip)}
Max wait time in seconds to wait for a request while the retries are happening. Default value 30
seconds.
</InfoTooltip>
</div>
<SpinButton
@@ -1014,8 +1021,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
step={1}
value={"" + MaxWaitTimeInSeconds}
onChange={handleOnMaxWaitTimeSpinButtonChange}
incrementButtonAriaLabel={t(Keys.common.increaseValueBy1)}
decrementButtonAriaLabel={t(Keys.common.decreaseValueBy1)}
incrementButtonAriaLabel="Increase value by 1"
decrementButtonAriaLabel="Decrease value by 1"
onIncrement={(newValue) =>
setMaxWaitTimeInSeconds(parseInt(newValue) + 1 || MaxWaitTimeInSeconds)
}
@@ -1032,26 +1039,24 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{!isEmulator && (
<AccordionItem value="8">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.enableContainerPagination)}</div>
<div className={styles.header}>Enable container pagination</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.enableContainerPaginationDescription)}
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
ariaLabel={t(Keys.panes.settings.enableContainerPagination)}
ariaLabel="Enable container pagination"
checked={containerPaginationEnabled}
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
label={t(Keys.panes.settings.enableContainerPagination)}
label="Enable container pagination"
onRenderLabel={() => (
<span style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.settings.enableContainerPagination)}
</span>
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable container pagination</span>
)}
/>
</div>
@@ -1061,25 +1066,24 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowCrossPartitionOption && (
<AccordionItem value="9">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.enableCrossPartitionQuery)}</div>
<div className={styles.header}>Enable cross-partition query</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.enableCrossPartitionQueryDescription)}
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>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
ariaLabel={t(Keys.panes.settings.enableCrossPartitionQuery)}
ariaLabel="Enable cross partition query"
checked={crossPartitionQueryEnabled}
onChange={() => setCrossPartitionQueryEnabled(!crossPartitionQueryEnabled)}
onRenderLabel={() => (
<span style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.settings.enableCrossPartitionQuery)}
</span>
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable cross-partition query</span>
)}
/>
</div>
@@ -1089,19 +1093,19 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowEnhancedQueryControl && (
<AccordionItem value="10">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.enhancedQueryControl)}</div>
<div className={styles.header}>Enhanced query control</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.maxDegreeOfParallelismQuery)}
Query up to the max degree of parallelism.
<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"
>
{" "}
{t(Keys.common.learnMore)}{" "}
Learn more{" "}
</a>
</div>
<Checkbox
@@ -1109,13 +1113,11 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
label: { padding: 0 },
}}
className="padding"
ariaLabel={t(Keys.panes.settings.enableQueryControl)}
ariaLabel="EnableQueryControl"
checked={queryControlEnabled}
onChange={() => setQueryControlEnabled(!queryControlEnabled)}
onRenderLabel={() => (
<span style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.settings.enableQueryControl)}
</span>
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable query control</span>
)}
/>
</div>
@@ -1125,12 +1127,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowParallelismOption && (
<AccordionItem value="10">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.maxDegreeOfParallelism)}</div>
<div className={styles.header}>Max degree of parallelism</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.maxDegreeOfParallelismDescription)}
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.
</div>
<SpinButton
min={-1}
@@ -1146,8 +1150,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
setMaxDegreeOfParallelism(parseInt(newValue) - 1 || maxDegreeOfParallelism)
}
onValidate={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) || maxDegreeOfParallelism)}
ariaLabel={t(Keys.panes.settings.maxDegreeOfParallelism)}
label={t(Keys.panes.settings.maxDegreeOfParallelism)}
ariaLabel="Max degree of parallelism"
label="Max degree of parallelism"
styles={spinButtonStyles}
/>
</div>
@@ -1157,12 +1161,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowPriorityLevelOption && (
<AccordionItem value="11">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.priorityLevel)}</div>
<div className={styles.header}>Priority Level</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.priorityLevelDescription)}
Sets the priority level for data-plane requests from Data Explorer when using Priority-Based
Execution. If &quot;None&quot; is selected, Data Explorer will not specify priority level, and the
server-side default priority level will be used.
</div>
<ChoiceGroup
ariaLabelledBy="priorityLevel"
@@ -1178,18 +1184,19 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowGraphAutoVizOption && (
<AccordionItem value="12">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.displayGremlinQueryResults)}&nbsp;</div>
<div className={styles.header}>Display Gremlin query results as:&nbsp;</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.displayGremlinQueryResultsDescription)}
Select Graph to automatically visualize the query results as a Graph or JSON to display the
results as JSON.
</div>
<ChoiceGroup
selectedKey={graphAutoVizDisabled}
options={graphAutoOptionList}
onChange={handleOnGremlinChange}
aria-label={t(Keys.panes.settings.graphAutoVisualization)}
aria-label="Graph Auto-visualization"
/>
</div>
</AccordionPanel>
@@ -1198,25 +1205,25 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowCopilotSampleDBOption && (
<AccordionItem value="13">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.enableSampleDatabase)}</div>
<div className={styles.header}>Enable sample database</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.enableSampleDatabaseDescription)}
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.
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
ariaLabel={t(Keys.panes.settings.enableSampleDbAriaLabel)}
ariaLabel="Enable sample db for query exploration"
checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange}
onRenderLabel={() => (
<span style={{ color: "var(--colorNeutralForeground1)" }}>
{t(Keys.panes.settings.enableSampleDatabase)}
</span>
<span style={{ color: "var(--colorNeutralForeground1)" }}>Enable sample database</span>
)}
/>
</div>
@@ -1226,12 +1233,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{shouldShowMongoGuidRepresentationOption && (
<AccordionItem value="14">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.guidRepresentation)}</div>
<div className={styles.header}>Guid Representation</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
{t(Keys.panes.settings.guidRepresentationDescription)}
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.
</div>
<Dropdown
aria-labelledby="mongoGuidRepresentation"
@@ -1245,7 +1253,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
)}
<AccordionItem value="15">
<AccordionHeader>
<div className={styles.header}>{t(Keys.panes.settings.advancedSettings)}</div>
<div className={styles.header}>Advanced Settings</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
@@ -1275,13 +1283,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
},
}}
className="padding"
ariaLabel={t(Keys.panes.settings.ignorePartitionKey)}
ariaLabel="Ignore partition key on document update"
checked={ignorePartitionKeyOnDocumentUpdate}
onChange={handleOnIgnorePartitionKeyOnDocumentUpdateChange}
label={t(Keys.panes.settings.ignorePartitionKey)}
label="Ignore partition key on document update"
/>
<InfoTooltip className={styles.headerIcon}>
{t(Keys.panes.settings.ignorePartitionKeyTooltip)}
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.
</InfoTooltip>
</Stack>
</div>
@@ -1311,9 +1320,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
}}
onClick={() => {
useDialog.getState().showOkCancelModalDialog(
t(Keys.panes.settings.clearHistory),
"Clear History",
undefined,
t(Keys.panes.settings.clearHistoryConfirm),
"Are you sure you want to proceed?",
() => {
deleteAllStates();
updateUserContext({
@@ -1323,33 +1332,35 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
});
useClientWriteEnabled.setState({ clientWriteEnabled: true });
},
t(Keys.common.cancel),
"Cancel",
undefined,
<>
<span>{t(Keys.panes.settings.clearHistoryDescription)}</span>
<span>
This action will clear the all customizations for this account in this browser, including:
</span>
<ul className={styles.bulletList}>
<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>
<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>
</ul>
</>,
);
}}
>
{t(Keys.panes.settings.clearHistory)}
Clear History
</DefaultButton>
</div>
</div>
<div className="settingsSection">
<div className={`settingsSectionPart ${styles.settingsSectionContainer}`}>
<div className="settingsSectionLabel">{t(Keys.panes.settings.explorerVersion)}</div>
<div className="settingsSectionLabel">Explorer Version</div>
<div>{explorerVersion}</div>
</div>
</div>
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionLabel">{t(Keys.panes.settings.sessionId)}</div>
<div className="settingsSectionLabel">Session ID</div>
<div>{sessionId}</div>
</div>
</div>

View File

@@ -660,7 +660,7 @@ exports[`Settings Pane should render Default properly 1`] = `
Send more than one request while executing a query. More than one request is necessary if the query is not scoped to single partition key value.
</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="Enable query control"
ariaLabel="EnableQueryControl"
checked={false}
className="padding"
onChange={[Function]}
@@ -1190,8 +1190,7 @@ 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>

View File

@@ -11,8 +11,6 @@ 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";
@@ -115,13 +113,13 @@ export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> =
<CosmosFluentProvider>
<div className="panelFormWrapper">
<div className="panelMainContent" style={{ display: "flex", flexDirection: "column" }}>
<Text>{t(Keys.panes.tableColumnSelection.selectColumns)}</Text>
<Text>Select which columns to display in your view of items in your container.</Text>
<div /* Wrap <SearchBox> to avoid margin-bottom set by panelMainContent css */>
<SearchBox
className={styles.searchBox}
value={columnSearchText}
onChange={onSearchChange}
placeholder={t(Keys.panes.tableColumnSelection.searchFields)}
placeholder="Search fields"
/>
</div>
@@ -132,9 +130,7 @@ export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> =
key={columnDefinition.id}
label={{
className: styles.checkboxLabel,
children: `${columnDefinition.label}${
columnDefinition.isPartitionKey ? t(Keys.panes.tableColumnSelection.partitionKeySuffix) : ""
}`,
children: `${columnDefinition.label}${columnDefinition.isPartitionKey ? " (partition key)" : ""}`,
}}
checked={selectedColumnIdsSet.has(columnDefinition.id)}
onChange={(_, data) => onCheckedValueChange(columnDefinition.id, data)}
@@ -142,15 +138,15 @@ export const TableColumnSelectionPane: React.FC<TableColumnSelectionPaneProps> =
))}
</div>
<Button appearance="secondary" size="small" onClick={() => setNewSelectedColumnIds(defaultSelection)}>
{t(Keys.panes.tableColumnSelection.reset)}
Reset
</Button>
</div>
<div className="panelFooter" style={{ display: "flex", gap: theme.spacingHorizontalS }}>
<Button appearance="primary" onClick={onSave}>
{t(Keys.common.save)}
Save
</Button>
<Button appearance="secondary" onClick={closeSidePanel}>
{t(Keys.common.cancel)}
Cancel
</Button>
</div>
</div>

View File

@@ -1,7 +1,5 @@
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";
@@ -102,8 +100,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(t(Keys.panes.tables.propertyEmptyError, { property }));
setFormError(t(Keys.panes.tables.propertyEmptyError, { property }));
logConsoleError(`${property} cannot be empty. Please input a value for ${property}`);
setFormError(`${property} cannot be empty. Please input a value for ${property}`);
return;
}
@@ -111,13 +109,13 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
(property === "PartitionKey" && containsAnyWhiteSpace(value) === true) ||
(property === "RowKey" && containsAnyWhiteSpace(value) === true)
) {
logConsoleError(t(Keys.panes.tables.whitespaceError, { property }));
setFormError(t(Keys.panes.tables.whitespaceError, { property }));
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`);
return;
}
if (!type) {
setFormError(t(Keys.panes.tables.propertyTypeEmptyError, { property }));
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
return;
}

View File

@@ -1,7 +1,5 @@
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";
@@ -200,7 +198,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
}
if (!type) {
setFormError(t(Keys.panes.tables.propertyTypeEmptyError, { property }));
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
return;
}
@@ -210,8 +208,8 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
(property === "RowKey" && value === "") ||
(property === "RowKey" && value === undefined)
) {
logConsoleError(t(Keys.panes.tables.propertyEmptyError, { property }));
setFormError(t(Keys.panes.tables.propertyEmptyError, { property }));
logConsoleError(`${property} cannot be empty. Please input a value for ${property}`);
setFormError(`${property} cannot be empty. Please input a value for ${property}`);
return;
}
}
@@ -405,7 +403,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
)}
</div>
<div className="panelNullWarning" style={{ padding: "20px", color: "red" }}>
{t(Keys.panes.tables.nullFieldsWarning)}
Warning: Null fields will not be displayed for editing.
</div>
</RightPaneForm>
);

View File

@@ -1,6 +1,4 @@
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";
@@ -37,7 +35,7 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
const props: RightPaneFormProps = {
formError: "",
isExecuting: false,
submitButtonText: t(Keys.common.ok),
submitButtonText: "OK",
onSubmit,
};
@@ -123,11 +121,11 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
<RightPaneForm {...props}>
<div className="panelFormWrapper">
<div className="panelMainContent">
<Text>{t(Keys.panes.tableQuerySelect.selectColumns)}</Text>
<Text>Select the columns that you want to query.</Text>
<div className="column-select-view">
<Checkbox
id="availableCheckbox"
label={t(Keys.panes.tableQuerySelect.availableColumns)}
label="Available Columns"
checked={isAvailableColumnChecked}
onChange={availableColumnsCheckboxClick}
/>

View File

@@ -13,8 +13,6 @@ 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";
@@ -65,8 +63,8 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
const onSubmit = () => {
setFormError("");
if (!files || files.length === 0) {
setFormError(t(Keys.panes.uploadItems.noFilesSpecifiedError));
logConsoleError(t(Keys.panes.uploadItems.noFilesSpecifiedError));
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.");
return;
}
@@ -152,7 +150,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
},
{
key: "fileName",
name: t(Keys.panes.uploadItems.fileNameColumn),
name: "FILE NAME",
fieldName: "fileName",
minWidth: 120,
maxWidth: 140,
@@ -171,7 +169,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
},
{
key: "status",
name: t(Keys.panes.uploadItems.statusColumn),
name: "STATUS",
fieldName: "numSucceeded",
minWidth: 120,
maxWidth: 140,
@@ -180,11 +178,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
data: "string",
isPadded: true,
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
const fieldContent = t(Keys.panes.uploadItems.uploadStatus, {
numSucceeded: item.numSucceeded,
numThrottled: item.numThrottled,
numFailed: item.numFailed,
});
const fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
return (
<TooltipHost
content={fieldContent}
@@ -203,12 +197,12 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
<div className="paneMainContent">
<Upload
key={reducer} // Force re-render on state change
label={t(Keys.panes.uploadItems.selectJsonFiles)}
label="Select JSON Files"
onUpload={updateSelectedFiles}
accept="application/json"
multiple
tabIndex={0}
tooltip={t(Keys.panes.uploadItems.selectJsonFilesTooltip)}
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."
/>
{uploadFileData?.length > 0 && (
<div className="fileUploadSummaryContainer" data-test="file-upload-status">

View File

@@ -714,7 +714,9 @@ 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

View File

@@ -6,8 +6,6 @@ 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";
@@ -161,8 +159,8 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
const getSplashScreenButtons = (): JSX.Element => {
const buttons: FabricHomeScreenButtonProps[] = [
{
title: t(Keys.splashScreen.fabric.newContainer.title),
description: t(Keys.splashScreen.fabric.newContainer.description),
title: "New container",
description: "Create a destination container to store your data",
icon: <DocumentAddRegular />,
onClick: () => {
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
@@ -170,8 +168,8 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
},
},
{
title: t(Keys.splashScreen.fabric.sampleData.title),
description: t(Keys.splashScreen.fabric.sampleData.description),
title: "Sample Data",
description: "Load sample data in your database",
icon: <img src={CosmosDbBlackIcon} alt={"Azure Cosmos DB icon"} aria-hidden="true" />,
onClick: () => {
setSelectedSampleDataConfiguration({
@@ -183,8 +181,8 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
},
},
{
title: t(Keys.splashScreen.fabric.sampleVectorData.title),
description: t(Keys.splashScreen.fabric.sampleVectorData.description),
title: "Sample Vector Data",
description: "Load sample vector data with text-embedding-ada-002",
icon: <img src={AzureOpenAiIcon} alt={"Azure Open AI icon"} aria-hidden="true" />,
onClick: () => {
setSelectedSampleDataConfiguration({
@@ -196,14 +194,14 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
},
},
{
title: t(Keys.splashScreen.fabric.appDevelopment.title),
description: t(Keys.splashScreen.fabric.appDevelopment.description),
title: "App development",
description: "Start here to use an SDK to build your apps",
icon: <LinkMultipleRegular />,
onClick: () => window.open("https://aka.ms/cosmosdbfabricsdk", "_blank"),
},
{
title: t(Keys.splashScreen.fabric.sampleGallery.title),
description: t(Keys.splashScreen.fabric.sampleGallery.description),
title: "Sample Gallery",
description: "Get real-world end-to-end samples",
icon: <img src={GithubIcon} alt={"GitHub icon"} aria-hidden="true" />,
onClick: () => window.open("https://aka.ms/CosmosFabricSamplesGallery", "_blank"),
},
@@ -224,9 +222,7 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
);
};
const title = isFabricNativeReadOnly()
? t(Keys.splashScreen.fabric.useTitle)
: t(Keys.splashScreen.fabric.buildTitle);
const title = isFabricNativeReadOnly() ? "Use your database" : "Build your database";
return (
<>
<CosmosFluentProvider className={styles.homeContainer}>
@@ -242,9 +238,9 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
{getSplashScreenButtons()}
{
<div className={styles.footer}>
{t(Keys.splashScreen.sections.needHelp)}{" "}
Need help?{" "}
<Link href="https://learn.microsoft.com/fabric/database/cosmos-db/overview" target="_blank">
{t(Keys.common.learnMore)} <OpenRegular />
Learn more <OpenRegular />
</Link>
</div>
}

View File

@@ -12,8 +12,6 @@ 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";
@@ -61,7 +59,7 @@ export const SampleDataImportDialog: React.FC<{
setStatus("creating");
const databaseName = props.sampleDataConfiguration.databaseName;
if (checkContainerExists(databaseName, containerName)) {
const msg = t(Keys.splashScreen.sampleDataDialog.errorContainerExists, { containerName, databaseName });
const msg = `The container "${containerName}" in database "${databaseName}" already exists. Please delete it and retry.`;
setStatus("error");
setErrorMessage(msg);
return;
@@ -77,11 +75,7 @@ export const SampleDataImportDialog: React.FC<{
);
} catch (error) {
setStatus("error");
setErrorMessage(
t(Keys.splashScreen.sampleDataDialog.errorCreateContainer, {
error: error instanceof Error ? error.message : String(error),
}),
);
setErrorMessage(`Failed to create container: ${error instanceof Error ? error.message : String(error)}`);
return;
}
@@ -92,11 +86,7 @@ export const SampleDataImportDialog: React.FC<{
setStatus("completed");
} catch (error) {
setStatus("error");
setErrorMessage(
t(Keys.splashScreen.sampleDataDialog.errorImportData, {
error: error instanceof Error ? error.message : String(error),
}),
);
setErrorMessage(`Failed to import data: ${error instanceof Error ? error.message : String(error)}`);
}
};
@@ -122,26 +112,14 @@ export const SampleDataImportDialog: React.FC<{
const renderContent = () => {
switch (status) {
case "idle":
return t(Keys.splashScreen.sampleDataDialog.createPrompt, { containerName });
return `Create a container "${containerName}" and import sample data into it. This may take a few minutes.`;
case "creating":
return (
<Spinner
size="small"
labelPosition="above"
label={t(Keys.splashScreen.sampleDataDialog.creatingContainer, { containerName })}
/>
);
return <Spinner size="small" labelPosition="above" label={`Creating container "${containerName}"...`} />;
case "importing":
return (
<Spinner
size="small"
labelPosition="above"
label={t(Keys.splashScreen.sampleDataDialog.importingData, { containerName })}
/>
);
return <Spinner size="small" labelPosition="above" label={`Importing data into "${containerName}"...`} />;
case "completed":
return t(Keys.splashScreen.sampleDataDialog.success, { containerName });
return `Successfully created "${containerName}" with sample data.`;
case "error":
return (
<div style={{ color: "red" }}>
@@ -154,14 +132,14 @@ export const SampleDataImportDialog: React.FC<{
const getButtonLabel = () => {
switch (status) {
case "idle":
return t(Keys.splashScreen.sampleDataDialog.startButton);
return "Start";
case "creating":
case "importing":
return t(Keys.common.close);
return "Close";
case "completed":
return t(Keys.common.close);
return "Close";
case "error":
return t(Keys.common.close);
return "Close";
}
};
@@ -169,7 +147,7 @@ export const SampleDataImportDialog: React.FC<{
<Dialog open={props.open} onOpenChange={(event, data) => props.setOpen(data.open)}>
<DialogSurface>
<DialogBody>
<DialogTitle>{t(Keys.splashScreen.sampleDataDialog.title)}</DialogTitle>
<DialogTitle>Sample Data</DialogTitle>
<DialogContent>
<div className={styles.dialogContent}>{renderContent()}</div>
</DialogContent>

View File

@@ -54,6 +54,6 @@
.mainButtonsContainer {
display: flex;
gap: 0 16px;
margin: 40px auto
margin-bottom: 10px
}

View File

@@ -16,8 +16,6 @@ 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";
@@ -166,23 +164,6 @@ 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(
{
@@ -251,8 +232,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
<Stack className="splashStackRow" horizontal>
<SplashScreenButton
imgSrc={QuickStartIcon}
title={t(Keys.splashScreen.quickStart.title)}
description={t(Keys.splashScreen.quickStart.description)}
title={"Launch quick start"}
description={"Launch a quick start tutorial to get started with sample data"}
onClick={() => {
container.onNewCollectionClicked({ isQuickstart: true });
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
@@ -260,8 +241,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
/>
<SplashScreenButton
imgSrc={ContainersIcon}
title={t(Keys.splashScreen.newCollection.title, { collectionName: getCollectionName() })}
description={t(Keys.splashScreen.newCollection.description)}
title={`New ${getCollectionName()}`}
description={"Create a new container for storage and throughput"}
onClick={() => {
container.onNewCollectionClicked();
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
@@ -272,8 +253,10 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
<SplashScreenButton
imgSrc={CosmosDBIcon}
imgSize={35}
title={t(Keys.splashScreen.samplesGallery.title)}
description={t(Keys.splashScreen.samplesGallery.description)}
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"
}
onClick={() => {
window.open("https://azurecosmosdb.github.io/gallery/?tags=example", "_blank");
traceOpen(Action.LearningResourcesClicked, { apiType: userContext.apiType });
@@ -281,8 +264,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
/>
<SplashScreenButton
imgSrc={ConnectIcon}
title={t(Keys.splashScreen.connectCard.title)}
description={t(Keys.splashScreen.connectCard.description)}
title={"Connect"}
description={"Prefer using your own choice of tooling? Find the connection string you need to connect"}
onClick={() => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect)}
/>
</Stack>
@@ -297,7 +280,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
usePostgres.getState().showPostgreTeachingBubble &&
!usePostgres.getState().showResetPasswordBubble && (
<TeachingBubble
headline={t(Keys.splashScreen.teachingBubble.newToPostgres.headline)}
headline="New to Cosmos DB PGSQL?"
target={"#mainButton-quickstartDescription"}
hasCloseButton
onDismiss={() => usePostgres.getState().setShowPostgreTeachingBubble(false)}
@@ -309,14 +292,15 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
preventDismissOnScroll: true,
}}
primaryButtonProps={{
text: t(Keys.common.getStarted),
text: "Get started",
onClick: () => {
useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart);
usePostgres.getState().setShowPostgreTeachingBubble(false);
},
}}
>
{t(Keys.splashScreen.teachingBubble.newToPostgres.body)}
Welcome! If you are new to Cosmos DB PGSQL and need help with getting started, here is where you can find
sample data, query.
</TeachingBubble>
)}
{/*TODO: convert below to use SplashScreenButton */}
@@ -348,7 +332,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
))}
{userContext.apiType === "Postgres" && usePostgres.getState().showResetPasswordBubble && (
<TeachingBubble
headline={t(Keys.splashScreen.teachingBubble.resetPassword.headline)}
headline="Create your password"
target={"#mainButton-quickstartDescription"}
hasCloseButton
onDismiss={() => {
@@ -363,7 +347,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
preventDismissOnScroll: true,
}}
primaryButtonProps={{
text: t(Keys.common.create),
text: "Create",
onClick: () => {
localStorage.setItem(userContext.databaseAccount.id, "true");
sendMessage({
@@ -373,7 +357,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
},
}}
>
{t(Keys.splashScreen.teachingBubble.resetPassword.body)}
If you haven&apos;t changed your password yet, change it now.
</TeachingBubble>
)}
</div>
@@ -392,8 +376,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
const launchQuickstartBtn = {
id: "quickstartDescription",
iconSrc: QuickStartIcon,
title: t(Keys.splashScreen.quickStart.title),
description: t(Keys.splashScreen.quickStart.description),
title: "Launch quick start",
description: "Launch a quick start tutorial to get started with sample data",
onClick: () => {
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart);
@@ -415,8 +399,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
if (userContext.apiType === "Postgres") {
return {
iconSrc: PowerShellIcon,
title: t(Keys.splashScreen.shell.postgres.title),
description: t(Keys.splashScreen.shell.postgres.description),
title: "PostgreSQL Shell",
description: "Create table and interact with data using PostgreSQL's shell interface",
onClick: () => container.openNotebookTerminal(TerminalKind.Postgres),
};
}
@@ -424,16 +408,16 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
if (userContext.apiType === "VCoreMongo") {
return {
iconSrc: PowerShellIcon,
title: t(Keys.splashScreen.shell.vcoreMongo.title),
description: t(Keys.splashScreen.shell.vcoreMongo.description),
title: "Mongo Shell",
description: "Create a collection and interact with data using MongoDB's shell interface",
onClick: () => container.openNotebookTerminal(TerminalKind.VCoreMongo),
};
}
return {
iconSrc: ContainersIcon,
title: t(Keys.splashScreen.newCollection.title, { collectionName: getCollectionName() }),
description: t(Keys.splashScreen.newCollection.description),
title: `New ${getCollectionName()}`,
description: "Create a new container for storage and throughput",
onClick: () => {
container.onNewCollectionClicked();
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
@@ -443,19 +427,19 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
const getThirdCard = (): SplashScreenItem => {
let icon = ConnectIcon;
let title = t(Keys.splashScreen.connectCard.title);
let description = t(Keys.splashScreen.connectCard.description);
let title = "Connect";
let description = "Prefer using your own choice of tooling? Find the connection string you need to connect";
let onClick = () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect);
if (userContext.apiType === "Postgres") {
title = t(Keys.splashScreen.connectCard.pgAdmin.title);
description = t(Keys.splashScreen.connectCard.pgAdmin.description);
title = "Connect with pgAdmin";
description = "Prefer pgAdmin? Find your connection strings here";
}
if (userContext.apiType === "VCoreMongo") {
icon = VisualStudioIcon;
title = t(Keys.splashScreen.connectCard.vsCode.title);
description = t(Keys.splashScreen.connectCard.vsCode.description);
title = "Connect with VS Code";
description = "Query and Manage your MongoDB and DocumentDB clusters in Visual Studio Code";
onClick = () => container?.openInVsCode && container.openInVsCode();
}
@@ -484,7 +468,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
info: activity.path,
iconSrc: NotebookIcon,
title: activity.name,
description: t(Keys.splashScreen.sections.notebook),
description: "Notebook",
onClick: () => {
const notebookItem = container.createNotebookContentItemFile(activity.name, activity.path);
notebookItem && container.openNotebook(notebookItem);
@@ -523,18 +507,18 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
items = [
{
link: "https://aka.ms/msl-modeling-partitioning-2",
title: t(Keys.splashScreen.top3Items.sql.advancedModeling.title),
description: t(Keys.splashScreen.top3Items.sql.advancedModeling.description),
title: "Advanced Modeling Patterns",
description: "Learn advanced strategies to optimize your database.",
},
{
link: "https://aka.ms/msl-modeling-partitioning-1",
title: t(Keys.splashScreen.top3Items.sql.partitioning.title),
description: t(Keys.splashScreen.top3Items.sql.partitioning.description),
title: "Partitioning Best Practices",
description: "Learn to apply data model and partitioning strategies.",
},
{
link: "https://aka.ms/msl-resource-planning",
title: t(Keys.splashScreen.top3Items.sql.resourcePlanning.title),
description: t(Keys.splashScreen.top3Items.sql.resourcePlanning.description),
title: "Plan Your Resource Requirements",
description: "Get to know the different configuration choices.",
},
];
break;
@@ -542,18 +526,18 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
items = [
{
link: "https://aka.ms/mongodbintro",
title: t(Keys.splashScreen.top3Items.mongo.whatIsMongo.title),
description: t(Keys.splashScreen.top3Items.mongo.whatIsMongo.description),
title: "What is the MongoDB API?",
description: "Understand Azure Cosmos DB for MongoDB and its features.",
},
{
link: "https://aka.ms/mongodbfeaturesupport",
title: t(Keys.splashScreen.top3Items.mongo.features.title),
description: t(Keys.splashScreen.top3Items.mongo.features.description),
title: "Features and Syntax",
description: "Discover the advantages and features",
},
{
link: "https://aka.ms/mongodbpremigration",
title: t(Keys.splashScreen.top3Items.mongo.migrate.title),
description: t(Keys.splashScreen.top3Items.mongo.migrate.description),
title: "Migrate Your Data",
description: "Pre-migration steps for moving data",
},
];
break;
@@ -561,18 +545,18 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
items = [
{
link: "https://aka.ms/cassandrajava",
title: t(Keys.splashScreen.top3Items.cassandra.buildJavaApp.title),
description: t(Keys.splashScreen.top3Items.cassandra.buildJavaApp.description),
title: "Build a Java App",
description: "Create a Java app using an SDK.",
},
{
link: "https://aka.ms/cassandrapartitioning",
title: t(Keys.splashScreen.top3Items.cassandra.partitioning.title),
description: t(Keys.splashScreen.top3Items.cassandra.partitioning.description),
title: "Partitioning Best Practices",
description: "Learn how partitioning works.",
},
{
link: "https://aka.ms/cassandraRu",
title: t(Keys.splashScreen.top3Items.cassandra.requestUnits.title),
description: t(Keys.splashScreen.top3Items.cassandra.requestUnits.description),
title: "Request Units (RUs)",
description: "Understand RU charges.",
},
];
break;
@@ -580,18 +564,18 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
items = [
{
link: "https://aka.ms/Graphdatamodeling",
title: t(Keys.splashScreen.top3Items.gremlin.dataModeling.title),
description: t(Keys.splashScreen.top3Items.gremlin.dataModeling.description),
title: "Data Modeling",
description: "Graph data modeling recommendations",
},
{
link: "https://aka.ms/graphpartitioning",
title: t(Keys.splashScreen.top3Items.gremlin.partitioning.title),
description: t(Keys.splashScreen.top3Items.gremlin.partitioning.description),
title: "Partitioning Best Practices",
description: "Learn how partitioning works",
},
{
link: "https://aka.ms/graphapiquery",
title: t(Keys.splashScreen.top3Items.gremlin.queryData.title),
description: t(Keys.splashScreen.top3Items.gremlin.queryData.description),
title: "Query Data",
description: "Querying data with Gremlin",
},
];
break;
@@ -599,18 +583,18 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
items = [
{
link: "https://aka.ms/tableintro",
title: t(Keys.splashScreen.top3Items.tables.whatIsTable.title),
description: t(Keys.splashScreen.top3Items.tables.whatIsTable.description),
title: "What is the Table API?",
description: "Understand Azure Cosmos DB for Table and its features",
},
{
link: "https://aka.ms/tableimport",
title: t(Keys.splashScreen.top3Items.tables.migrate.title),
description: t(Keys.splashScreen.top3Items.tables.migrate.description),
title: "Migrate your data",
description: "Learn how to migrate your data",
},
{
link: "https://aka.ms/tablefaq",
title: t(Keys.splashScreen.top3Items.tables.faq.title),
description: t(Keys.splashScreen.top3Items.tables.faq.description),
title: "Azure Cosmos DB for Table FAQs",
description: "Common questions about Azure Cosmos DB for Table",
},
];
break;
@@ -667,7 +651,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
</ul>
{recentItems.length > 0 && (
<Link onClick={() => clearMostRecent()} className={styles.listItemTitle}>
{t(Keys.splashScreen.sections.clearRecents)}
Clear Recents
</Link>
)}
</Stack>
@@ -682,15 +666,15 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
}
const cdbLiveTv: item = {
link: "https://developer.azurecosmosdb.com/tv",
title: t(Keys.splashScreen.learningResources.liveTv.title),
description: t(Keys.splashScreen.learningResources.liveTv.description),
title: "Learn the Fundamentals",
description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.",
};
const commonItems: item[] = [
{
link: "https://learn.microsoft.com/azure/cosmos-db/data-explorer-shortcuts",
title: t(Keys.splashScreen.learningResources.shortcuts.title),
description: t(Keys.splashScreen.learningResources.shortcuts.description),
title: "Data Explorer keyboard shortcuts",
description: "Learn keyboard shortcuts to navigate Data Explorer.",
},
];
@@ -701,14 +685,14 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
apiItems = [
{
link: "https://aka.ms/msl-sdk-connect",
title: t(Keys.splashScreen.learningResources.sql.sdk.title),
description: t(Keys.splashScreen.learningResources.sql.sdk.description),
title: "Get Started using an SDK",
description: "Learn about the Azure Cosmos DB SDK.",
},
cdbLiveTv,
{
link: "https://aka.ms/msl-move-data",
title: t(Keys.splashScreen.learningResources.sql.migrate.title),
description: t(Keys.splashScreen.learningResources.sql.migrate.description),
title: "Migrate Your Data",
description: "Migrate data using Azure services and open-source solutions.",
},
];
break;
@@ -716,13 +700,13 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
apiItems = [
{
link: "https://aka.ms/mongonodejs",
title: t(Keys.splashScreen.learningResources.mongo.nodejs.title),
description: t(Keys.splashScreen.learningResources.mongo.nodejs.description),
title: "Build an app with Node.js",
description: "Create a Node.js app.",
},
{
link: "https://aka.ms/mongopython",
title: t(Keys.splashScreen.learningResources.mongo.gettingStarted.title),
description: t(Keys.splashScreen.learningResources.mongo.gettingStarted.description),
title: "Getting Started Guide",
description: "Learn the basics to get started.",
},
cdbLiveTv,
];
@@ -731,14 +715,14 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
apiItems = [
{
link: "https://aka.ms/cassandracontainer",
title: t(Keys.splashScreen.learningResources.cassandra.createContainer.title),
description: t(Keys.splashScreen.learningResources.cassandra.createContainer.description),
title: "Create a Container",
description: "Get to know the create a container options.",
},
cdbLiveTv,
{
link: "https://aka.ms/Cassandrathroughput",
title: t(Keys.splashScreen.learningResources.cassandra.throughput.title),
description: t(Keys.splashScreen.learningResources.cassandra.throughput.description),
title: "Provision Throughput",
description: "Learn how to configure throughput.",
},
];
break;
@@ -746,13 +730,13 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
apiItems = [
{
link: "https://aka.ms/graphquickstart",
title: t(Keys.splashScreen.learningResources.gremlin.getStarted.title),
description: t(Keys.splashScreen.learningResources.gremlin.getStarted.description),
title: "Get Started ",
description: "Create, query, and traverse using the Gremlin console",
},
{
link: "https://aka.ms/graphimport",
title: t(Keys.splashScreen.learningResources.gremlin.importData.title),
description: t(Keys.splashScreen.learningResources.gremlin.importData.description),
title: "Import Graph Data",
description: "Learn Bulk ingestion data using BulkExecutor",
},
cdbLiveTv,
];
@@ -761,13 +745,13 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
apiItems = [
{
link: "https://aka.ms/tabledotnet",
title: t(Keys.splashScreen.learningResources.tables.dotnet.title),
description: t(Keys.splashScreen.learningResources.tables.dotnet.description),
title: "Build a .NET App",
description: "How to access Azure Cosmos DB for Table from a .NET app.",
},
{
link: "https://aka.ms/Tablejava",
title: t(Keys.splashScreen.learningResources.tables.java.title),
description: t(Keys.splashScreen.learningResources.tables.java.description),
title: "Build a Java App",
description: "Create a Azure Cosmos DB for Table app with Java SDK ",
},
cdbLiveTv,
];
@@ -806,17 +790,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: t(Keys.splashScreen.nextStepItems.postgres.dataModeling),
title: "Data Modeling",
description: "",
},
{
link: " https://go.microsoft.com/fwlink/?linkid=2206941 ",
title: t(Keys.splashScreen.nextStepItems.postgres.distributionColumn),
title: "How to choose a Distribution Column",
description: "",
},
{
link: "https://go.microsoft.com/fwlink/?linkid=2207425",
title: t(Keys.splashScreen.nextStepItems.postgres.buildApps),
title: "Build Apps with Python/Java/Django",
description: "",
},
];
@@ -824,17 +808,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: t(Keys.splashScreen.nextStepItems.vcoreMongo.migrateData),
title: "Migrate Data",
description: "",
},
{
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/vector-search-ai",
title: t(Keys.splashScreen.nextStepItems.vcoreMongo.vectorSearch),
title: "Build AI apps with Vector Search",
description: "",
},
{
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/tutorial-nodejs-web-app?tabs=github-codespaces",
title: t(Keys.splashScreen.nextStepItems.vcoreMongo.buildApps),
title: "Build Apps with Nodejs",
description: "",
},
];
@@ -862,17 +846,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: t(Keys.splashScreen.learnMoreItems.postgres.performanceTuning),
title: "Performance Tuning",
description: "",
},
{
link: "https://go.microsoft.com/fwlink/?linkid=2208037",
title: t(Keys.splashScreen.learnMoreItems.postgres.diagnosticQueries),
title: "Useful Diagnostic Queries",
description: "",
},
{
link: "https://go.microsoft.com/fwlink/?linkid=2205270",
title: t(Keys.splashScreen.learnMoreItems.postgres.sqlReference),
title: "Distributed SQL Reference",
description: "",
},
];
@@ -880,17 +864,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: t(Keys.splashScreen.learnMoreItems.vcoreMongo.vectorSearch),
title: "Vector Search",
description: "",
},
{
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/how-to-create-text-index",
title: t(Keys.splashScreen.learnMoreItems.vcoreMongo.textIndexing),
title: "Text Indexing",
description: "",
},
{
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/troubleshoot-common-issues",
title: t(Keys.splashScreen.learnMoreItems.vcoreMongo.troubleshoot),
title: "Troubleshoot common issues",
description: "",
},
];
@@ -918,11 +902,10 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
return (
<div className={styles.splashScreenContainer}>
<div className={styles.splashScreen}>
<h2 className={styles.title} role="heading" aria-label={title}>
{title}
<span className="activePatch"></span>
<h2 className={styles.title} role="heading" aria-label="Welcome to Azure Cosmos DB">
Welcome to Azure Cosmos DB<span className="activePatch"></span>
</h2>
<div className={styles.subtitle}>{subtitle}</div>
<div className={styles.subtitle}>Globally distributed, multi-model database service for any scale</div>
{getSplashScreenButtons()}
{useCarousel.getState().showCoachMark && (
<Coachmark
@@ -931,25 +914,24 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
persistentBeak
>
<TeachingBubbleContent
headline={t(Keys.splashScreen.teachingBubble.coachMark.headline, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
headline={`Start with sample ${getCollectionName().toLocaleLowerCase()}`}
hasCloseButton
closeButtonAriaLabel={t(Keys.common.close)}
closeButtonAriaLabel="Close"
primaryButtonProps={{
text: t(Keys.common.getStarted),
text: "Get started",
onClick: () => {
useCarousel.getState().setShowCoachMark(false);
container.onNewCollectionClicked({ isQuickstart: true });
},
}}
secondaryButtonProps={{
text: t(Keys.common.cancel),
text: "Cancel",
onClick: () => useCarousel.getState().setShowCoachMark(false),
}}
onDismiss={() => useCarousel.getState().setShowCoachMark(false)}
>
{t(Keys.splashScreen.teachingBubble.coachMark.body)}
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
</TeachingBubbleContent>
</Coachmark>
)}
@@ -963,7 +945,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
fontFamily: '"Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif',
}}
>
{t(Keys.splashScreen.sections.nextSteps)}
Next steps
</Text>
{getNextStepItems()}
</Stack>
@@ -975,7 +957,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
fontFamily: '"Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif',
}}
>
{t(Keys.splashScreen.sections.tipsAndLearnMore)}
Tips & learn more
</Text>
{getTipsAndLearnMoreItems()}
</Stack>
@@ -984,15 +966,15 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
) : (
<div className={styles.moreStuffContainer}>
<div className={styles.moreStuffColumn}>
<h2 className={styles.columnTitle}>{t(Keys.splashScreen.sections.recents)}</h2>
<h2 className={styles.columnTitle}>Recents</h2>
{getRecentItems()}
</div>
<div className={styles.moreStuffColumn}>
<h2 className={styles.columnTitle}>{t(Keys.splashScreen.sections.top3)}</h2>
<h2 className={styles.columnTitle}>Top 3 things you need to know</h2>
{top3Items()}
</div>
<div className={styles.moreStuffColumn}>
<h2 className={styles.columnTitle}>{t(Keys.splashScreen.sections.learningResources)}</h2>
<h2 className={styles.columnTitle}>Learning Resources</h2>
{getLearningResourceItems()}
</div>
</div>

View File

@@ -18,8 +18,6 @@ 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";
@@ -59,7 +57,7 @@ export default class ConflictsTab extends TabsBase {
private _documentsIterator: MinimalQueryIterator;
private _container: Explorer;
private _acceptButtonLabel: ko.Observable<string> = ko.observable(t(Keys.common.save));
private _acceptButtonLabel: ko.Observable<string> = ko.observable("Save");
constructor(options: ViewModels.ConflictsTabOptions) {
super(options);
@@ -215,9 +213,9 @@ export default class ConflictsTab extends TabsBase {
this.selectedConflictContent.subscribe((newContent: string) => this._onEditorContentChange(newContent));
this.conflictOperation.subscribe((newOperationType: string) => {
let operationLabel = t(Keys.common.save);
let operationLabel = "Save";
if (newOperationType === Constants.ConflictOperationType.Replace) {
operationLabel = t(Keys.common.update);
operationLabel = "Update";
}
this._acceptButtonLabel(operationLabel);
@@ -231,7 +229,7 @@ export default class ConflictsTab extends TabsBase {
this._documentsIterator = this.createIterator();
await this.loadNextPage();
} catch (error) {
useDialog.getState().showOkModalDialog(t(Keys.tabs.conflicts.refreshGridFailed), getErrorMessage(error));
useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error));
}
}
@@ -259,11 +257,11 @@ export default class ConflictsTab extends TabsBase {
useDialog
.getState()
.showOkCancelModalDialog(
t(Keys.tabs.conflicts.unsavedChanges),
t(Keys.tabs.conflicts.changesWillBeLost),
t(Keys.common.ok),
"Unsaved changes",
"Changes will be lost. Do you want to continue?",
"OK",
async () => await this.resolveConflict(),
t(Keys.common.cancel),
"Cancel",
undefined,
);
} else {
@@ -334,7 +332,7 @@ export default class ConflictsTab extends TabsBase {
} catch (error) {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog(t(Keys.tabs.conflicts.resolveConflictFailed), errorMessage);
useDialog.getState().showOkModalDialog("Resolve conflict failed", errorMessage);
TelemetryProcessor.traceFailure(
Action.ResolveConflict,
{
@@ -388,7 +386,7 @@ export default class ConflictsTab extends TabsBase {
} catch (error) {
this.isExecutionError(true);
const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog(t(Keys.tabs.conflicts.deleteConflictFailed), errorMessage);
useDialog.getState().showOkModalDialog("Delete conflict failed", errorMessage);
TelemetryProcessor.traceFailure(
Action.DeleteConflict,
{
@@ -619,7 +617,7 @@ export default class ConflictsTab extends TabsBase {
}
if (this.discardButton.visible()) {
const label = t(Keys.common.discard);
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
@@ -632,7 +630,7 @@ export default class ConflictsTab extends TabsBase {
}
if (this.deleteButton.visible()) {
const label = t(Keys.common.delete);
const label = "Delete";
buttons.push({
iconSrc: DeleteIcon,
iconAlt: label,

View File

@@ -41,8 +41,6 @@ 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";
@@ -351,7 +349,7 @@ export const getTabsButtons = ({
}
const buttons: CommandButtonComponentProps[] = [];
const label = !isPreferredApiMongoDB ? t(Keys.tabs.documents.newItem) : t(Keys.tabs.documents.newDocument);
const label = !isPreferredApiMongoDB ? "New Item" : "New Document";
if (getNewDocumentButtonState(editorState).visible) {
buttons.push({
iconSrc: NewDocumentIcon,
@@ -370,7 +368,7 @@ export const getTabsButtons = ({
}
if (getSaveNewDocumentButtonState(editorState).visible) {
const label = t(Keys.common.save);
const label = "Save";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
@@ -388,7 +386,7 @@ export const getTabsButtons = ({
}
if (getDiscardNewDocumentChangesButtonState(editorState).visible) {
const label = t(Keys.common.discard);
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
@@ -405,7 +403,7 @@ export const getTabsButtons = ({
}
if (getSaveExistingDocumentButtonState(editorState).visible) {
const label = t(Keys.common.update);
const label = "Update";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
@@ -423,7 +421,7 @@ export const getTabsButtons = ({
}
if (getDiscardExistingDocumentChangesButtonState(editorState).visible) {
const label = t(Keys.common.discard);
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
@@ -440,7 +438,7 @@ export const getTabsButtons = ({
}
if (selectedRows.size > 0) {
const label = t(Keys.common.delete);
const label = "Delete";
buttons.push({
iconSrc: DeleteDocumentIcon,
iconAlt: label,
@@ -455,7 +453,7 @@ export const getTabsButtons = ({
}
if (!isPreferredApiMongoDB) {
const label = t(Keys.tabs.documents.uploadItem);
const label = "Upload Item";
buttons.push({
id: UPLOAD_BUTTON_ID,
iconSrc: UploadIcon,
@@ -739,18 +737,17 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
} else if (result.statusCode >= 400) {
newFailed.push(result.documentId);
logConsoleError(
t(Keys.tabs.documents.deleteDocumentFailedLog, {
documentId: result.documentId.id(),
statusCode: result.statusCode,
}),
`Failed to delete document ${result.documentId.id()} with status code ${result.statusCode}`,
);
}
});
logConsoleInfo(t(Keys.tabs.documents.deleteSuccessLog, { count: newSuccessful.length }));
logConsoleInfo(`Successfully deleted ${newSuccessful.length} document(s)`);
if (newThrottled.length > 0) {
logConsoleError(t(Keys.tabs.documents.deleteThrottledLog, { count: newThrottled.length }));
logConsoleError(
`Failed to delete ${newThrottled.length} document(s) due to "Request too large" (429) error. Retrying...`,
);
}
// Update result of the bulk delete: method is called again, because the state variables changed
@@ -792,7 +789,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
);
let partitionKeyProperties = useMemo(() => {
return partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) =>
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, "").replace(/["]+/g, ""),
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""),
);
}, [partitionKeyPropertyHeaders]);
@@ -920,11 +917,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
useDialog
.getState()
.showOkCancelModalDialog(
t(Keys.tabs.documents.unsavedChanges),
t(Keys.tabs.documents.unsavedChangesMessage),
t(Keys.common.ok),
"Unsaved changes",
"Your unsaved changes will be lost. Do you want to continue?",
"OK",
onDiscard,
t(Keys.common.cancel),
"Cancel",
onCancelDiscard,
);
} else {
@@ -1014,7 +1011,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
(error) => {
onExecutionErrorChange(true);
const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.createDocumentFailed), errorMessage);
useDialog.getState().showOkModalDialog("Create document failed", errorMessage);
TelemetryProcessor.traceFailure(
Action.CreateDocument,
{
@@ -1100,7 +1097,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
selectedDocumentId.partitionKeyValue = originalPartitionKeyValue;
onExecutionErrorChange(true);
const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.updateDocumentFailed), errorMessage);
useDialog.getState().showOkModalDialog("Update document failed", errorMessage);
TelemetryProcessor.traceFailure(
Action.UpdateDocument,
{
@@ -1177,12 +1174,7 @@ 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(
t(Keys.tabs.documents.deleteDocumentDialogTitle),
t(Keys.tabs.documents.documentDeleted),
);
useDialog.getState().showOkModalDialog("Delete document", "Document successfully deleted.");
return [toDeleteDocumentIds[0]];
});
// ----------------------------------------------------------------------------------------------------
@@ -1259,20 +1251,17 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
useDialog
.getState()
.showOkModalDialog(
t(Keys.tabs.documents.deleteDocumentsDialogTitle),
t(Keys.tabs.documents.throttlingError),
"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.`,
{
linkText: t(Keys.common.learnMore),
linkText: "Learn More",
linkUrl: MONGO_THROTTLING_DOC_URL,
},
);
} else {
useDialog
.getState()
.showOkModalDialog(
t(Keys.tabs.documents.deleteDocumentsDialogTitle),
t(Keys.tabs.documents.deleteFailed, { error: error.message }),
);
.showOkModalDialog("Delete documents", `Deleting document(s) failed (${error.message})`);
}
},
)
@@ -1286,21 +1275,21 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
const isPlural = selectedRows.size > 1;
const documentName = !isPreferredApiMongoDB
? isPlural
? t(Keys.tabs.documents.selectedItems, { count: selectedRows.size })
: t(Keys.tabs.documents.selectedItem)
? `the selected ${selectedRows.size} items`
: "the selected item"
: isPlural
? t(Keys.tabs.documents.selectedDocuments, { count: selectedRows.size })
: t(Keys.tabs.documents.selectedDocument);
const msg = t(Keys.tabs.documents.confirmDelete, { documentName });
? `the selected ${selectedRows.size} documents`
: "the selected document";
const msg = `Are you sure you want to delete ${documentName}?`;
useDialog
.getState()
.showOkCancelModalDialog(
t(Keys.tabs.documents.confirmDeleteTitle),
"Confirm delete",
msg,
t(Keys.common.delete),
"Delete",
() => deleteDocuments(Array.from(selectedRows).map((index) => documentIds[index as number])),
t(Keys.common.cancel),
"Cancel",
undefined,
);
}, [deleteDocuments, documentIds, isPreferredApiMongoDB, selectedRows]);
@@ -1481,11 +1470,7 @@ 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, "")
.replace(/["]+/g, ""),
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""),
);
return newDocumentId(rawDocument, partitionKeyProperties, partitionKeyValue);
@@ -1834,8 +1819,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
const partitionKeyProperty = partitionKeyProperties?.[0];
if (partitionKeyProperty !== "_id" && !_hasShardKeySpecified(documentContent)) {
const message = t(Keys.tabs.documents.missingShardProperty, { partitionKeyProperty });
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.createDocumentFailed), message);
const message = `The document is lacking the shard property: ${partitionKeyProperty}`;
useDialog.getState().showOkModalDialog("Create document failed", message);
onExecutionErrorChange(true);
TelemetryProcessor.traceFailure(
Action.CreateDocument,
@@ -1846,7 +1831,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
},
startKey,
);
Logger.logError(t(Keys.tabs.documents.missingShardKeyLog), "MongoDocumentsTab");
Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab");
throw new Error("Document without shard key");
}
@@ -1889,7 +1874,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
(error) => {
onExecutionErrorChange(true);
const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.createDocumentFailed), errorMessage);
useDialog.getState().showOkModalDialog("Create document failed", errorMessage);
TelemetryProcessor.traceFailure(
Action.CreateDocument,
{
@@ -1960,7 +1945,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
(error) => {
onExecutionErrorChange(true);
const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.updateDocumentFailed), errorMessage);
useDialog.getState().showOkModalDialog("Update document failed", errorMessage);
TelemetryProcessor.traceFailure(
Action.UpdateDocument,
{
@@ -2069,7 +2054,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
}
} catch (error) {
console.error(error);
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.refreshGridFailed), getErrorMessage(error));
useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error));
}
},
[createIterator, filterContent],
@@ -2081,17 +2066,18 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
* @returns 429 warning message
*/
const get429WarningMessageNoSql = (): string => {
let message = t(Keys.tabs.documents.requestTooLargeBase);
let message = 'Some delete requests failed due to a "Request too large" exception (429)';
if (bulkDeleteOperation.count === bulkDeleteProcess.successfulIds.length) {
message += ", " + t(Keys.tabs.documents.retriedSuccessfully);
message += ", but were successfully retried.";
} else if (bulkDeleteMode === "inProgress" || bulkDeleteMode === "aborting") {
message += ". " + t(Keys.tabs.documents.retryingNow);
message += ". Retrying now.";
} else {
message += ".";
}
return (message += " " + t(Keys.tabs.documents.increaseThroughputTip));
return (message +=
" To prevent this in the future, consider increasing the throughput on your container or database.");
};
const onColumnSelectionChange = (newSelectedColumnIds: string[]): void => {
@@ -2138,7 +2124,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
const nonBlankLastFilters = lastFilterContents.filter((filter) => filter.trim() !== "");
if (nonBlankLastFilters.length > 0) {
options.push({
label: t(Keys.tabs.documents.savedFilters),
label: "Saved filters",
options: nonBlankLastFilters,
});
}
@@ -2167,14 +2153,14 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
dropdownOptions={getFilterChoices()}
placeholder={
isPreferredApiMongoDB
? t(Keys.tabs.documents.mongoFilterPlaceholder)
: t(Keys.tabs.documents.sqlFilterPlaceholder)
? "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."
}
title={t(Keys.tabs.documents.filterTooltip)}
title="Type a query predicate or choose one from the list."
value={filterContent}
onChange={updateFilterContent}
onKeyDown={onFilterKeyDown}
bottomLink={{ text: t(Keys.common.learnMore), url: DATA_EXPLORER_DOC_URL }}
bottomLink={{ text: "Learn more", url: DATA_EXPLORER_DOC_URL }}
/>
<Button
appearance="primary"
@@ -2190,12 +2176,10 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
}
}}
disabled={isExecuting && isPreferredApiMongoDB}
aria-label={
!isExecuting || isPreferredApiMongoDB ? t(Keys.tabs.documents.applyFilter) : t(Keys.common.cancel)
}
aria-label={!isExecuting || isPreferredApiMongoDB ? "Apply filter" : "Cancel"}
tabIndex={0}
>
{!isExecuting || isPreferredApiMongoDB ? t(Keys.tabs.documents.applyFilter) : t(Keys.common.cancel)}
{!isExecuting || isPreferredApiMongoDB ? "Apply Filter" : "Cancel"}
</Button>
</div>
<Allotment
@@ -2239,14 +2223,14 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
</div>
{tableContainerSizePx?.width >= calculateOffset(selectedColumnIds.length) + 200 && (
<div
title={t(Keys.common.refresh)}
title="Refresh"
className={styles.refreshBtn}
role="button"
onClick={() => refreshDocumentsGrid(false)}
aria-label={t(Keys.common.refresh)}
aria-label="Refresh"
tabIndex={0}
>
<img src={RefreshIcon} alt={t(Keys.common.refresh)} />
<img src={RefreshIcon} alt="Refresh" />
</div>
)}
</div>
@@ -2259,7 +2243,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
onClick={() => loadNextPage(documentsIterator.iterator, false)}
onKeyDown={onLoadMoreKeyInput}
>
{t(Keys.tabs.documents.loadMore)}
Load more
</a>
)}
</div>
@@ -2271,7 +2255,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
language={"json"}
content={selectedDocumentContent}
isReadOnly={false}
ariaLabel={t(Keys.tabs.documents.documentEditor)}
ariaLabel={"Document editor"}
lineNumbers={"on"}
theme={"_theme"}
onContentChanged={_onEditorContentChange}
@@ -2279,9 +2263,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
/>
)}
{selectedRows.size > 1 && (
<span style={{ margin: 10 }}>
{t(Keys.tabs.documents.numberOfSelectedDocuments, { count: selectedRows.size })}
</span>
<span style={{ margin: 10 }}>Number of selected documents: {selectedRows.size}</span>
)}
</div>
</Allotment.Pane>
@@ -2290,43 +2272,42 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
{bulkDeleteOperation && (
<ProgressModalDialog
isOpen={isBulkDeleteDialogOpen}
dismissText={t(Keys.tabs.documents.abort)}
dismissText="Abort"
onDismiss={() => {
setIsBulkDeleteDialogOpen(false);
setBulkDeleteOperation(undefined);
}}
onCancel={() => setBulkDeleteMode("aborting")}
title={t(Keys.tabs.documents.deletingDocuments, { count: bulkDeleteOperation.count })}
message={t(Keys.tabs.documents.deletedDocumentsSuccess, { count: bulkDeleteProcess.successfulIds.length })}
title={`Deleting ${bulkDeleteOperation.count} document(s)`}
message={`Successfully deleted ${bulkDeleteProcess.successfulIds.length} document(s).`}
maxValue={bulkDeleteOperation.count}
value={bulkDeleteProcess.successfulIds.length}
mode={bulkDeleteMode}
>
<div className={styles.deleteProgressContent}>
{(bulkDeleteMode === "aborting" || bulkDeleteMode === "aborted") && (
<div style={{ paddingBottom: tokens.spacingVerticalL }}>{t(Keys.tabs.documents.deleteAborted)}</div>
<div style={{ paddingBottom: tokens.spacingVerticalL }}>Deleting document(s) was aborted.</div>
)}
{(bulkDeleteProcess.failedIds.length > 0 ||
(bulkDeleteProcess.throttledIds.length > 0 && bulkDeleteMode !== "inProgress")) && (
<MessageBar intent="error" style={{ marginBottom: tokens.spacingVerticalL }}>
<MessageBarBody>
<MessageBarTitle>{t(Keys.tabs.documents.error)}</MessageBarTitle>
{t(Keys.tabs.documents.failedToDeleteDocuments, {
count:
bulkDeleteMode === "inProgress"
? bulkDeleteProcess.failedIds.length
: bulkDeleteProcess.failedIds.length + bulkDeleteProcess.throttledIds.length,
})}
<MessageBarTitle>Error</MessageBarTitle>
Failed to delete{" "}
{bulkDeleteMode === "inProgress"
? bulkDeleteProcess.failedIds.length
: bulkDeleteProcess.failedIds.length + bulkDeleteProcess.throttledIds.length}{" "}
document(s).
</MessageBarBody>
</MessageBar>
)}
{bulkDeleteProcess.hasBeenThrottled && (
<MessageBar intent="warning">
<MessageBarBody>
<MessageBarTitle>{t(Keys.tabs.documents.warning)}</MessageBarTitle>
<MessageBarTitle>Warning</MessageBarTitle>
{get429WarningMessageNoSql()}{" "}
<Link href={NO_SQL_THROTTLING_DOC_URL} target="_blank">
{t(Keys.common.learnMore)}
Learn More
</Link>
</MessageBarBody>
</MessageBar>

View File

@@ -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]}

View File

@@ -1,8 +1,6 @@
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";
@@ -214,7 +212,7 @@ export default class MongoShellTabComponent extends Component<
src={this.state.url}
id={this.props.tabsBaseInstance.tabId}
onLoad={(event) => this.setContentFocus(event)}
title={t(Keys.tabs.mongoShell.title)}
title="Mongo Shell"
role="tabpanel"
></iframe>
);

View File

@@ -17,8 +17,6 @@ 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";
@@ -317,9 +315,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
};
public onSaveQueryClick = (): void => {
useSidePanel
.getState()
.openSidePanel(t(Keys.tabs.query.saveQuery), <SaveQueryPane explorer={this.props.collection.container} />);
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />);
};
public launchQueryCopilotChat = (): void => {
@@ -329,10 +325,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
public onSavedQueriesClick = (): void => {
useSidePanel
.getState()
.openSidePanel(
t(Keys.tabs.query.openSavedQueries),
<BrowseQueriesPane explorer={this.props.collection.container} />,
);
.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this.props.collection.container} />);
};
public toggleResult(): void {
@@ -480,8 +473,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (this.executeQueryButton.visible) {
const label =
this.state.selectedContent?.length > 0 ? t(Keys.tabs.query.executeSelection) : t(Keys.tabs.query.executeQuery);
const label = this.state.selectedContent?.length > 0 ? "Execute Selection" : "Execute Query";
buttons.push({
iconSrc: ExecuteQueryIcon,
iconAlt: label,
@@ -498,7 +490,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
if (this.saveQueryButton.visible) {
if (configContext.platform !== Platform.Fabric) {
const label = t(Keys.tabs.query.saveQuery);
const label = "Save Query";
buttons.push({
iconSrc: SaveQueryIcon,
iconAlt: label,
@@ -515,11 +507,11 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
buttons.push({
iconSrc: DownloadQueryIcon,
iconAlt: t(Keys.tabs.query.downloadQuery),
iconAlt: "Download Query",
keyboardAction: KeyboardAction.DOWNLOAD_ITEM,
onCommandClick: this.onDownloadQueryClick,
commandButtonLabel: t(Keys.tabs.query.downloadQuery),
ariaLabel: t(Keys.tabs.query.downloadQuery),
commandButtonLabel: "Download Query",
ariaLabel: "Download Query",
hasPopup: false,
disabled: !this.saveQueryButton.enabled,
});
@@ -576,7 +568,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
// }
if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) {
const label = t(Keys.tabs.query.cancelQuery);
const label = "Cancel query";
buttons.push({
iconSrc: CancelQueryIcon,
iconAlt: label,
@@ -597,23 +589,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: t(Keys.tabs.query.vertical),
ariaLabel: t(Keys.tabs.query.vertical),
commandButtonLabel: "Vertical",
ariaLabel: "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: t(Keys.tabs.query.horizontal),
ariaLabel: t(Keys.tabs.query.horizontal),
commandButtonLabel: "Horizontal",
ariaLabel: "Horizontal",
onCommandClick: () => this._setViewLayout(SplitterDirection.Horizontal),
hasPopup: false,
};
return {
commandButtonLabel: t(Keys.tabs.query.view),
ariaLabel: t(Keys.tabs.query.view),
commandButtonLabel: "View",
ariaLabel: "View",
hasPopup: true,
children: [verticalButton, horizontalButton],
};
@@ -790,7 +782,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
modelMarkers={this.state.modelMarkers}
isReadOnly={false}
wordWrap={"on"}
ariaLabel={t(Keys.tabs.query.editingQuery)}
ariaLabel={"Editing Query"}
lineNumbers={"on"}
theme={this.props.monacoTheme}
onContentChanged={(newContent: string) => this.onChangeContent(newContent)}

View File

@@ -11,8 +11,6 @@ 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";
@@ -135,7 +133,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(t(Keys.tabs.storedProcedure.body));
this.props.scriptTabBaseInstance.ariaLabel("Stored Procedure Body");
this.props.iStorProcTabComponentAccessor({
onExecuteSprocsResultEvent: this.onExecuteSprocsResult.bind(this),
@@ -320,7 +318,7 @@ export default class StoredProcedureTabComponent extends React.Component<
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const label = t(Keys.common.save);
const label = "Save";
if (this.state.saveButton.visible) {
buttons.push({
iconSrc: SaveIcon,
@@ -335,7 +333,7 @@ export default class StoredProcedureTabComponent extends React.Component<
}
if (this.state.updateButton.visible) {
const label = t(Keys.common.update);
const label = "Update";
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
@@ -349,7 +347,7 @@ export default class StoredProcedureTabComponent extends React.Component<
}
if (this.state.discardButton.visible) {
const label = t(Keys.common.discard);
const label = "Discard";
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
@@ -363,7 +361,7 @@ export default class StoredProcedureTabComponent extends React.Component<
}
if (this.state.executeButton.visible) {
const label = t(Keys.common.execute);
const label = "Execute";
buttons.push({
iconSrc: ExecuteQueryIcon,
iconAlt: label,
@@ -521,7 +519,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">
{t(Keys.tabs.storedProcedure.id)}
Stored Procedure Id
<span className="mandatoryStar" style={{ color: "#ff0707", fontSize: "14px", fontWeight: "bold" }}>
*&nbsp;
</span>
@@ -533,28 +531,28 @@ export default class StoredProcedureTabComponent extends React.Component<
required
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
aria-label={t(Keys.tabs.storedProcedure.idAriaLabel)}
placeholder={t(Keys.tabs.storedProcedure.idPlaceholder)}
aria-label="Stored procedure id"
placeholder="Enter the new stored procedure id"
size={40}
value={this.state.id}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => this.handleIdOnChange(event)}
/>
</span>
<div className="spUdfTriggerHeader">{t(Keys.tabs.storedProcedure.body)}</div>
<div className="spUdfTriggerHeader">Stored Procedure Body</div>
<EditorReact
language={"javascript"}
content={this.state.sProcEditorContent}
isReadOnly={false}
ariaLabel={t(Keys.tabs.storedProcedure.bodyAriaLabel)}
ariaLabel={"Stored procedure body"}
lineNumbers={"on"}
theme={"_theme"}
onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
/>
{this.state.hasResults && (
<div className="results-container">
<Pivot aria-label={t(Keys.tabs.storedProcedure.successfulExecution)} style={{ height: "100%" }}>
<Pivot aria-label="Successful execution of stored procedure" style={{ height: "100%" }}>
<PivotItem
headerText={t(Keys.common.result)}
headerText="Result"
headerButtonProps={{
"data-order": 1,
"data-title": "Result",
@@ -565,11 +563,11 @@ export default class StoredProcedureTabComponent extends React.Component<
language={"javascript"}
content={this.state.resultData}
isReadOnly={true}
ariaLabel={t(Keys.tabs.storedProcedure.resultAriaLabel)}
ariaLabel={"Execute stored procedure result"}
/>
</PivotItem>
<PivotItem
headerText={t(Keys.tabs.storedProcedure.consoleLogTab)}
headerText="console.log"
headerButtonProps={{
"data-order": 2,
"data-title": "console.log",
@@ -580,7 +578,7 @@ export default class StoredProcedureTabComponent extends React.Component<
language={"javascript"}
content={this.state.logsData}
isReadOnly={true}
ariaLabel={t(Keys.tabs.storedProcedure.logsAriaLabel)}
ariaLabel={"Execute stored procedure logs"}
/>
</PivotItem>
</Pivot>
@@ -588,16 +586,16 @@ export default class StoredProcedureTabComponent extends React.Component<
)}
{this.state.hasErrors && (
<div className="errors-container">
<div className="errors-header">{t(Keys.tabs.storedProcedure.errors)}</div>
<div className="errors-header">Errors:</div>
<div className="errorContent">
<span className="errorMessage">{this.state.error}</span>
<span className="errorDetailsLink">
<a
aria-label={t(Keys.tabs.storedProcedure.errorDetailsAriaLabel)}
aria-label="Error details link"
onClick={() => this.onErrorDetailsClick()}
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) => this.onErrorDetailsKeyPress(event)}
>
{t(Keys.tabs.storedProcedure.moreDetails)}
More details
</a>
</span>
</div>

View File

@@ -11,8 +11,6 @@ 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";
@@ -21,8 +19,8 @@ import { EditorReact } from "../Controls/Editor/EditorReact";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import TriggerTab from "./TriggerTab";
const triggerTypeOptions: IDropdownOption[] = [
{ key: "Pre", text: t(Keys.tabs.trigger.pre) },
{ key: "Post", text: t(Keys.tabs.trigger.post) },
{ key: "Pre", text: "Pre" },
{ key: "Post", text: "Post" },
];
const dropdownStyles: Partial<IDropdownStyles> = {
@@ -149,10 +147,10 @@ const dropdownStyles: Partial<IDropdownStyles> = {
};
const triggerOperationOptions: IDropdownOption[] = [
{ 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) },
{ key: "All", text: "All" },
{ key: "Create", text: "Create" },
{ key: "Delete", text: "Delete" },
{ key: "Replace", text: "Replace" },
];
interface Ibutton {
@@ -336,7 +334,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const label = t(Keys.common.save);
const label = "Save";
if (this.saveButton.visible) {
buttons.push({
setState: this.setState,
@@ -353,7 +351,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
}
if (this.updateButton.visible) {
const label = t(Keys.common.update);
const label = "Update";
buttons.push({
...this,
iconSrc: SaveIcon,
@@ -368,7 +366,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
}
if (this.discardButton.visible) {
const label = t(Keys.common.discard);
const label = "Discard";
buttons.push({
setState: this.setState,
...this,
@@ -417,14 +415,14 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
<div className="tab-pane flexContainer trigger-form" role="tabpanel">
<TextField
className="trigger-field"
label={t(Keys.tabs.trigger.id)}
label="Trigger Id"
id="entityTimeId"
autoFocus
required
type="text"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={t(Keys.tabs.trigger.idPlaceholder)}
placeholder="Enter the new trigger id"
size={40}
value={triggerId}
readOnly={!isIdEditable}
@@ -448,8 +446,8 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
}}
/>
<Dropdown
placeholder={t(Keys.tabs.trigger.type)}
label={t(Keys.tabs.trigger.type)}
placeholder="Trigger Type"
label="Trigger Type"
options={triggerTypeOptions}
selectedKey={triggerType}
className="trigger-field"
@@ -457,8 +455,8 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
styles={dropdownStyles}
/>
<Dropdown
placeholder={t(Keys.tabs.trigger.operation)}
label={t(Keys.tabs.trigger.operation)}
placeholder="Trigger Operation"
label="Trigger Operation"
selectedKey={triggerOperation}
options={triggerOperationOptions}
className="trigger-field"
@@ -467,12 +465,12 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
}
styles={dropdownStyles}
/>
<Label className="trigger-field">{t(Keys.tabs.trigger.body)}</Label>
<Label className="trigger-field">Trigger Body</Label>
<EditorReact
language={"json"}
content={triggerBody}
isReadOnly={false}
ariaLabel={t(Keys.tabs.trigger.bodyAriaLabel)}
ariaLabel={"Graph JSON"}
onContentChanged={this.handleTriggerBodyChange}
/>
</div>

View File

@@ -12,8 +12,6 @@ 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";
@@ -84,7 +82,7 @@ export default class UserDefinedFunctionTabContent extends Component<
protected getTabsButtons(): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const label = t(Keys.common.save);
const label = "Save";
if (this.saveButton.visible) {
buttons.push({
...this,
@@ -101,7 +99,7 @@ export default class UserDefinedFunctionTabContent extends Component<
}
if (this.updateButton.visible) {
const label = t(Keys.common.update);
const label = "Update";
buttons.push({
...this,
iconSrc: SaveIcon,
@@ -116,7 +114,7 @@ export default class UserDefinedFunctionTabContent extends Component<
}
if (this.discardButton.visible) {
const label = t(Keys.common.discard);
const label = "Discard";
buttons.push({
setState: this.setState,
...this,
@@ -267,7 +265,7 @@ export default class UserDefinedFunctionTabContent extends Component<
<FluentProvider theme={currentTheme}>
<TextField
className="trigger-field"
label={t(Keys.tabs.udf.id)}
label="User Defined Function Id"
id="entityTimeId"
autoFocus
required
@@ -275,7 +273,7 @@ export default class UserDefinedFunctionTabContent extends Component<
type="text"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={t(Keys.tabs.udf.idPlaceholder)}
placeholder="Enter the new user defined function id"
size={40}
value={udfId}
onChange={this.handleUdfIdChange}
@@ -301,12 +299,12 @@ export default class UserDefinedFunctionTabContent extends Component<
}}
/>{" "}
</FluentProvider>
<Label className="trigger-field">{t(Keys.tabs.udf.body)}</Label>
<Label className="trigger-field">User Defined Function Body</Label>
<EditorReact
language={"javascript"}
content={udfBody}
isReadOnly={false}
ariaLabel={t(Keys.tabs.udf.bodyAriaLabel)}
ariaLabel={"User defined function body"}
onContentChanged={this.handleUdfBodyChange}
/>
</div>

View File

@@ -141,6 +141,7 @@ export default class Collection implements ViewModels.Collection {
const defaultDataMaskingPolicy: DataModels.DataMaskingPolicy = {
includedPaths: Array<{ path: string; strategy: string; startPosition: number; length: number }>(),
excludedPaths: Array<string>(),
isPolicyEnabled: true,
};
const observablePolicy = ko.observable(data.dataMaskingPolicy || defaultDataMaskingPolicy);
observablePolicy.subscribe(() => {});

View File

@@ -1,4 +1,3 @@
import "./i18n";
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Arrow from "../images/Arrow.svg";

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