Compare commits

...

36 Commits

Author SHA1 Message Date
sunghyunkang1111
3385b62b60 Merge branch 'master' into loc_batch4 2026-03-06 16:04:36 -06:00
Sung-Hyun Kang
79a00080bd Update test snap 2026-03-06 15:18:20 -06:00
Sung-Hyun Kang
8bc875ae92 run prettier 2026-03-06 14:35:24 -06:00
Sung-Hyun Kang
47b8737ab1 Add localization strings batch 4 2026-03-06 13:37:25 -06:00
asier-isayas
a106b0ebac add the aad endpoint to known authorities when setting up MSAL (#2415)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2026-03-06 10:59:35 -08:00
jawelton74
dafb257fa3 Fix the keys blade links to point to the common connection strings (#2417)
blade.
2026-03-06 10:12:24 -08:00
sunghyunkang1111
b256ac1e1f Added localizations 3rd batch (#2413)
* Added localizations 3rd batch

* Fix unit tests
2026-03-06 11:19:00 -06:00
olprod
915f549df9 Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2444293 (#2412)
* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2444219

* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2444293

---------

Co-authored-by: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com>
2026-03-05 12:54:04 -06:00
sunghyunkang1111
77132be3b3 Localization second batch (#2410)
* Localization second batch

* update test

* fix tests

* Fix test
2026-03-05 12:05:45 -06:00
jawelton74
c343bad630 Remove chromium from the playwright test matrix. (#2409) 2026-03-04 06:05:27 -08:00
sunghyunkang1111
9579d1270b Localization first batch (#2408) 2026-03-03 16:17:18 -06:00
sunghyunkang1111
9496cf83ee Fixing metrics (#2395)
* Fixing metrics

* run prettier
2026-03-02 11:32:51 -06:00
jawelton74
dbe26654f1 Add new config for sov. clouds (#2402)
* Add new cloud origins for CSP.

* Remove the explorer endpoint, it is not needed here.

* Add other clouds as well.

* Fix formatting.
2026-03-02 07:52:00 -08:00
sakshigupta12feb
b478f2732c fixed the % update issue in copyjobs (#2399)
Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
2026-03-02 17:37:08 +05:30
sunghyunkang1111
204444b878 Added quote escaping for partition key extraction (#2403) 2026-02-27 14:58:14 -06:00
sunghyunkang1111
2e5c355479 Added localization build (#2380)
* Added localization build

* Commit types

* Added locProject.json

* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2425084 (#2392)

* Fixed package.json

* Fix compilation error

* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2431677 (#2396)

* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2432830 (#2397)

* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2431872

* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2432783

* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2432830

* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2434398 (#2400)

---------

Co-authored-by: olprod <olprod@microsoft.com>
2026-02-26 13:32:33 -06:00
Laurent Nguyen
5832170b2b Fix extractHeaderStatus to handle undefined and non-string messages (#2393)
* Fix extractHeaderStatus to handle undefined and non-string messages

* Fix unsafe casts

---------

Co-authored-by: Laurent Nguyen <languye@microsoft.com>
2026-02-20 16:42:17 +01:00
sunghyunkang1111
cc26e2800e Fix health metrics race condition and improve expected-failure handling (#2387)
* Fix health monitoring

* fix compile error
2026-02-17 12:54:04 -06:00
Laurent Nguyen
39ac7cf3f2 Add SC1010 to codes redacted in syntax error messages (#2388) 2026-02-16 18:34:54 +01:00
bogercraig
ed3a79f880 Adding origins for additional sov cloud. (#2389) 2026-02-13 16:58:07 -08:00
asier-isayas
68ba19d406 dependabot security alerts + delete containers after each migration e2e test (#2384)
* dependabot security alerts + delete containers after each migration e2e test

* catch error

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2026-02-12 11:24:30 -08:00
asier-isayas
81ad13508b dependabot batch 3 (#2379)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2026-02-11 12:52:05 -08:00
sunghyunkang1111
3f959e2235 Added expected errort handling (#2368) 2026-02-09 14:34:40 -06:00
sindhuba
67250f0f6b Fix timeout for flaky tests (#2378)
* Add changes for Load more option to work

* Remove localhost

* Add Mongo Pagination tests

* Run npm format

* Fix timeout in tests

* Revert "Fix timeout in tests"

This reverts commit 418b81c490.

* Fix timeout in tests

* Minor fix

* Revert "Minor fix"

This reverts commit 87fe289e3a.

* Revert "Fix timeout in tests"

This reverts commit 11c5748a31.

* Fix timeout
2026-02-05 16:14:37 -08:00
asier-isayas
df6312038a Dependabot Part 2 (#2370)
* dependabot part 1

* fix npm ci

* remove overrides

* undo playwright test update

* dependabot part 2

* cross-spawn

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2026-02-05 08:45:21 -08:00
sindhuba
e6c27f39be Fix pagination issue (#2298)
* Add changes for Load more option to work

* Remove localhost

* Add Mongo Pagination tests

* Run npm format

* Fix error in tests

* Cleanup CORSByPass

* Revert "Cleanup CORSByPass"

This reverts commit de11ece337.
2026-02-03 17:02:23 -08:00
asier-isayas
937989a47d Dependabot part 1 (#2362)
* dependabot part 1

* fix npm ci

* remove overrides

* undo playwright test update

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2026-02-03 13:21:10 -08:00
sakshigupta12feb
f166ef9c66 Add Playwright E2E tests for Dynamic Data Masking feature (#2348) (#2349)
* Add Playwright E2E tests for Dynamic Data Masking feature (#2348)

* Initial plan

* Add DDM Playwright test with comprehensive test cases

Co-authored-by: sakshigupta12feb <206070758+sakshigupta12feb@users.noreply.github.com>

* Remove disable DDM test - DDM cannot be disabled once enabled

Co-authored-by: sakshigupta12feb <206070758+sakshigupta12feb@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sakshigupta12feb <206070758+sakshigupta12feb@users.noreply.github.com>

* Fix format and lint errors in DDM Playwright tests (#2350)

* Initial plan

* Fix format and lint errors in dataMasking.spec.ts

Co-authored-by: sakshigupta12feb <206070758+sakshigupta12feb@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sakshigupta12feb <206070758+sakshigupta12feb@users.noreply.github.com>

* playwright testfor DDM

* fixed errors

* fixed playwright based on isPolicyEnabled(removal)

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sakshigupta12feb <206070758+sakshigupta12feb@users.noreply.github.com>
Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
2026-02-02 13:02:36 +05:30
sakshigupta12feb
b83f54e253 DDM login issue fixed while logging in via connection string (#2353)
* DDM login issue fixed while logging in via connection string

* Removed isPolicyEnabled. The includePaths field should be mandatory and, if present, must be an array

* updated the included value validation and excluded to be optional

---------

Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
2026-02-02 11:28:49 +05:30
sindhuba
a255dd502b Add sharedThroughput db tests (#2287)
* Add sharedThroughput db tests

* Run npm commands

* Run npm format

* Fix error with adding container to shared db test

* Add changes for Load more option to work

* Remove localhost

* Add Mongo Pagination tests

* Run npm format

* Revert "Add Mongo Pagination tests"

This reverts commit 50a244e6f9.

* Minor fixes

* Fix errors

* Minor fix

* Fix tests

* Address comments

* Address comments
2026-01-27 12:00:56 -08:00
asier-isayas
2998f14d52 Fix playwright tests (#2342)
* dont refresh tree when opening scale & settings

* disable offline/online migration tests

* delete db after each test

* DEBUG: expand console for mongo testing

* find first execute button for stored procedure

* DEBUG: wait for editor to process changes

* increase wait time to 5s

* verify document text was set

* keep document spec as original

* debug new document and save document count

* when loading a document, wait for document text to appear then click new document

* wait for document to be loaded

* remove debug statement

* wait for results to attach

* do forced wait instead

* cleanup tests

* uncomment container copy tests

* run test account cleanup every 12 hours

* change cleanup frequency to once a day

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2026-01-26 12:37:07 -08:00
BChoudhury-ms
05407b3e0f Add search/filter support to Copy Jobs list with pagination updates (#2343)
* search the copy job

* remove timeout

* Update src/Explorer/ContainerCopy/MonitorCopyJobs/Components/CopyJobsList.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix pagination race condition when filtering copy jobs (#2351)

* Initial plan

* Fix pagination race condition by resetting startIndex synchronously

Co-authored-by: BChoudhury-ms <201893606+BChoudhury-ms@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: BChoudhury-ms <201893606+BChoudhury-ms@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: BChoudhury-ms <201893606+BChoudhury-ms@users.noreply.github.com>
2026-01-23 20:58:46 +05:30
Laurent Nguyen
703218debf Fix error message in bulk delete to reflect total document count (#2331)
* Fix error message in bulk delete to reflect total document count

* Fix format
2026-01-23 07:38:04 +01:00
Laurent Nguyen
f83a2c4442 feat: redact sensitive information from BadRequest errors in telemetry logging (#2321)
Co-authored-by: Laurent Nguyen <languye@microsoft.com>
2026-01-23 07:37:45 +01:00
sakshigupta12feb
2ff01c6379 updated the feature value to hardcode true (#2346)
Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
2026-01-22 20:02:29 +05:30
sakshigupta12feb
31385950dd removed NotebookViewer file (#2281)
Co-authored-by: Sakshi Gupta <sakshig@microsoft.com>
2026-01-22 00:07:06 +05:30
161 changed files with 13749 additions and 12584 deletions

View File

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

1
.gitignore vendored
View File

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

14319
package-lock.json generated

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,8 @@ 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 { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
@@ -24,6 +26,7 @@ import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
import * as ViewModels from "../Contracts/ViewModels";
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { useSidePanel } from "../hooks/useSidePanel";
@@ -35,7 +38,6 @@ import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
import { useSelectedNode } from "./useSelectedNode";
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
export interface CollectionContextMenuButtonParams {
databaseId: string;
@@ -57,7 +59,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
{
iconSrc: AddCollectionIcon,
onClick: () => container.onNewCollectionClicked({ databaseId }),
label: `New ${getCollectionName()}`,
label: t(Keys.contextMenu.newContainer, { containerName: getCollectionName() }),
},
];
@@ -67,7 +69,7 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
items.push({
iconSrc: AddCollectionIcon,
onClick: () => openRestoreContainerDialog(),
label: `Restore ${getCollectionName()}`,
label: t(Keys.contextMenu.restoreContainer, { containerName: getCollectionName() }),
});
}
}
@@ -80,11 +82,11 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
useSidePanel
.getState()
.openSidePanel(
"Delete " + getDatabaseName(),
t(Keys.contextMenu.deleteDatabase, { databaseName: getDatabaseName() }),
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
);
},
label: `Delete ${getDatabaseName()}`,
label: t(Keys.contextMenu.deleteDatabase, { databaseName: getDatabaseName() }),
styleClass: "deleteDatabaseMenuItem",
});
}
@@ -100,7 +102,7 @@ export const createCollectionContextMenuButton = (
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, undefined),
label: "New SQL Query",
label: t(Keys.contextMenu.newSqlQuery),
});
}
@@ -108,7 +110,7 @@ export const createCollectionContextMenuButton = (
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, undefined),
label: "New Query",
label: t(Keys.contextMenu.newQuery),
});
items.push({
@@ -123,8 +125,8 @@ export const createCollectionContextMenuButton = (
},
label:
useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell
? "Open Mongo Shell"
: "New Shell",
? t(Keys.contextMenu.openMongoShell)
: t(Keys.contextMenu.newShell),
});
}
@@ -137,7 +139,7 @@ export const createCollectionContextMenuButton = (
onClick: () => {
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
},
label: "Open Cassandra Shell",
label: t(Keys.contextMenu.openCassandraShell),
});
}
@@ -150,7 +152,7 @@ export const createCollectionContextMenuButton = (
onClick: () => {
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
},
label: "New Stored Procedure",
label: t(Keys.contextMenu.newStoredProcedure),
});
items.push({
@@ -158,7 +160,7 @@ export const createCollectionContextMenuButton = (
onClick: () => {
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
},
label: "New UDF",
label: t(Keys.contextMenu.newUdf),
});
items.push({
@@ -166,7 +168,7 @@ export const createCollectionContextMenuButton = (
onClick: () => {
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
},
label: "New Trigger",
label: t(Keys.contextMenu.newTrigger),
});
}
@@ -179,11 +181,11 @@ export const createCollectionContextMenuButton = (
useSidePanel
.getState()
.openSidePanel(
"Delete " + getCollectionName(),
t(Keys.contextMenu.deleteContainer, { containerName: getCollectionName() }),
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
);
},
label: `Delete ${getCollectionName()}`,
label: t(Keys.contextMenu.deleteContainer, { containerName: getCollectionName() }),
styleClass: "deleteCollectionMenuItem",
});
}
@@ -220,14 +222,14 @@ export const createSampleCollectionContextMenuButton = (): TreeNodeMenuItem[] =>
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
},
label: "New SQL Query",
label: t(Keys.contextMenu.newSqlQuery),
});
} else if (copilotVersion === "v2.0") {
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => sampleCollection && sampleCollection.onNewQueryClick(sampleCollection, undefined),
label: "New SQL Query",
label: t(Keys.contextMenu.newSqlQuery),
});
}
}
@@ -247,7 +249,7 @@ export const createStoreProcedureContextMenuItems = (
{
iconSrc: DeleteSprocIcon,
onClick: () => storedProcedure.delete(),
label: "Delete Stored Procedure",
label: t(Keys.contextMenu.deleteStoredProcedure),
},
];
};
@@ -261,7 +263,7 @@ export const createTriggerContextMenuItems = (container: Explorer, trigger: Trig
{
iconSrc: DeleteTriggerIcon,
onClick: () => trigger.delete(),
label: "Delete Trigger",
label: t(Keys.contextMenu.deleteTrigger),
},
];
};
@@ -278,7 +280,7 @@ export const createUserDefinedFunctionContextMenuItems = (
{
iconSrc: DeleteUDFIcon,
onClick: () => userDefinedFunction.delete(),
label: "Delete User Defined Function",
label: t(Keys.contextMenu.deleteUdf),
},
];
};

View File

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

View File

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

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 { isCapabilityEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg";
@@ -44,6 +44,8 @@ import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
import "./SettingsComponent.less";
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
import { Keys } from "../../../Localization/Keys.generated";
import { t } from "../../../Localization/t";
import {
ConflictResolutionComponent,
ConflictResolutionComponentProps,
@@ -70,6 +72,7 @@ import {
getMongoNotification,
getTabTitle,
hasDatabaseSharedThroughput,
isDataMaskingEnabled,
isDirty,
parseConflictResolutionMode,
parseConflictResolutionProcedure,
@@ -686,22 +689,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.setState({ isComputedPropertiesDirty: isComputedPropertiesDirty });
private onDataMaskingContentChange = (newDataMasking: DataModels.DataMaskingPolicy): void => {
if (!newDataMasking.excludedPaths) {
newDataMasking.excludedPaths = [];
}
if (!newDataMasking.includedPaths) {
newDataMasking.includedPaths = [];
}
const validationErrors = [];
if (!Array.isArray(newDataMasking.includedPaths)) {
validationErrors.push("includedPaths must be an array");
if (newDataMasking.includedPaths === undefined || newDataMasking.includedPaths === null) {
validationErrors.push(t(Keys.controls.settings.dataMasking.includedPathsRequired));
} else if (!Array.isArray(newDataMasking.includedPaths)) {
validationErrors.push(t(Keys.controls.settings.dataMasking.includedPathsMustBeArray));
}
if (!Array.isArray(newDataMasking.excludedPaths)) {
validationErrors.push("excludedPaths must be an array");
}
if (typeof newDataMasking.isPolicyEnabled !== "boolean") {
validationErrors.push("isPolicyEnabled must be a boolean");
if (newDataMasking.excludedPaths !== undefined && !Array.isArray(newDataMasking.excludedPaths)) {
validationErrors.push(t(Keys.controls.settings.dataMasking.excludedPathsMustBeArray));
}
this.setState({
@@ -842,7 +837,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const dataMaskingContent: DataModels.DataMaskingPolicy = {
includedPaths: this.collection.dataMaskingPolicy?.()?.includedPaths || [],
excludedPaths: this.collection.dataMaskingPolicy?.()?.excludedPaths || [],
isPolicyEnabled: this.collection.dataMaskingPolicy?.()?.isPolicyEnabled ?? true,
};
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
@@ -904,7 +898,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const buttons: CommandButtonComponentProps[] = [];
const isExecuting = this.props.settingsTab.isExecuting();
if (this.saveSettingsButton.isVisible()) {
const label = "Save";
const label = t(Keys.common.save);
buttons.push({
iconSrc: SaveIcon,
iconAlt: label,
@@ -917,7 +911,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}
if (this.discardSettingsChangesButton.isVisible()) {
const label = "Discard";
const label = t(Keys.common.discard);
buttons.push({
iconSrc: DiscardIcon,
iconAlt: label,
@@ -942,9 +936,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
const throughputDelta = (newThroughput - this.offer.autoscaleMaxThroughput) * numberOfRegions;
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
this.totalThroughputUsed + throughputDelta
} RU/s. Change total throughput limit in cost management.`;
throughputError = t(Keys.controls.settings.throughput.throughputCapError, {
throughputCap: String(throughputCap),
newTotalThroughput: String(this.totalThroughputUsed + throughputDelta),
});
}
this.setState({ autoPilotThroughput: newThroughput, throughputError });
};
@@ -955,9 +950,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
const throughputDelta = (newThroughput - this.offer.manualThroughput) * numberOfRegions;
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
this.totalThroughputUsed + throughputDelta
} RU/s. Change total throughput limit in cost management.`;
throughputError = t(Keys.controls.settings.throughput.throughputCapError, {
throughputCap: String(throughputCap),
newTotalThroughput: String(this.totalThroughputUsed + throughputDelta),
});
}
this.setState({ throughput: newThroughput, throughputError });
};
@@ -1073,8 +1069,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
newCollection.fullTextPolicy = this.state.fullTextPolicy;
// Only send data masking policy if it was modified (dirty)
if (this.state.isDataMaskingDirty && isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking)) {
// Only send data masking policy if it was modified (dirty) and data masking is enabled
if (this.state.isDataMaskingDirty && isDataMaskingEnabled(this.collection.dataMaskingPolicy?.())) {
newCollection.dataMaskingPolicy = this.state.dataMaskingContent;
}
@@ -1463,15 +1459,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
});
}
// Check if DDM should be enabled
const shouldEnableDDM = (): boolean => {
const hasDataMaskingCapability = isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking);
const isSqlAccount = userContext.apiType === "SQL";
return isSqlAccount && hasDataMaskingCapability; // Only show for SQL accounts with DDM capability
};
if (shouldEnableDDM()) {
if (isDataMaskingEnabled(this.collection.dataMaskingPolicy?.())) {
const dataMaskingComponentProps: DataMaskingComponentProps = {
shouldDiscardDataMasking: this.state.shouldDiscardDataMasking,
resetShouldDiscardDataMasking: this.resetShouldDiscardDataMasking,
@@ -1576,7 +1564,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}
>
{this.shouldShowKeyspaceSharedThroughputMessage() && (
<div>This table shared throughput is configured at the keyspace</div>
<div>{t(Keys.controls.settings.scale.keyspaceSharedThroughput)}</div>
)}
<div

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -4,6 +4,8 @@ import * as Constants from "../../../../Common/Constants";
import { Platform, configContext } from "../../../../ConfigContext";
import * as DataModels from "../../../../Contracts/DataModels";
import * as ViewModels from "../../../../Contracts/ViewModels";
import { Keys } from "../../../../Localization/Keys.generated";
import { t } from "../../../../Localization/t";
import * as SharedConstants from "../../../../Shared/Constants";
import { userContext } from "../../../../UserContext";
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
@@ -156,14 +158,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
const freeTierLimits = SharedConstants.FreeTierLimits;
return (
<Text>
With free tier, you will get the first {freeTierLimits.RU} RU/s and {freeTierLimits.Storage} GB of storage in
this account for free. To keep your account free, keep the total RU/s across all resources in the account to{" "}
{freeTierLimits.RU} RU/s.
{t(Keys.controls.settings.scale.freeTierInfo, { ru: freeTierLimits.RU, storage: freeTierLimits.Storage })}
<Link
href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts"
target="_blank"
>
Learn more.
{t(Keys.controls.settings.scale.freeTierLearnMore)}
</Link>
</Text>
);
@@ -188,12 +188,9 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
{/* TODO: Replace link with call to the Azure Support blade */}
{this.isAutoScaleEnabled() && (
<Stack {...titleAndInputStackProps}>
<Text>Throughput (RU/s)</Text>
<Text>{t(Keys.controls.settings.scale.throughputRuS)}</Text>
<TextField disabled styles={getTextFieldStyles(undefined, undefined)} />
<Text>
Your account has custom settings that prevents setting throughput at the container level. Please work with
your Cosmos DB engineering team point of contact to make changes.
</Text>
<Text>{t(Keys.controls.settings.scale.autoScaleCustomSettings)}</Text>
</Stack>
)}
</Stack>

View File

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

View File

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

View File

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

View File

@@ -561,9 +561,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
}
}
>
You are not able to lower throughput below your current minimum of
10000
RU/s. For more information on this limit, please refer to our service quote documentation.
You are not able to lower throughput below your current minimum of 10000 RU/s. For more information on this limit, please refer to our service quote documentation.
<StyledLinkBase
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank"
@@ -581,9 +579,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
}
}
>
Based on usage, your
container
throughput will scale from
Based on usage, your container throughput will scale from
<b>
400
@@ -692,7 +688,8 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$
35.04
min
min
</Text>
<Text
style={
@@ -705,7 +702,8 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$
350.40
max
max
</Text>
</Stack>
</div>
@@ -739,7 +737,8 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$
35.04
min
min
</Text>
<Text
style={
@@ -752,7 +751,8 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
$
350.40
max
max
</Text>
</Stack>
</Stack>
@@ -1169,9 +1169,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
}
}
>
You are not able to lower throughput below your current minimum of
10000
RU/s. For more information on this limit, please refer to our service quote documentation.
You are not able to lower throughput below your current minimum of 10000 RU/s. For more information on this limit, please refer to our service quote documentation.
<StyledLinkBase
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank"
@@ -1755,9 +1753,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
}
}
>
You are not able to lower throughput below your current minimum of
10000
RU/s. For more information on this limit, please refer to our service quote documentation.
You are not able to lower throughput below your current minimum of 10000 RU/s. For more information on this limit, please refer to our service quote documentation.
<StyledLinkBase
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
target="_blank"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -604,6 +604,58 @@ exports[`SettingsComponent renders 1`] = `
/>
</Stack>
</PivotItem>
<PivotItem
headerButtonProps={
{
"data-test": "settings-tab-header/DataMaskingTab",
}
}
headerText="Masking Policy (preview)"
itemKey="DataMaskingTab"
key="DataMaskingTab"
style={
{
"backgroundColor": "var(--colorNeutralBackground1)",
"color": "var(--colorNeutralForeground1)",
"marginTop": 20,
}
}
>
<Stack
styles={
{
"root": {
"backgroundColor": "var(--colorNeutralBackground1)",
"color": "var(--colorNeutralForeground1)",
},
}
}
>
<DataMaskingComponent
dataMaskingContent={
{
"excludedPaths": [
"/excludedPath",
],
"includedPaths": [],
}
}
dataMaskingContentBaseline={
{
"excludedPaths": [
"/excludedPath",
],
"includedPaths": [],
}
}
onDataMaskingContentChange={[Function]}
onDataMaskingDirtyChange={[Function]}
resetShouldDiscardDataMasking={[Function]}
shouldDiscardDataMasking={false}
validationErrors={[]}
/>
</Stack>
</PivotItem>
<PivotItem
headerButtonProps={
{

View File

@@ -127,9 +127,13 @@ exports[`SettingsUtils functions render 1`] = `
>
The request to increase the throughput has successfully been submitted. This operation will take 1-3 business days to complete. View the latest status in Notifications.
<br />
Database:
Database:
sampleDb
, Container:
,
Container:
sampleCollection
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
@@ -309,7 +313,7 @@ exports[`SettingsUtils functions render 1`] = `
}
}
>
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
<StyledLinkBase
onClick={[Function]}
>

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);
NotificationConsoleUtils.logConsoleError(error instanceof Error ? error.message : String(error));
}
}),
);

View File

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

View File

@@ -42,6 +42,8 @@ import {
} from "Explorer/Panes/AddCollectionPanel/AddCollectionPanelUtility";
import { useSidePanel } from "hooks/useSidePanel";
import { useTeachingBubble } from "hooks/useTeachingBubble";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import { DEFAULT_FABRIC_NATIVE_CONTAINER_THROUGHPUT, isFabricNative } from "Platform/Fabric/FabricUtil";
import React from "react";
import { CollectionCreation } from "Shared/Constants";
@@ -177,7 +179,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
messageType="info"
showErrorDetails={false}
link={Constants.Urls.freeTierInformation}
linkText="Learn more"
linkText={t(Keys.common.learnMore)}
/>
)}
@@ -274,17 +276,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).toLocaleLowerCase()}.`}
content={t(Keys.panes.addCollection.databaseTooltip, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
true,
).toLocaleLowerCase()}.`}
ariaLabel={t(Keys.panes.addCollection.databaseTooltip, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
/>
</TooltipHost>
</Stack>
@@ -304,7 +306,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Create new</span>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.createNew)}</span>
<input
className="panelRadioBtn"
@@ -317,7 +319,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Use existing</span>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.useExisting)}</span>
</div>
</Stack>
)}
@@ -347,7 +349,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && (
<Stack horizontal>
<Checkbox
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
label={t(Keys.panes.addCollection.shareThroughput, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
checked={this.state.isSharedThroughputChecked}
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -365,17 +369,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`Throughput configured at the database level will be shared across all ${getCollectionName(
true,
).toLocaleLowerCase()} within the database.`}
content={t(Keys.panes.addCollection.shareThroughputTooltip, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
true,
).toLocaleLowerCase()} within the database.`}
ariaLabel={t(Keys.panes.addCollection.shareThroughputTooltip, {
collectionName: getCollectionName(true).toLocaleLowerCase(),
})}
/>
</TooltipHost>
</Stack>
@@ -424,14 +428,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
content={t(Keys.panes.addCollection.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
>
<Icon
role="button"
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
ariaLabel={t(Keys.panes.addCollection.collectionIdTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
/>
</TooltipHost>
</Stack>
@@ -445,10 +453,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
autoComplete="off"
pattern={ValidCosmosDbIdInputPattern.source}
title={ValidCosmosDbIdDescription}
placeholder={`e.g., ${getCollectionName()}1`}
placeholder={t(Keys.panes.addCollection.collectionIdPlaceholder, { collectionName: getCollectionName() })}
size={40}
className="panelTextField"
aria-label={`${getCollectionName()} id, Example ${getCollectionName()}1`}
aria-label={t(Keys.panes.addCollection.collectionIdAriaLabel, { collectionName: getCollectionName() })}
value={this.state.collectionId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ collectionId: event.target.value })
@@ -462,7 +470,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Indexing
{t(Keys.panes.addCollection.indexing)}
</Text>
</Stack>
@@ -470,32 +478,32 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input
className="panelRadioBtn"
checked={this.state.enableIndexing}
aria-label="Turn on indexing"
aria-label={t(Keys.panes.addCollection.turnOnIndexing)}
aria-checked={this.state.enableIndexing}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onTurnOnIndexing.bind(this)}
/>
<span className="panelRadioBtnLabel">Automatic</span>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.automatic)}</span>
<input
className="panelRadioBtn"
checked={!this.state.enableIndexing}
aria-label="Turn off indexing"
aria-label={t(Keys.panes.addCollection.turnOffIndexing)}
aria-checked={!this.state.enableIndexing}
type="radio"
role="radio"
tabIndex={0}
onChange={this.onTurnOffIndexing.bind(this)}
/>
<span className="panelRadioBtnLabel">Off</span>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.off)}</span>
</Stack>
<Text variant="small">
{this.getFreeTierIndexingText()}{" "}
<Link target="_blank" href="https://aka.ms/cosmos-indexing-policy">
Learn more
{t(Keys.common.learnMore)}
</Link>
</Text>
</Stack>
@@ -508,21 +516,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal style={{ marginTop: -5, marginBottom: -4 }}>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Sharding
{t(Keys.panes.addCollection.sharding)}
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
}
content={t(Keys.panes.addCollection.shardingTooltip)}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
}
ariaLabel={t(Keys.panes.addCollection.shardingTooltip)}
/>
</TooltipHost>
</Stack>
@@ -531,7 +535,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<input
className="panelRadioBtn"
checked={!this.state.isSharded}
aria-label="Unsharded"
aria-label={t(Keys.panes.addCollection.unsharded)}
aria-checked={!this.state.isSharded}
name="unsharded"
type="radio"
@@ -540,12 +544,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onUnshardedRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Unsharded (20GB limit)</span>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.unshardedLabel)}</span>
<input
className="panelRadioBtn"
checked={this.state.isSharded}
aria-label="Sharded"
aria-label={t(Keys.panes.addCollection.sharded)}
aria-checked={this.state.isSharded}
name="sharded"
type="radio"
@@ -554,7 +558,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onShardedRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Sharded</span>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.sharded)}</span>
</Stack>
</Stack>
)}
@@ -679,15 +683,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
disabled={this.state.subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
onClick={() => this.setState({ subPartitionKeys: [...this.state.subPartitionKeys, ""] })}
>
Add hierarchical partition key
{t(Keys.panes.addCollection.addPartitionKey)}
</DefaultButton>
{this.state.subPartitionKeys.length > 0 && (
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
partition your data with up to three levels of keys for better data distribution. Requires .NET
V3, Java V4 SDK, or preview JavaScript V3 SDK.{" "}
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} />{" "}
{t(Keys.panes.addCollection.hierarchicalPartitionKeyInfo)}{" "}
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
Learn more
{t(Keys.common.learnMore)}
</Link>
</Text>
)}
@@ -700,7 +703,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{!isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && (
<Stack horizontal verticalAlign="center">
<Checkbox
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
label={t(Keys.panes.addCollection.provisionDedicatedThroughput, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
checked={this.state.enableDedicatedThroughput}
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -718,23 +723,19 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
true,
).toLocaleLowerCase()} in the database and
does not count towards the throughput you provisioned for the database. This throughput amount will be
billed in addition to the throughput amount you provisioned at the database level.`}
content={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
})}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
true,
).toLocaleLowerCase()} in the database and
does not count towards the throughput you provisioned for the database. This throughput amount will be
billed in addition to the throughput amount you provisioned at the database level.`}
ariaLabel={t(Keys.panes.addCollection.provisionDedicatedThroughputTooltip, {
collectionName: getCollectionName().toLocaleLowerCase(),
collectionNamePlural: getCollectionName(true).toLocaleLowerCase(),
})}
/>
</TooltipHost>
</Stack>
@@ -769,8 +770,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
autoComplete="off"
placeholder={
userContext.apiType === "Mongo"
? "Comma separated paths e.g. firstName,address.zipCode"
: "Comma separated paths e.g. /firstName,/address/zipCode"
? t(Keys.panes.addCollection.uniqueKeysPlaceholderMongo)
: t(Keys.panes.addCollection.uniqueKeysPlaceholderSql)
}
className="panelTextField"
value={uniqueKey}
@@ -802,7 +803,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
styles={{ root: { padding: 0 }, label: { fontSize: 12, color: "var(--colorNeutralForeground1)" } }}
onClick={() => this.setState({ uniqueKeys: [...this.state.uniqueKeys, ""] })}
>
Add unique key
{t(Keys.panes.addCollection.addUniqueKey)}
</ActionButton>
</Stack>
)}
@@ -823,7 +824,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
className="panelRadioBtn"
checked={this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label="Enable analytical store"
aria-label={t(Keys.panes.addCollection.enableAnalyticalStore)}
aria-checked={this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
@@ -832,13 +833,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">On</span>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.on)}</span>
<input
className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore}
disabled={!isSynapseLinkEnabled()}
aria-label="Disable analytical store"
aria-label={t(Keys.panes.addCollection.disableAnalyticalStore)}
aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore"
type="radio"
@@ -847,26 +848,28 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
tabIndex={0}
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
/>
<span className="panelRadioBtnLabel">Off</span>
<span className="panelRadioBtnLabel">{t(Keys.panes.addCollection.off)}</span>
</div>
</Stack>
{!isSynapseLinkEnabled() && (
<Stack className="panelGroupSpacing">
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
Azure Synapse Link is required for creating an analytical store{" "}
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
{t(Keys.panes.addCollection.analyticalStoreSynapseLinkRequired, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}{" "}
<br />
<Link
href="https://aka.ms/cosmosdb-synapselink"
target="_blank"
aria-label={Constants.ariaLabelForLearnMoreLink.AzureSynapseLink}
className="capacitycalculator-link"
>
Learn more
{t(Keys.common.learnMore)}
</Link>
</Text>
<DefaultButton
text="Enable"
text={t(Keys.panes.addCollection.enable)}
onClick={() => this.props.explorer.openEnableSynapseLinkDialog()}
style={{ height: 27, width: 80 }}
styles={{ label: { fontSize: 12 } }}
@@ -878,7 +881,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowVectorSearchParameters() && (
<Stack>
<CollapsibleSectionComponent
title="Container Vector Policy"
title={t(Keys.panes.addCollection.containerVectorPolicy)}
isExpandedByDefault={false}
onExpand={() => {
scrollToSection("collapsibleVectorPolicySectionContent");
@@ -906,7 +909,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{this.shouldShowFullTextSearchParameters() && (
<Stack>
<CollapsibleSectionComponent
title="Container Full Text Search Policy"
title={t(Keys.panes.addCollection.containerFullTextSearchPolicy)}
isExpandedByDefault={false}
onExpand={() => {
scrollToSection("collapsibleFullTextPolicySectionContent");
@@ -935,7 +938,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
{!isFabricNative() && userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent
title="Advanced"
title={t(Keys.panes.addCollection.advanced)}
isExpandedByDefault={false}
onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
@@ -948,23 +951,23 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
Indexing
{t(Keys.panes.addCollection.indexing)}
</Text>
<TooltipHost
directionalHint={DirectionalHint.bottomLeftEdge}
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
content={t(Keys.panes.addCollection.mongoIndexingTooltip)}
>
<Icon
iconName="Info"
className="panelInfoIcon"
tabIndex={0}
ariaLabel="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
ariaLabel={t(Keys.panes.addCollection.mongoIndexingTooltip)}
/>
</TooltipHost>
</Stack>
<Checkbox
label="Create a Wildcard Index on all fields"
label={t(Keys.panes.addCollection.createWildcardIndex)}
checked={this.state.createMongoWildCardIndex}
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -986,7 +989,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
{userContext.apiType === "SQL" && (
<Stack className="panelGroupSpacing">
<Checkbox
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
label={t(Keys.panes.addCollection.legacySdkCheckbox)}
checked={this.state.useHashV1}
styles={{
text: { fontSize: 12, color: "var(--colorNeutralForeground1)" },
@@ -1003,11 +1006,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
/>
<Text variant="small" style={{ color: "var(--colorNeutralForeground1)" }}>
<Icon iconName="InfoSolid" className="removeIcon" /> To ensure compatibility with older SDKs, the
created container will use a legacy partitioning scheme that supports partition key values of size
only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.{" "}
<Icon iconName="InfoSolid" className="removeIcon" /> {t(Keys.panes.addCollection.legacySdkInfo)}{" "}
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
Learn more
{t(Keys.common.learnMore)}
</Link>
</Text>
</Stack>
@@ -1018,7 +1019,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</div>
{!this.props.isCopyJobFlow && (
<PanelFooterComponent buttonLabel="OK" isButtonDisabled={this.state.isThroughputCapExceeded} />
<PanelFooterComponent buttonLabel={t(Keys.common.ok)} isButtonDisabled={this.state.isThroughputCapExceeded} />
)}
{this.state.isExecuting && (
@@ -1044,7 +1045,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
progressTrack: { backgroundColor: "#A6A6A6" },
progressBar: { background: "white" },
}}
label="Adding sample data set"
label={t(Keys.panes.addCollection.addingSampleDataSet)}
/>
</TeachingBubble>
)}
@@ -1150,8 +1151,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
private getFreeTierIndexingText(): string {
return this.state.enableIndexing
? "All properties in your documents will be indexed by default for flexible and efficient queries."
: "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.";
? t(Keys.panes.addCollection.indexingOnInfo)
: t(Keys.panes.addCollection.indexingOffInfo);
}
private getPartitionKeySubtext(): string {
@@ -1249,14 +1250,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
const throughput = this.state.createNewDatabase ? this.newDatabaseThroughput : this.collectionThroughput;
if (throughput > CollectionCreation.DefaultCollectionRUs100K && !this.isCostAcknowledged) {
const errorMessage = this.isNewDatabaseAutoscale
? "Please acknowledge the estimated monthly spend."
: "Please acknowledge the estimated daily spend.";
? t(Keys.panes.addCollection.acknowledgeSpendErrorMonthly)
: t(Keys.panes.addCollection.acknowledgeSpendErrorDaily);
this.setState({ errorMessage });
return false;
}
if (throughput > CollectionCreation.MaxRUPerPartition && !this.state.isSharded) {
this.setState({ errorMessage: "Unsharded collections support up to 10,000 RUs" });
this.setState({ errorMessage: t(Keys.panes.addCollection.unshardedMaxRuError) });
return false;
}
@@ -1270,12 +1271,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
if (this.shouldShowVectorSearchParameters()) {
if (!this.state.vectorPolicyValidated) {
this.setState({ errorMessage: "Please fix errors in container vector policy" });
this.setState({ errorMessage: t(Keys.panes.addCollection.vectorPolicyError) });
return false;
}
if (!this.state.fullTextPolicyValidated) {
this.setState({ errorMessage: "Please fix errors in container full text search polilcy" });
this.setState({ errorMessage: t(Keys.panes.addCollection.fullTextSearchPolicyError) });
return false;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,9 +34,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
<span
className="css-109"
>
Confirm by typing the
container
id
Confirm by typing the container id
</span>
</Text>
<StyledTextFieldBase

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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="EnableQueryControl"
ariaLabel="Enable query control"
checked={false}
className="padding"
onChange={[Function]}
@@ -1190,7 +1190,8 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
<div
className="___j7dlp70_0000000 fq02s40 f19n0e5"
>
Display Gremlin query results as: 
Display Gremlin query results as:
 
</div>
</AccordionHeader>
<AccordionPanel>

View File

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

View File

@@ -1,5 +1,7 @@
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as _ from "underscore";
@@ -100,8 +102,8 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
for (let i = 0; i < entities.length; i++) {
const { property, type, value } = entities[i];
if ((property === "PartitionKey" && value === "") || (property === "RowKey" && value === "")) {
logConsoleError(`${property} cannot be empty. Please input a value for ${property}`);
setFormError(`${property} cannot be empty. Please input a value for ${property}`);
logConsoleError(t(Keys.panes.tables.propertyEmptyError, { property }));
setFormError(t(Keys.panes.tables.propertyEmptyError, { property }));
return;
}
@@ -109,13 +111,13 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
(property === "PartitionKey" && containsAnyWhiteSpace(value) === true) ||
(property === "RowKey" && containsAnyWhiteSpace(value) === true)
) {
logConsoleError(`${property} cannot have whitespace. Please input a value for ${property} without whitespace`);
setFormError(`${property} cannot have whitespace. Please input a value for ${property} without whitespace`);
logConsoleError(t(Keys.panes.tables.whitespaceError, { property }));
setFormError(t(Keys.panes.tables.whitespaceError, { property }));
return;
}
if (!type) {
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
setFormError(t(Keys.panes.tables.propertyTypeEmptyError, { property }));
return;
}

View File

@@ -1,5 +1,7 @@
import { IDropdownOption, Image, Label, Stack, Text, TextField } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as _ from "underscore";
@@ -198,7 +200,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
}
if (!type) {
setFormError(`Property type cannot be empty. Please select a type from the dropdown for property ${property}`);
setFormError(t(Keys.panes.tables.propertyTypeEmptyError, { property }));
return;
}
@@ -208,8 +210,8 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
(property === "RowKey" && value === "") ||
(property === "RowKey" && value === undefined)
) {
logConsoleError(`${property} cannot be empty. Please input a value for ${property}`);
setFormError(`${property} cannot be empty. Please input a value for ${property}`);
logConsoleError(t(Keys.panes.tables.propertyEmptyError, { property }));
setFormError(t(Keys.panes.tables.propertyEmptyError, { property }));
return;
}
}
@@ -403,7 +405,7 @@ export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps>
)}
</div>
<div className="panelNullWarning" style={{ padding: "20px", color: "red" }}>
Warning: Null fields will not be displayed for editing.
{t(Keys.panes.tables.nullFieldsWarning)}
</div>
</RightPaneForm>
);

View File

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

View File

@@ -13,6 +13,8 @@ import {
} from "@fluentui/react";
import { Upload } from "Common/Upload/Upload";
import { UploadDetailsRecord } from "Contracts/ViewModels";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import { logConsoleError } from "Utils/NotificationConsoleUtils";
import React, { ChangeEvent, FunctionComponent, useReducer, useState } from "react";
import { getErrorMessage } from "../../Tables/Utilities";
@@ -63,8 +65,8 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
const onSubmit = () => {
setFormError("");
if (!files || files.length === 0) {
setFormError("No files were specified. Please input at least one file.");
logConsoleError("Could not upload items -- No files were specified. Please input at least one file.");
setFormError(t(Keys.panes.uploadItems.noFilesSpecifiedError));
logConsoleError(t(Keys.panes.uploadItems.noFilesSpecifiedError));
return;
}
@@ -150,7 +152,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
},
{
key: "fileName",
name: "FILE NAME",
name: t(Keys.panes.uploadItems.fileNameColumn),
fieldName: "fileName",
minWidth: 120,
maxWidth: 140,
@@ -169,7 +171,7 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
},
{
key: "status",
name: "STATUS",
name: t(Keys.panes.uploadItems.statusColumn),
fieldName: "numSucceeded",
minWidth: 120,
maxWidth: 140,
@@ -178,7 +180,11 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
data: "string",
isPadded: true,
onRender: (item: UploadDetailsRecord, index: number, column: IColumn) => {
const fieldContent = `${item.numSucceeded} created, ${item.numThrottled} throttled, ${item.numFailed} errors`;
const fieldContent = t(Keys.panes.uploadItems.uploadStatus, {
numSucceeded: item.numSucceeded,
numThrottled: item.numThrottled,
numFailed: item.numFailed,
});
return (
<TooltipHost
content={fieldContent}
@@ -197,12 +203,12 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
<div className="paneMainContent">
<Upload
key={reducer} // Force re-render on state change
label="Select JSON Files"
label={t(Keys.panes.uploadItems.selectJsonFiles)}
onUpload={updateSelectedFiles}
accept="application/json"
multiple
tabIndex={0}
tooltip="Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB. You can perform multiple upload operations for larger data sets."
tooltip={t(Keys.panes.uploadItems.selectJsonFilesTooltip)}
/>
{uploadFileData?.length > 0 && (
<div className="fileUploadSummaryContainer" data-test="file-upload-status">

View File

@@ -714,9 +714,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
<span
className="css-126"
>
What is the reason why you are deleting this
Database
?
What is the reason why you are deleting this Database?
</span>
</Text>
<StyledTextFieldBase

View File

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

View File

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

View File

@@ -16,6 +16,8 @@ import { sendMessage } from "Common/MessageHandler";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { TerminalKind } from "Contracts/ViewModels";
import { SplashScreenButton } from "Explorer/SplashScreen/SplashScreenButton";
import { Keys } from "Localization/Keys.generated";
import { t } from "Localization/t";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
import { useCarousel } from "hooks/useCarousel";
@@ -169,16 +171,16 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
switch (userContext.apiType) {
case "Postgres":
title = "Welcome to Azure Cosmos DB for PostgreSQL";
subtitle = "Get started with our sample datasets, documentation, and additional tools.";
title = t(Keys.splashScreen.title.postgres);
subtitle = t(Keys.splashScreen.subtitle.getStarted);
break;
case "VCoreMongo":
title = "Welcome to Azure DocumentDB (with MongoDB compatibility)";
subtitle = "Get started with our sample datasets, documentation, and additional tools.";
title = t(Keys.splashScreen.title.vcoreMongo);
subtitle = t(Keys.splashScreen.subtitle.getStarted);
break;
default:
title = "Welcome to Azure Cosmos DB";
subtitle = "Globally distributed, multi-model database service for any scale";
title = t(Keys.splashScreen.title.default);
subtitle = t(Keys.splashScreen.subtitle.default);
}
React.useEffect(() => {
@@ -249,8 +251,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
<Stack className="splashStackRow" horizontal>
<SplashScreenButton
imgSrc={QuickStartIcon}
title={"Launch quick start"}
description={"Launch a quick start tutorial to get started with sample data"}
title={t(Keys.splashScreen.quickStart.title)}
description={t(Keys.splashScreen.quickStart.description)}
onClick={() => {
container.onNewCollectionClicked({ isQuickstart: true });
traceOpen(Action.LaunchQuickstart, { apiType: userContext.apiType });
@@ -258,8 +260,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
/>
<SplashScreenButton
imgSrc={ContainersIcon}
title={`New ${getCollectionName()}`}
description={"Create a new container for storage and throughput"}
title={t(Keys.splashScreen.newCollection.title, { collectionName: getCollectionName() })}
description={t(Keys.splashScreen.newCollection.description)}
onClick={() => {
container.onNewCollectionClicked();
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
@@ -270,10 +272,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
<SplashScreenButton
imgSrc={CosmosDBIcon}
imgSize={35}
title={"Azure Cosmos DB Samples Gallery"}
description={
"Discover samples that showcase scalable, intelligent app patterns. Try one now to see how fast you can go from concept to code with Cosmos DB"
}
title={t(Keys.splashScreen.samplesGallery.title)}
description={t(Keys.splashScreen.samplesGallery.description)}
onClick={() => {
window.open("https://azurecosmosdb.github.io/gallery/?tags=example", "_blank");
traceOpen(Action.LearningResourcesClicked, { apiType: userContext.apiType });
@@ -281,8 +281,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
/>
<SplashScreenButton
imgSrc={ConnectIcon}
title={"Connect"}
description={"Prefer using your own choice of tooling? Find the connection string you need to connect"}
title={t(Keys.splashScreen.connectCard.title)}
description={t(Keys.splashScreen.connectCard.description)}
onClick={() => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect)}
/>
</Stack>
@@ -297,7 +297,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
usePostgres.getState().showPostgreTeachingBubble &&
!usePostgres.getState().showResetPasswordBubble && (
<TeachingBubble
headline="New to Cosmos DB PGSQL?"
headline={t(Keys.splashScreen.teachingBubble.newToPostgres.headline)}
target={"#mainButton-quickstartDescription"}
hasCloseButton
onDismiss={() => usePostgres.getState().setShowPostgreTeachingBubble(false)}
@@ -309,15 +309,14 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
preventDismissOnScroll: true,
}}
primaryButtonProps={{
text: "Get started",
text: t(Keys.common.getStarted),
onClick: () => {
useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart);
usePostgres.getState().setShowPostgreTeachingBubble(false);
},
}}
>
Welcome! If you are new to Cosmos DB PGSQL and need help with getting started, here is where you can find
sample data, query.
{t(Keys.splashScreen.teachingBubble.newToPostgres.body)}
</TeachingBubble>
)}
{/*TODO: convert below to use SplashScreenButton */}
@@ -349,7 +348,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
))}
{userContext.apiType === "Postgres" && usePostgres.getState().showResetPasswordBubble && (
<TeachingBubble
headline="Create your password"
headline={t(Keys.splashScreen.teachingBubble.resetPassword.headline)}
target={"#mainButton-quickstartDescription"}
hasCloseButton
onDismiss={() => {
@@ -364,7 +363,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
preventDismissOnScroll: true,
}}
primaryButtonProps={{
text: "Create",
text: t(Keys.common.create),
onClick: () => {
localStorage.setItem(userContext.databaseAccount.id, "true");
sendMessage({
@@ -374,7 +373,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
},
}}
>
If you haven&apos;t changed your password yet, change it now.
{t(Keys.splashScreen.teachingBubble.resetPassword.body)}
</TeachingBubble>
)}
</div>
@@ -393,8 +392,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
const launchQuickstartBtn = {
id: "quickstartDescription",
iconSrc: QuickStartIcon,
title: "Launch quick start",
description: "Launch a quick start tutorial to get started with sample data",
title: t(Keys.splashScreen.quickStart.title),
description: t(Keys.splashScreen.quickStart.description),
onClick: () => {
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
useTabs.getState().openAndActivateReactTab(ReactTabKind.Quickstart);
@@ -416,8 +415,8 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
if (userContext.apiType === "Postgres") {
return {
iconSrc: PowerShellIcon,
title: "PostgreSQL Shell",
description: "Create table and interact with data using PostgreSQL's shell interface",
title: t(Keys.splashScreen.shell.postgres.title),
description: t(Keys.splashScreen.shell.postgres.description),
onClick: () => container.openNotebookTerminal(TerminalKind.Postgres),
};
}
@@ -425,16 +424,16 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
if (userContext.apiType === "VCoreMongo") {
return {
iconSrc: PowerShellIcon,
title: "Mongo Shell",
description: "Create a collection and interact with data using MongoDB's shell interface",
title: t(Keys.splashScreen.shell.vcoreMongo.title),
description: t(Keys.splashScreen.shell.vcoreMongo.description),
onClick: () => container.openNotebookTerminal(TerminalKind.VCoreMongo),
};
}
return {
iconSrc: ContainersIcon,
title: `New ${getCollectionName()}`,
description: "Create a new container for storage and throughput",
title: t(Keys.splashScreen.newCollection.title, { collectionName: getCollectionName() }),
description: t(Keys.splashScreen.newCollection.description),
onClick: () => {
container.onNewCollectionClicked();
traceOpen(Action.NewContainerHomepage, { apiType: userContext.apiType });
@@ -444,19 +443,19 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
const getThirdCard = (): SplashScreenItem => {
let icon = ConnectIcon;
let title = "Connect";
let description = "Prefer using your own choice of tooling? Find the connection string you need to connect";
let title = t(Keys.splashScreen.connectCard.title);
let description = t(Keys.splashScreen.connectCard.description);
let onClick = () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect);
if (userContext.apiType === "Postgres") {
title = "Connect with pgAdmin";
description = "Prefer pgAdmin? Find your connection strings here";
title = t(Keys.splashScreen.connectCard.pgAdmin.title);
description = t(Keys.splashScreen.connectCard.pgAdmin.description);
}
if (userContext.apiType === "VCoreMongo") {
icon = VisualStudioIcon;
title = "Connect with VS Code";
description = "Query and Manage your MongoDB and DocumentDB clusters in Visual Studio Code";
title = t(Keys.splashScreen.connectCard.vsCode.title);
description = t(Keys.splashScreen.connectCard.vsCode.description);
onClick = () => container?.openInVsCode && container.openInVsCode();
}
@@ -485,7 +484,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
info: activity.path,
iconSrc: NotebookIcon,
title: activity.name,
description: "Notebook",
description: t(Keys.splashScreen.sections.notebook),
onClick: () => {
const notebookItem = container.createNotebookContentItemFile(activity.name, activity.path);
notebookItem && container.openNotebook(notebookItem);
@@ -524,18 +523,18 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
items = [
{
link: "https://aka.ms/msl-modeling-partitioning-2",
title: "Advanced Modeling Patterns",
description: "Learn advanced strategies to optimize your database.",
title: t(Keys.splashScreen.top3Items.sql.advancedModeling.title),
description: t(Keys.splashScreen.top3Items.sql.advancedModeling.description),
},
{
link: "https://aka.ms/msl-modeling-partitioning-1",
title: "Partitioning Best Practices",
description: "Learn to apply data model and partitioning strategies.",
title: t(Keys.splashScreen.top3Items.sql.partitioning.title),
description: t(Keys.splashScreen.top3Items.sql.partitioning.description),
},
{
link: "https://aka.ms/msl-resource-planning",
title: "Plan Your Resource Requirements",
description: "Get to know the different configuration choices.",
title: t(Keys.splashScreen.top3Items.sql.resourcePlanning.title),
description: t(Keys.splashScreen.top3Items.sql.resourcePlanning.description),
},
];
break;
@@ -543,18 +542,18 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
items = [
{
link: "https://aka.ms/mongodbintro",
title: "What is the MongoDB API?",
description: "Understand Azure Cosmos DB for MongoDB and its features.",
title: t(Keys.splashScreen.top3Items.mongo.whatIsMongo.title),
description: t(Keys.splashScreen.top3Items.mongo.whatIsMongo.description),
},
{
link: "https://aka.ms/mongodbfeaturesupport",
title: "Features and Syntax",
description: "Discover the advantages and features",
title: t(Keys.splashScreen.top3Items.mongo.features.title),
description: t(Keys.splashScreen.top3Items.mongo.features.description),
},
{
link: "https://aka.ms/mongodbpremigration",
title: "Migrate Your Data",
description: "Pre-migration steps for moving data",
title: t(Keys.splashScreen.top3Items.mongo.migrate.title),
description: t(Keys.splashScreen.top3Items.mongo.migrate.description),
},
];
break;
@@ -562,18 +561,18 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
items = [
{
link: "https://aka.ms/cassandrajava",
title: "Build a Java App",
description: "Create a Java app using an SDK.",
title: t(Keys.splashScreen.top3Items.cassandra.buildJavaApp.title),
description: t(Keys.splashScreen.top3Items.cassandra.buildJavaApp.description),
},
{
link: "https://aka.ms/cassandrapartitioning",
title: "Partitioning Best Practices",
description: "Learn how partitioning works.",
title: t(Keys.splashScreen.top3Items.cassandra.partitioning.title),
description: t(Keys.splashScreen.top3Items.cassandra.partitioning.description),
},
{
link: "https://aka.ms/cassandraRu",
title: "Request Units (RUs)",
description: "Understand RU charges.",
title: t(Keys.splashScreen.top3Items.cassandra.requestUnits.title),
description: t(Keys.splashScreen.top3Items.cassandra.requestUnits.description),
},
];
break;
@@ -581,18 +580,18 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
items = [
{
link: "https://aka.ms/Graphdatamodeling",
title: "Data Modeling",
description: "Graph data modeling recommendations",
title: t(Keys.splashScreen.top3Items.gremlin.dataModeling.title),
description: t(Keys.splashScreen.top3Items.gremlin.dataModeling.description),
},
{
link: "https://aka.ms/graphpartitioning",
title: "Partitioning Best Practices",
description: "Learn how partitioning works",
title: t(Keys.splashScreen.top3Items.gremlin.partitioning.title),
description: t(Keys.splashScreen.top3Items.gremlin.partitioning.description),
},
{
link: "https://aka.ms/graphapiquery",
title: "Query Data",
description: "Querying data with Gremlin",
title: t(Keys.splashScreen.top3Items.gremlin.queryData.title),
description: t(Keys.splashScreen.top3Items.gremlin.queryData.description),
},
];
break;
@@ -600,18 +599,18 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
items = [
{
link: "https://aka.ms/tableintro",
title: "What is the Table API?",
description: "Understand Azure Cosmos DB for Table and its features",
title: t(Keys.splashScreen.top3Items.tables.whatIsTable.title),
description: t(Keys.splashScreen.top3Items.tables.whatIsTable.description),
},
{
link: "https://aka.ms/tableimport",
title: "Migrate your data",
description: "Learn how to migrate your data",
title: t(Keys.splashScreen.top3Items.tables.migrate.title),
description: t(Keys.splashScreen.top3Items.tables.migrate.description),
},
{
link: "https://aka.ms/tablefaq",
title: "Azure Cosmos DB for Table FAQs",
description: "Common questions about Azure Cosmos DB for Table",
title: t(Keys.splashScreen.top3Items.tables.faq.title),
description: t(Keys.splashScreen.top3Items.tables.faq.description),
},
];
break;
@@ -668,7 +667,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
</ul>
{recentItems.length > 0 && (
<Link onClick={() => clearMostRecent()} className={styles.listItemTitle}>
Clear Recents
{t(Keys.splashScreen.sections.clearRecents)}
</Link>
)}
</Stack>
@@ -683,15 +682,15 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
}
const cdbLiveTv: item = {
link: "https://developer.azurecosmosdb.com/tv",
title: "Learn the Fundamentals",
description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.",
title: t(Keys.splashScreen.learningResources.liveTv.title),
description: t(Keys.splashScreen.learningResources.liveTv.description),
};
const commonItems: item[] = [
{
link: "https://learn.microsoft.com/azure/cosmos-db/data-explorer-shortcuts",
title: "Data Explorer keyboard shortcuts",
description: "Learn keyboard shortcuts to navigate Data Explorer.",
title: t(Keys.splashScreen.learningResources.shortcuts.title),
description: t(Keys.splashScreen.learningResources.shortcuts.description),
},
];
@@ -702,14 +701,14 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
apiItems = [
{
link: "https://aka.ms/msl-sdk-connect",
title: "Get Started using an SDK",
description: "Learn about the Azure Cosmos DB SDK.",
title: t(Keys.splashScreen.learningResources.sql.sdk.title),
description: t(Keys.splashScreen.learningResources.sql.sdk.description),
},
cdbLiveTv,
{
link: "https://aka.ms/msl-move-data",
title: "Migrate Your Data",
description: "Migrate data using Azure services and open-source solutions.",
title: t(Keys.splashScreen.learningResources.sql.migrate.title),
description: t(Keys.splashScreen.learningResources.sql.migrate.description),
},
];
break;
@@ -717,13 +716,13 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
apiItems = [
{
link: "https://aka.ms/mongonodejs",
title: "Build an app with Node.js",
description: "Create a Node.js app.",
title: t(Keys.splashScreen.learningResources.mongo.nodejs.title),
description: t(Keys.splashScreen.learningResources.mongo.nodejs.description),
},
{
link: "https://aka.ms/mongopython",
title: "Getting Started Guide",
description: "Learn the basics to get started.",
title: t(Keys.splashScreen.learningResources.mongo.gettingStarted.title),
description: t(Keys.splashScreen.learningResources.mongo.gettingStarted.description),
},
cdbLiveTv,
];
@@ -732,14 +731,14 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
apiItems = [
{
link: "https://aka.ms/cassandracontainer",
title: "Create a Container",
description: "Get to know the create a container options.",
title: t(Keys.splashScreen.learningResources.cassandra.createContainer.title),
description: t(Keys.splashScreen.learningResources.cassandra.createContainer.description),
},
cdbLiveTv,
{
link: "https://aka.ms/Cassandrathroughput",
title: "Provision Throughput",
description: "Learn how to configure throughput.",
title: t(Keys.splashScreen.learningResources.cassandra.throughput.title),
description: t(Keys.splashScreen.learningResources.cassandra.throughput.description),
},
];
break;
@@ -747,13 +746,13 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
apiItems = [
{
link: "https://aka.ms/graphquickstart",
title: "Get Started ",
description: "Create, query, and traverse using the Gremlin console",
title: t(Keys.splashScreen.learningResources.gremlin.getStarted.title),
description: t(Keys.splashScreen.learningResources.gremlin.getStarted.description),
},
{
link: "https://aka.ms/graphimport",
title: "Import Graph Data",
description: "Learn Bulk ingestion data using BulkExecutor",
title: t(Keys.splashScreen.learningResources.gremlin.importData.title),
description: t(Keys.splashScreen.learningResources.gremlin.importData.description),
},
cdbLiveTv,
];
@@ -762,13 +761,13 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
apiItems = [
{
link: "https://aka.ms/tabledotnet",
title: "Build a .NET App",
description: "How to access Azure Cosmos DB for Table from a .NET app.",
title: t(Keys.splashScreen.learningResources.tables.dotnet.title),
description: t(Keys.splashScreen.learningResources.tables.dotnet.description),
},
{
link: "https://aka.ms/Tablejava",
title: "Build a Java App",
description: "Create a Azure Cosmos DB for Table app with Java SDK ",
title: t(Keys.splashScreen.learningResources.tables.java.title),
description: t(Keys.splashScreen.learningResources.tables.java.description),
},
cdbLiveTv,
];
@@ -807,17 +806,17 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
const postgresNextStepItems: { link: string; title: string; description: string }[] = [
{
link: "https://go.microsoft.com/fwlink/?linkid=2208312",
title: "Data Modeling",
title: t(Keys.splashScreen.nextStepItems.postgres.dataModeling),
description: "",
},
{
link: " https://go.microsoft.com/fwlink/?linkid=2206941 ",
title: "How to choose a Distribution Column",
title: t(Keys.splashScreen.nextStepItems.postgres.distributionColumn),
description: "",
},
{
link: "https://go.microsoft.com/fwlink/?linkid=2207425",
title: "Build Apps with Python/Java/Django",
title: t(Keys.splashScreen.nextStepItems.postgres.buildApps),
description: "",
},
];
@@ -825,17 +824,17 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
const vcoreMongoNextStepItems: { link: string; title: string; description: string }[] = [
{
link: "https://learn.microsoft.com/azure/cosmos-db/mongodb/vcore/migration-options",
title: "Migrate Data",
title: t(Keys.splashScreen.nextStepItems.vcoreMongo.migrateData),
description: "",
},
{
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/vector-search-ai",
title: "Build AI apps with Vector Search",
title: t(Keys.splashScreen.nextStepItems.vcoreMongo.vectorSearch),
description: "",
},
{
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/tutorial-nodejs-web-app?tabs=github-codespaces",
title: "Build Apps with Nodejs",
title: t(Keys.splashScreen.nextStepItems.vcoreMongo.buildApps),
description: "",
},
];
@@ -863,17 +862,17 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
const postgresLearnMoreItems: { link: string; title: string; description: string }[] = [
{
link: "https://go.microsoft.com/fwlink/?linkid=2207226",
title: "Performance Tuning",
title: t(Keys.splashScreen.learnMoreItems.postgres.performanceTuning),
description: "",
},
{
link: "https://go.microsoft.com/fwlink/?linkid=2208037",
title: "Useful Diagnostic Queries",
title: t(Keys.splashScreen.learnMoreItems.postgres.diagnosticQueries),
description: "",
},
{
link: "https://go.microsoft.com/fwlink/?linkid=2205270",
title: "Distributed SQL Reference",
title: t(Keys.splashScreen.learnMoreItems.postgres.sqlReference),
description: "",
},
];
@@ -881,17 +880,17 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
const vcoreMongoLearnMoreItems: { link: string; title: string; description: string }[] = [
{
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/vector-search",
title: "Vector Search",
title: t(Keys.splashScreen.learnMoreItems.vcoreMongo.vectorSearch),
description: "",
},
{
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/how-to-create-text-index",
title: "Text Indexing",
title: t(Keys.splashScreen.learnMoreItems.vcoreMongo.textIndexing),
description: "",
},
{
link: "https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/vcore/troubleshoot-common-issues",
title: "Troubleshoot common issues",
title: t(Keys.splashScreen.learnMoreItems.vcoreMongo.troubleshoot),
description: "",
},
];
@@ -932,24 +931,25 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
persistentBeak
>
<TeachingBubbleContent
headline={`Start with sample ${getCollectionName().toLocaleLowerCase()}`}
headline={t(Keys.splashScreen.teachingBubble.coachMark.headline, {
collectionName: getCollectionName().toLocaleLowerCase(),
})}
hasCloseButton
closeButtonAriaLabel="Close"
closeButtonAriaLabel={t(Keys.common.close)}
primaryButtonProps={{
text: "Get started",
text: t(Keys.common.getStarted),
onClick: () => {
useCarousel.getState().setShowCoachMark(false);
container.onNewCollectionClicked({ isQuickstart: true });
},
}}
secondaryButtonProps={{
text: "Cancel",
text: t(Keys.common.cancel),
onClick: () => useCarousel.getState().setShowCoachMark(false),
}}
onDismiss={() => useCarousel.getState().setShowCoachMark(false)}
>
You will be guided to create a sample container with sample data, then we will give you a tour of data
explorer. You can also cancel launching this tour and explore yourself
{t(Keys.splashScreen.teachingBubble.coachMark.body)}
</TeachingBubbleContent>
</Coachmark>
)}
@@ -963,7 +963,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
fontFamily: '"Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif',
}}
>
Next steps
{t(Keys.splashScreen.sections.nextSteps)}
</Text>
{getNextStepItems()}
</Stack>
@@ -975,7 +975,7 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
fontFamily: '"Segoe UI Semibold", "Segoe UI", "Segoe WP", Tahoma, Arial, sans-serif',
}}
>
Tips & learn more
{t(Keys.splashScreen.sections.tipsAndLearnMore)}
</Text>
{getTipsAndLearnMoreItems()}
</Stack>
@@ -984,15 +984,15 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
) : (
<div className={styles.moreStuffContainer}>
<div className={styles.moreStuffColumn}>
<h2 className={styles.columnTitle}>Recents</h2>
<h2 className={styles.columnTitle}>{t(Keys.splashScreen.sections.recents)}</h2>
{getRecentItems()}
</div>
<div className={styles.moreStuffColumn}>
<h2 className={styles.columnTitle}>Top 3 things you need to know</h2>
<h2 className={styles.columnTitle}>{t(Keys.splashScreen.sections.top3)}</h2>
{top3Items()}
</div>
<div className={styles.moreStuffColumn}>
<h2 className={styles.columnTitle}>Learning Resources</h2>
<h2 className={styles.columnTitle}>{t(Keys.splashScreen.sections.learningResources)}</h2>
{getLearningResourceItems()}
</div>
</div>

View File

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

View File

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

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,6 +1,8 @@
import React, { Component } from "react";
import { configContext } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Keys } from "../../../Localization/Keys.generated";
import { t } from "../../../Localization/t";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext";
@@ -212,7 +214,7 @@ export default class MongoShellTabComponent extends Component<
src={this.state.url}
id={this.props.tabsBaseInstance.tabId}
onLoad={(event) => this.setContentFocus(event)}
title="Mongo Shell"
title={t(Keys.tabs.mongoShell.title)}
role="tabpanel"
></iframe>
);

View File

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

View File

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

View File

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

View File

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

View File

@@ -141,7 +141,6 @@ 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,3 +1,4 @@
import "./i18n";
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Arrow from "../images/Arrow.svg";

View File

@@ -0,0 +1,14 @@
{
"Projects": [
{
"LanguageSet": "Azure_LanguagesExt",
"LocItems": [
{
"SourceFile": "src\\Localization\\en\\Resources.json",
"CopyOption": "LangIDOnPath",
"OutputPath": "src\\Localization"
}
]
}
]
}

View File

@@ -0,0 +1,295 @@
{
"common": {
"ok": "OK",
"cancel": "Zrušit",
"close": "Zavřít",
"save": "Uložit",
"delete": "Odstranit",
"update": "Aktualizovat",
"discard": "Zahodit",
"execute": "Execute",
"loading": "Načítání",
"loadingEllipsis": "Načítání…",
"next": "Další",
"previous": "Předchozí",
"yes": "Ano",
"no": "Ne",
"result": "Výsledek",
"learnMore": "Další informace",
"getStarted": "Začínáme",
"retry": "Zkusit znovu",
"apply": "Použít",
"refresh": "Aktualizovat",
"copy": "Kopírovat",
"create": "Vytvořit",
"confirm": "Potvrdit",
"open": "Otevřít",
"rename": "Přejmenovat",
"download": "Stáhnout",
"upload": "Nahrát",
"connect": "Připojit",
"remove": "Odebrat",
"increaseValueBy1": "Zvýšit hodnotu o 1",
"decreaseValueBy1": "Snížit hodnotu o 1"
},
"splashScreen": {
"title": {
"default": "Vítá vás Azure Cosmos DB",
"postgres": "Vítá vás Azure Cosmos DB for PostgreSQL",
"vcoreMongo": "Vítá vás Azure DocumentDB (s kompatibilitou MongoDB)"
},
"subtitle": {
"default": "Globálně distribuovaná databázová služba s více modely pro libovolné škálování",
"getStarted": "Začněte s našimi ukázkovými datovými sadami, dokumentací a dalšími nástroji."
},
"quickStart": {
"title": "Launch quick start",
"description": "Launch a quick start tutorial to get started with sample data"
},
"newCollection": {
"title": "New {{collectionName}}",
"description": "Create a new container for storage and throughput"
},
"samplesGallery": {
"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"
},
"connectCard": {
"title": "Connect",
"description": "Prefer using your own choice of tooling? Find the connection string you need to connect",
"pgAdmin": {
"title": "Connect with pgAdmin",
"description": "Prefer pgAdmin? Find your connection strings here"
},
"vsCode": {
"title": "Připojení pomocí VS Code",
"description": "Query and Manage your MongoDB and DocumentDB clusters in Visual Studio Code"
}
},
"shell": {
"postgres": {
"title": "PostgreSQL Shell",
"description": "Create table and interact with data using PostgreSQL's shell interface"
},
"vcoreMongo": {
"title": "Mongo Shell",
"description": "Create a collection and interact with data using MongoDB's shell interface"
}
},
"teachingBubble": {
"newToPostgres": {
"headline": "New to Cosmos DB PGSQL?",
"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."
},
"resetPassword": {
"headline": "Create your password",
"body": "If you haven't changed your password yet, change it now."
},
"coachMark": {
"headline": "Start with sample {{collectionName}}",
"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"
}
},
"sections": {
"recents": "Recents",
"clearRecents": "Clear Recents",
"top3": "Top 3 things you need to know",
"learningResources": "Learning Resources",
"nextSteps": "Next steps",
"tipsAndLearnMore": "Tips & learn more",
"notebook": "Notebook",
"needHelp": "Need help?"
},
"top3Items": {
"sql": {
"advancedModeling": {
"title": "Advanced Modeling Patterns",
"description": "Learn advanced strategies to optimize your database."
},
"partitioning": {
"title": "Partitioning Best Practices",
"description": "Learn to apply data model and partitioning strategies."
},
"resourcePlanning": {
"title": "Plan Your Resource Requirements",
"description": "Get to know the different configuration choices."
}
},
"mongo": {
"whatIsMongo": {
"title": "What is the MongoDB API?",
"description": "Understand Azure Cosmos DB for MongoDB and its features."
},
"features": {
"title": "Features and Syntax",
"description": "Discover the advantages and features"
},
"migrate": {
"title": "Migrate Your Data",
"description": "Pre-migration steps for moving data"
}
},
"cassandra": {
"buildJavaApp": {
"title": "Build a Java App",
"description": "Create a Java app using an SDK."
},
"partitioning": {
"title": "Partitioning Best Practices",
"description": "Learn how partitioning works."
},
"requestUnits": {
"title": "Request Units (RUs)",
"description": "Understand RU charges."
}
},
"gremlin": {
"dataModeling": {
"title": "Data Modeling",
"description": "Graph data modeling recommendations"
},
"partitioning": {
"title": "Partitioning Best Practices",
"description": "Learn how partitioning works"
},
"queryData": {
"title": "Query Data",
"description": "Querying data with Gremlin"
}
},
"tables": {
"whatIsTable": {
"title": "What is the Table API?",
"description": "Understand Azure Cosmos DB for Table and its features"
},
"migrate": {
"title": "Migrate your data",
"description": "Learn how to migrate your data"
},
"faq": {
"title": "Azure Cosmos DB for Table FAQs",
"description": "Common questions about Azure Cosmos DB for Table"
}
}
},
"learningResources": {
"shortcuts": {
"title": "Data Explorer keyboard shortcuts",
"description": "Learn keyboard shortcuts to navigate Data Explorer."
},
"liveTv": {
"title": "Learn the Fundamentals",
"description": "Watch Azure Cosmos DB Live TV show introductory and how to videos."
},
"sql": {
"sdk": {
"title": "Get Started using an SDK",
"description": "Learn about the Azure Cosmos DB SDK."
},
"migrate": {
"title": "Migrate Your Data",
"description": "Migrate data using Azure services and open-source solutions."
}
},
"mongo": {
"nodejs": {
"title": "Build an app with Node.js",
"description": "Create a Node.js app."
},
"gettingStarted": {
"title": "Getting Started Guide",
"description": "Learn the basics to get started."
}
},
"cassandra": {
"createContainer": {
"title": "Create a Container",
"description": "Get to know the create a container options."
},
"throughput": {
"title": "Provision Throughput",
"description": "Learn how to configure throughput."
}
},
"gremlin": {
"getStarted": {
"title": "Get Started ",
"description": "Create, query, and traverse using the Gremlin console"
},
"importData": {
"title": "Import Graph Data",
"description": "Learn Bulk ingestion data using BulkExecutor"
}
},
"tables": {
"dotnet": {
"title": "Build a .NET App",
"description": "How to access Azure Cosmos DB for Table from a .NET app."
},
"java": {
"title": "Build a Java App",
"description": "Create a Azure Cosmos DB for Table app with Java SDK "
}
}
},
"nextStepItems": {
"postgres": {
"dataModeling": "Data Modeling",
"distributionColumn": "How to choose a Distribution Column",
"buildApps": "Build Apps with Python/Java/Django"
},
"vcoreMongo": {
"migrateData": "Migrate Data",
"vectorSearch": "Build AI apps with Vector Search",
"buildApps": "Build Apps with Nodejs"
}
},
"learnMoreItems": {
"postgres": {
"performanceTuning": "Performance Tuning",
"diagnosticQueries": "Useful Diagnostic Queries",
"sqlReference": "Distributed SQL Reference"
},
"vcoreMongo": {
"vectorSearch": "Vector Search",
"textIndexing": "Text Indexing",
"troubleshoot": "Troubleshoot common issues"
}
},
"fabric": {
"buildTitle": "Build your database",
"useTitle": "Use your database",
"newContainer": {
"title": "New container",
"description": "Create a destination container to store your data"
},
"sampleData": {
"title": "Sample Data",
"description": "Load sample data in your database"
},
"sampleVectorData": {
"title": "Sample Vector Data",
"description": "Load sample vector data with text-embedding-ada-002"
},
"appDevelopment": {
"title": "App development",
"description": "Start here to use an SDK to build your apps"
},
"sampleGallery": {
"title": "Sample Gallery",
"description": "Get real-world end-to-end samples"
}
},
"sampleDataDialog": {
"title": "Sample Data",
"startButton": "Start",
"createPrompt": "Create a container \"{{containerName}}\" and import sample data into it. This may take a few minutes.",
"creatingContainer": "Creating container \"{{containerName}}\"...",
"importingData": "Importing data into \"{{containerName}}\"...",
"success": "Successfully created \"{{containerName}}\" with sample data.",
"errorContainerExists": "The container \"{{containerName}}\" in database \"{{databaseName}}\" already exists. Please delete it and retry.",
"errorCreateContainer": "Failed to create container: {{error}}",
"errorImportData": "Failed to import data: {{error}}"
}
}
}

View File

@@ -0,0 +1,295 @@
{
"common": {
"ok": "OK",
"cancel": "Abbrechen",
"close": "Schließen",
"save": "Speichern",
"delete": "Löschen",
"update": "Aktualisieren",
"discard": "Verwerfen",
"execute": "Ausführen",
"loading": "Wird geladen",
"loadingEllipsis": "Wird geladen...",
"next": "Weiter",
"previous": "Zurück",
"yes": "Ja",
"no": "Nein",
"result": "Ergebnis",
"learnMore": "Weitere Informationen",
"getStarted": "Erste Schritte",
"retry": "Wiederholen",
"apply": "Anwenden",
"refresh": "Aktualisieren",
"copy": "Kopieren",
"create": "Erstellen",
"confirm": "Bestätigen",
"open": "Öffnen",
"rename": "Umbenennen",
"download": "Herunterladen",
"upload": "Hochladen",
"connect": "Verbinden",
"remove": "Entfernen",
"increaseValueBy1": "Wert um 1 erhöhen",
"decreaseValueBy1": "Wert um 1 verringern"
},
"splashScreen": {
"title": {
"default": "Willkommen bei Azure Cosmos DB",
"postgres": "Willkommen bei Azure Cosmos DB for PostgreSQL",
"vcoreMongo": "Willkommen bei Azure DocumentDB (mit MongoDB-Kompatibilität)"
},
"subtitle": {
"default": "Global verteilter Datenbankdienst mit Unterstützung mehrerer Datenmodelle in jeder Größenordnung",
"getStarted": "Erste Schritte mit unseren Beispieldatensätzen, der Dokumentation und weiteren Tools."
},
"quickStart": {
"title": "Schnellstart starten",
"description": "Starten Sie ein Schnellstarttutorial, um mit Beispieldaten zu beginnen."
},
"newCollection": {
"title": "Neue {{collectionName}}",
"description": "Neuen Container für Speicher und Durchsatz erstellen"
},
"samplesGallery": {
"title": "Azure Cosmos DB-Beispielgalerie",
"description": "Entdecken Sie Beispiele, die skalierbare, intelligente App-Muster zeigen. Probieren Sie jetzt eines aus, um zu sehen, wie schnell Sie mit Cosmos DB vom Konzept zum Code gelangen."
},
"connectCard": {
"title": "Verbinden",
"description": "Bevorzugen Sie Ihre eigene Toolauswahl? Finden Sie die Verbindungszeichenfolge, die Sie für die Verbindung benötigen.",
"pgAdmin": {
"title": "Verbindung mit pgAdmin herstellen",
"description": "pgAdmin bevorzugen? Hier finden Sie Ihre Verbindungszeichenfolgen."
},
"vsCode": {
"title": "Mit VS Code verbinden",
"description": "Abfragen und Verwalten Ihrer MongoDB- und DocumentDB-Cluster in Visual Studio Code."
}
},
"shell": {
"postgres": {
"title": "PostgreSQL-Shell",
"description": "Erstellen Sie eine Tabelle und interagieren Sie mit Daten über die Shellschnittstelle von PostgreSQL."
},
"vcoreMongo": {
"title": "Mongo-Shell",
"description": "Erstellen Sie eine Sammlung und interagieren Sie mit Daten über die Shellschnittstelle von MongoDB."
}
},
"teachingBubble": {
"newToPostgres": {
"headline": "Neu bei Cosmos DB PGSQL?",
"body": "Willkommen! Wenn Sie neu bei Cosmos DB PGSQL sind und Hilfe beim Einstieg benötigen, finden Sie hier Beispieldaten und Abfragen."
},
"resetPassword": {
"headline": "Ihr Kennwort erstellen",
"body": "Wenn Sie Ihr Kennwort noch nicht geändert haben, ändern Sie es jetzt."
},
"coachMark": {
"headline": "Mit Beispiel „{{collectionName}}“ starten",
"body": "Sie werden angeleitet, einen Beispielcontainer mit Beispieldaten zu erstellen. Anschließend erhalten Sie eine Tour durch den Datenexplorer. Sie können die Tour auch abbrechen und selbst erkunden."
}
},
"sections": {
"recents": "Zuletzt verwendet",
"clearRecents": "Zuletzt verwendete Elemente löschen",
"top3": "Die drei wichtigsten Dinge, die Sie wissen sollten",
"learningResources": "Lernressourcen",
"nextSteps": "Nächste Schritte",
"tipsAndLearnMore": "Tipps und weitere Informationen",
"notebook": "Notebook",
"needHelp": "Benötigen Sie Hilfe?"
},
"top3Items": {
"sql": {
"advancedModeling": {
"title": "Erweiterte Modellierungsmuster",
"description": "Erfahren Sie mehr über fortgeschrittene Strategien zur Optimierung Ihrer Datenbank."
},
"partitioning": {
"title": "Best Practices für die Partitionierung",
"description": "Lernen Sie, Datenmodell- und Partitionierungsstrategien anzuwenden."
},
"resourcePlanning": {
"title": "Ihre Ressourcenanforderungen planen",
"description": "Machen Sie sich mit den verschiedenen Konfigurationsmöglichkeiten vertraut."
}
},
"mongo": {
"whatIsMongo": {
"title": "Was ist die MongoDB-API?",
"description": "Verstehen Sie Azure Cosmos DB for MongoDB und seine Funktionen."
},
"features": {
"title": "Funktionen und Syntax",
"description": "Entdecken Sie die Vorteile und Funktionen"
},
"migrate": {
"title": "Ihre Daten migrieren",
"description": "Vorbereitende Schritte zur Datenmigration"
}
},
"cassandra": {
"buildJavaApp": {
"title": "Java-App erstellen",
"description": "Java-App mithilfe eines SDK erstellen."
},
"partitioning": {
"title": "Best Practices für die Partitionierung",
"description": "Erfahren Sie, wie Partitionierung funktioniert."
},
"requestUnits": {
"title": "Anforderungseinheiten (RUs)",
"description": "RU-Gebühren verstehen"
}
},
"gremlin": {
"dataModeling": {
"title": "Datenmodellierung",
"description": "Empfehlungen zur Graphdatenmodellierung"
},
"partitioning": {
"title": "Best Practices für die Partitionierung",
"description": "Erfahren Sie, wie Partitionierung funktioniert"
},
"queryData": {
"title": "Daten abfragen",
"description": "Daten mit Gremlin abfragen"
}
},
"tables": {
"whatIsTable": {
"title": "Was ist die Table-API?",
"description": "Verstehen Sie Azure Cosmos DB for Table und seine Funktionen."
},
"migrate": {
"title": "Ihre Daten migrieren",
"description": "Erfahren Sie, wie Sie Ihre Daten migrieren."
},
"faq": {
"title": "Häufig gestellte Fragen zu Azure Cosmos DB for Table",
"description": "Häufige Fragen zu Azure Cosmos DB for Table"
}
}
},
"learningResources": {
"shortcuts": {
"title": "Tastenkombinationen im Datenexplorer",
"description": "Lernen Sie Tastenkombinationen zur Navigation im Datenexplorer."
},
"liveTv": {
"title": "Grundlagen erlernen",
"description": "Sehen Sie sich die Einführung und Anleitungsvideos der Azure Cosmos DB Live TV-Sendung an."
},
"sql": {
"sdk": {
"title": "Erste Schritte mit einem SDK",
"description": "Mehr über das Azure Cosmos DB SDK erfahren."
},
"migrate": {
"title": "Ihre Daten migrieren",
"description": "Migrieren Sie Daten mit Azure-Diensten und Open-Source-Lösungen."
}
},
"mongo": {
"nodejs": {
"title": "App mit Node.js erstellen",
"description": "Node.js-App erstellen."
},
"gettingStarted": {
"title": "Leitfaden für erste Schritte",
"description": "Lernen Sie die Grundlagen für den Einstieg kennen."
}
},
"cassandra": {
"createContainer": {
"title": "Container erstellen",
"description": "Erfahren Sie mehr über die Optionen zum Erstellen eines Containers."
},
"throughput": {
"title": "Durchsatz bereitstellen",
"description": "Erfahren Sie, wie Sie den Durchsatz konfigurieren."
}
},
"gremlin": {
"getStarted": {
"title": "Erste Schritte ",
"description": "Erstellen, Abfragen und Durchlaufen mithilfe der Gremlin-Konsole"
},
"importData": {
"title": "Graphdaten importieren",
"description": "Massendatenaufnahme mit BulkExecutor erlernen"
}
},
"tables": {
"dotnet": {
"title": ".NET-App erstellen",
"description": "So greifen Sie mit einer .NET-App auf Azure Cosmos DB for Table zu."
},
"java": {
"title": "Java-App erstellen",
"description": "Erstellen Sie eine Azure Cosmos DB for Table-App mit dem Java SDK. "
}
}
},
"nextStepItems": {
"postgres": {
"dataModeling": "Datenmodellierung",
"distributionColumn": "So wählen Sie eine Verteilungsspalte aus.",
"buildApps": "Apps mit Python/Java/Django erstellen"
},
"vcoreMongo": {
"migrateData": "Daten migrieren",
"vectorSearch": "KI-Apps mit Vektorsuche erstellen",
"buildApps": "Apps mit Node.js erstellen"
}
},
"learnMoreItems": {
"postgres": {
"performanceTuning": "Leistungsoptimierung",
"diagnosticQueries": "Nützliche Diagnoseabfragen",
"sqlReference": "Referenz für verteiltes SQL"
},
"vcoreMongo": {
"vectorSearch": "Vektorsuche",
"textIndexing": "Textindizierung",
"troubleshoot": "Häufige Probleme beheben"
}
},
"fabric": {
"buildTitle": "Ihre Datenbank erstellen",
"useTitle": "Ihre Datenbank nutzen",
"newContainer": {
"title": "Neuer Container",
"description": "Zielcontainer zum Speichern Ihrer Daten erstellen."
},
"sampleData": {
"title": "Beispieldaten",
"description": "Beispieldaten in Ihrer Datenbank laden"
},
"sampleVectorData": {
"title": "Beispielvektordaten",
"description": "Beispielvektordaten mit text-embedding-ada-002 laden"
},
"appDevelopment": {
"title": "App-Entwicklung",
"description": "Starten Sie hier, um ein SDK zum Erstellen Ihrer Apps zu verwenden."
},
"sampleGallery": {
"title": "Beispielgalerie",
"description": "End-to-End-Beispiele aus der Praxis ansehen"
}
},
"sampleDataDialog": {
"title": "Beispieldaten",
"startButton": "Starten",
"createPrompt": "Erstellen Sie einen Container „{{containerName}}“ und importieren Sie Beispieldaten. Dies kann einige Minuten dauern.",
"creatingContainer": "Container „{{containerName}}“ wird erstellt...",
"importingData": "Daten werden in „{{containerName}}“ importiert...",
"success": "„{{containerName}}“ wurde erfolgreich mit Beispieldaten erstellt.",
"errorContainerExists": "Der Container „{{containerName}}“ in der Datenbank „{{databaseName}}“ existiert bereits. Bitte löschen Sie ihn und versuchen Sie es erneut.",
"errorCreateContainer": "Fehler beim Erstellen des Containers: {{error}}",
"errorImportData": "Fehler beim Importieren von Daten: {{error}}"
}
}
}

View File

@@ -0,0 +1,938 @@
{
"common": {
"ok": "OK",
"cancel": "Cancel",
"close": "Close",
"save": "Save",
"delete": "Delete",
"update": "Update",
"discard": "Discard",
"execute": "Execute",
"loading": "Loading",
"loadingEllipsis": "Loading...",
"next": "Next",
"previous": "Previous",
"yes": "Yes",
"no": "No",
"result": "Result",
"learnMore": "Learn more",
"getStarted": "Get started",
"retry": "Retry",
"apply": "Apply",
"refresh": "Refresh",
"copy": "Copy",
"create": "Create",
"confirm": "Confirm",
"open": "Open",
"rename": "Rename",
"download": "Download",
"upload": "Upload",
"connect": "Connect",
"remove": "Remove",
"load": "Load",
"publish": "Publish",
"browse": "Browse",
"increaseValueBy1": "Increase value by 1",
"decreaseValueBy1": "Decrease value by 1"
},
"splashScreen": {
"title": {
"default": "Welcome to Azure Cosmos DB",
"postgres": "Welcome to Azure Cosmos DB for PostgreSQL",
"vcoreMongo": "Welcome to Azure DocumentDB (with MongoDB compatibility)"
},
"subtitle": {
"default": "Globally distributed, multi-model database service for any scale",
"getStarted": "Get started with our sample datasets, documentation, and additional tools."
},
"quickStart": {
"title": "Launch quick start",
"description": "Launch a quick start tutorial to get started with sample data"
},
"newCollection": {
"title": "New {{collectionName}}",
"description": "Create a new container for storage and throughput"
},
"samplesGallery": {
"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"
},
"connectCard": {
"title": "Connect",
"description": "Prefer using your own choice of tooling? Find the connection string you need to connect",
"pgAdmin": {
"title": "Connect with pgAdmin",
"description": "Prefer pgAdmin? Find your connection strings here"
},
"vsCode": {
"title": "Connect with VS Code",
"description": "Query and Manage your MongoDB and DocumentDB clusters in Visual Studio Code"
}
},
"shell": {
"postgres": {
"title": "PostgreSQL Shell",
"description": "Create table and interact with data using PostgreSQL's shell interface"
},
"vcoreMongo": {
"title": "Mongo Shell",
"description": "Create a collection and interact with data using MongoDB's shell interface"
}
},
"teachingBubble": {
"newToPostgres": {
"headline": "New to Cosmos DB PGSQL?",
"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."
},
"resetPassword": {
"headline": "Create your password",
"body": "If you haven't changed your password yet, change it now."
},
"coachMark": {
"headline": "Start with sample {{collectionName}}",
"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"
}
},
"sections": {
"recents": "Recents",
"clearRecents": "Clear Recents",
"top3": "Top 3 things you need to know",
"learningResources": "Learning Resources",
"nextSteps": "Next steps",
"tipsAndLearnMore": "Tips & learn more",
"notebook": "Notebook",
"needHelp": "Need help?"
},
"top3Items": {
"sql": {
"advancedModeling": {
"title": "Advanced Modeling Patterns",
"description": "Learn advanced strategies to optimize your database."
},
"partitioning": {
"title": "Partitioning Best Practices",
"description": "Learn to apply data model and partitioning strategies."
},
"resourcePlanning": {
"title": "Plan Your Resource Requirements",
"description": "Get to know the different configuration choices."
}
},
"mongo": {
"whatIsMongo": {
"title": "What is the MongoDB API?",
"description": "Understand Azure Cosmos DB for MongoDB and its features."
},
"features": {
"title": "Features and Syntax",
"description": "Discover the advantages and features"
},
"migrate": {
"title": "Migrate Your Data",
"description": "Pre-migration steps for moving data"
}
},
"cassandra": {
"buildJavaApp": {
"title": "Build a Java App",
"description": "Create a Java app using an SDK."
},
"partitioning": {
"title": "Partitioning Best Practices",
"description": "Learn how partitioning works."
},
"requestUnits": {
"title": "Request Units (RUs)",
"description": "Understand RU charges."
}
},
"gremlin": {
"dataModeling": {
"title": "Data Modeling",
"description": "Graph data modeling recommendations"
},
"partitioning": {
"title": "Partitioning Best Practices",
"description": "Learn how partitioning works"
},
"queryData": {
"title": "Query Data",
"description": "Querying data with Gremlin"
}
},
"tables": {
"whatIsTable": {
"title": "What is the Table API?",
"description": "Understand Azure Cosmos DB for Table and its features"
},
"migrate": {
"title": "Migrate your data",
"description": "Learn how to migrate your data"
},
"faq": {
"title": "Azure Cosmos DB for Table FAQs",
"description": "Common questions about Azure Cosmos DB for Table"
}
}
},
"learningResources": {
"shortcuts": {
"title": "Data Explorer keyboard shortcuts",
"description": "Learn keyboard shortcuts to navigate Data Explorer."
},
"liveTv": {
"title": "Learn the Fundamentals",
"description": "Watch Azure Cosmos DB Live TV show introductory and how to videos."
},
"sql": {
"sdk": {
"title": "Get Started using an SDK",
"description": "Learn about the Azure Cosmos DB SDK."
},
"migrate": {
"title": "Migrate Your Data",
"description": "Migrate data using Azure services and open-source solutions."
}
},
"mongo": {
"nodejs": {
"title": "Build an app with Node.js",
"description": "Create a Node.js app."
},
"gettingStarted": {
"title": "Getting Started Guide",
"description": "Learn the basics to get started."
}
},
"cassandra": {
"createContainer": {
"title": "Create a Container",
"description": "Get to know the create a container options."
},
"throughput": {
"title": "Provision Throughput",
"description": "Learn how to configure throughput."
}
},
"gremlin": {
"getStarted": {
"title": "Get Started ",
"description": "Create, query, and traverse using the Gremlin console"
},
"importData": {
"title": "Import Graph Data",
"description": "Learn Bulk ingestion data using BulkExecutor"
}
},
"tables": {
"dotnet": {
"title": "Build a .NET App",
"description": "How to access Azure Cosmos DB for Table from a .NET app."
},
"java": {
"title": "Build a Java App",
"description": "Create a Azure Cosmos DB for Table app with Java SDK "
}
}
},
"nextStepItems": {
"postgres": {
"dataModeling": "Data Modeling",
"distributionColumn": "How to choose a Distribution Column",
"buildApps": "Build Apps with Python/Java/Django"
},
"vcoreMongo": {
"migrateData": "Migrate Data",
"vectorSearch": "Build AI apps with Vector Search",
"buildApps": "Build Apps with Nodejs"
}
},
"learnMoreItems": {
"postgres": {
"performanceTuning": "Performance Tuning",
"diagnosticQueries": "Useful Diagnostic Queries",
"sqlReference": "Distributed SQL Reference"
},
"vcoreMongo": {
"vectorSearch": "Vector Search",
"textIndexing": "Text Indexing",
"troubleshoot": "Troubleshoot common issues"
}
},
"fabric": {
"buildTitle": "Build your database",
"useTitle": "Use your database",
"newContainer": {
"title": "New container",
"description": "Create a destination container to store your data"
},
"sampleData": {
"title": "Sample Data",
"description": "Load sample data in your database"
},
"sampleVectorData": {
"title": "Sample Vector Data",
"description": "Load sample vector data with text-embedding-ada-002"
},
"appDevelopment": {
"title": "App development",
"description": "Start here to use an SDK to build your apps"
},
"sampleGallery": {
"title": "Sample Gallery",
"description": "Get real-world end-to-end samples"
}
},
"sampleDataDialog": {
"title": "Sample Data",
"startButton": "Start",
"createPrompt": "Create a container \"{{containerName}}\" and import sample data into it. This may take a few minutes.",
"creatingContainer": "Creating container \"{{containerName}}\"...",
"importingData": "Importing data into \"{{containerName}}\"...",
"success": "Successfully created \"{{containerName}}\" with sample data.",
"errorContainerExists": "The container \"{{containerName}}\" in database \"{{databaseName}}\" already exists. Please delete it and retry.",
"errorCreateContainer": "Failed to create container: {{error}}",
"errorImportData": "Failed to import data: {{error}}"
}
},
"contextMenu": {
"newContainer": "New {{containerName}}",
"restoreContainer": "Restore {{containerName}}",
"deleteDatabase": "Delete {{databaseName}}",
"deleteContainer": "Delete {{containerName}}",
"newSqlQuery": "New SQL Query",
"newQuery": "New Query",
"openMongoShell": "Open Mongo Shell",
"newShell": "New Shell",
"openCassandraShell": "Open Cassandra Shell",
"newStoredProcedure": "New Stored Procedure",
"newUdf": "New UDF",
"newTrigger": "New Trigger",
"deleteStoredProcedure": "Delete Stored Procedure",
"deleteTrigger": "Delete Trigger",
"deleteUdf": "Delete User Defined Function"
},
"tabs": {
"documents": {
"newItem": "New Item",
"newDocument": "New Document",
"uploadItem": "Upload Item",
"applyFilter": "Apply Filter",
"unsavedChanges": "Unsaved changes",
"unsavedChangesMessage": "Your unsaved changes will be lost. Do you want to continue?",
"createDocumentFailed": "Create document failed",
"updateDocumentFailed": "Update document failed",
"documentDeleted": "Document successfully deleted.",
"deleteDocumentDialogTitle": "Delete document",
"deleteDocumentsDialogTitle": "Delete documents",
"throttlingError": "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.",
"deleteFailed": "Deleting document(s) failed ({{error}})",
"missingShardProperty": "The document is lacking the shard property: {{partitionKeyProperty}}",
"refreshGridFailed": "Refresh documents grid failed",
"confirmDelete": "Are you sure you want to delete {{documentName}}?",
"confirmDeleteTitle": "Confirm delete",
"selectedItems": "the selected {{count}} items",
"selectedItem": "the selected item",
"selectedDocuments": "the selected {{count}} documents",
"selectedDocument": "the selected document",
"deleteDocumentFailedLog": "Failed to delete document {{documentId}} with status code {{statusCode}}",
"deleteSuccessLog": "Successfully deleted {{count}} document(s)",
"deleteThrottledLog": "Failed to delete {{count}} document(s) due to \"Request too large\" (429) error. Retrying...",
"missingShardKeyLog": "Failed to save new document: Document shard key not defined",
"filterTooltip": "Type a query predicate or choose one from the list.",
"loadMore": "Load more",
"documentEditor": "Document editor",
"savedFilters": "Saved filters",
"defaultFilters": "Default filters",
"abort": "Abort",
"deletingDocuments": "Deleting {{count}} document(s)",
"deletedDocumentsSuccess": "Successfully deleted {{count}} document(s).",
"deleteAborted": "Deleting document(s) was aborted.",
"failedToDeleteDocuments": "Failed to delete {{count}} document(s).",
"requestTooLargeBase": "Some delete requests failed due to a \"Request too large\" exception (429)",
"retriedSuccessfully": "but were successfully retried.",
"retryingNow": "Retrying now.",
"increaseThroughputTip": "To prevent this in the future, consider increasing the throughput on your container or database.",
"numberOfSelectedDocuments": "Number of selected documents: {{count}}",
"mongoFilterPlaceholder": "Type a query predicate (e.g., {\"id\":\"foo\"}), or choose one from the drop down list, or leave empty to query all documents.",
"sqlFilterPlaceholder": "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.",
"error": "Error",
"warning": "Warning"
},
"query": {
"executeQuery": "Execute Query",
"executeSelection": "Execute Selection",
"saveQuery": "Save Query",
"downloadQuery": "Download Query",
"cancelQuery": "Cancel query",
"openSavedQueries": "Open Saved Queries",
"vertical": "Vertical",
"horizontal": "Horizontal",
"view": "View",
"editingQuery": "Editing Query"
},
"storedProcedure": {
"id": "Stored Procedure Id",
"idPlaceholder": "Enter the new stored procedure id",
"idAriaLabel": "Stored procedure id",
"body": "Stored Procedure Body",
"bodyAriaLabel": "Stored procedure body",
"successfulExecution": "Successful execution of stored procedure",
"resultAriaLabel": "Execute stored procedure result",
"logsAriaLabel": "Execute stored procedure logs",
"errors": "Errors:",
"errorDetailsAriaLabel": "Error details link",
"moreDetails": "More details",
"consoleLogTab": "console.log"
},
"trigger": {
"id": "Trigger Id",
"idPlaceholder": "Enter the new trigger id",
"type": "Trigger Type",
"operation": "Trigger Operation",
"body": "Trigger Body",
"bodyAriaLabel": "Trigger body",
"pre": "Pre",
"post": "Post",
"all": "All",
"operationCreate": "Create",
"operationDelete": "Delete",
"operationReplace": "Replace"
},
"udf": {
"id": "User Defined Function Id",
"idPlaceholder": "Enter the new user defined function id",
"body": "User Defined Function Body",
"bodyAriaLabel": "User defined function body"
},
"conflicts": {
"unsavedChanges": "Unsaved changes",
"changesWillBeLost": "Changes will be lost. Do you want to continue?",
"resolveConflictFailed": "Resolve conflict failed",
"deleteConflictFailed": "Delete conflict failed",
"refreshGridFailed": "Refresh documents grid failed"
},
"mongoShell": {
"title": "Mongo Shell"
}
},
"panes": {
"deleteDatabase": {
"panelTitle": "Delete {{databaseName}}",
"warningMessage": "Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
"confirmPrompt": "Confirm by typing the {{databaseName}} id (name)",
"inputMismatch": "Input {{databaseName}} name \"{{input}}\" does not match the selected {{databaseName}} \"{{selectedId}}\"",
"feedbackTitle": "Help us improve Azure Cosmos DB!",
"feedbackReason": "What is the reason why you are deleting this {{databaseName}}?"
},
"deleteCollection": {
"panelTitle": "Delete {{collectionName}}",
"confirmPrompt": "Confirm by typing the {{collectionName}} id",
"inputMismatch": "Input id {{input}} does not match the selected {{selectedId}}",
"feedbackTitle": "Help us improve Azure Cosmos DB!",
"feedbackReason": "What is the reason why you are deleting this {{collectionName}}?"
},
"addDatabase": {
"databaseLabel": "Database {{suffix}}",
"databaseIdLabel": "Database id",
"keyspaceIdLabel": "Keyspace id",
"databaseIdPlaceholder": "Type a new {{databaseLabel}} id",
"databaseTooltip": "A {{databaseLabel}} is a logical container of one or more {{collectionsLabel}}",
"shareThroughput": "Share throughput across {{collectionsLabel}}",
"shareThroughputTooltip": "Provisioned throughput at the {{databaseLabel}} level will be shared across all {{collectionsLabel}} within the {{databaseLabel}}.",
"greaterThanError": "Please enter a value greater than {{minValue}} for autopilot throughput",
"acknowledgeSpendError": "Please acknowledge the estimated {{period}} spend."
},
"addCollection": {
"createNew": "Create new",
"useExisting": "Use existing",
"databaseTooltip": "A database is analogous to a namespace. It is the unit of management for a set of {{collectionName}}.",
"shareThroughput": "Share throughput across {{collectionName}}",
"shareThroughputTooltip": "Throughput configured at the database level will be shared across all {{collectionName}} within the database.",
"collectionIdLabel": "{{collectionName}} id",
"collectionIdTooltip": "Unique identifier for the {{collectionName}} and used for id-based routing through REST and all SDKs.",
"collectionIdPlaceholder": "e.g., {{collectionName}}1",
"collectionIdAriaLabel": "{{collectionName}} id, Example {{collectionName}}1",
"existingDatabaseAriaLabel": "Choose existing {{databaseName}} id",
"existingDatabasePlaceholder": "Choose existing {{databaseName}} id",
"indexing": "Indexing",
"turnOnIndexing": "Turn on indexing",
"automatic": "Automatic",
"turnOffIndexing": "Turn off indexing",
"off": "Off",
"sharding": "Sharding",
"shardingTooltip": "Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data.",
"unsharded": "Unsharded",
"unshardedLabel": "Unsharded (20GB limit)",
"sharded": "Sharded",
"addPartitionKey": "Add hierarchical partition key",
"hierarchicalPartitionKeyInfo": "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.",
"provisionDedicatedThroughput": "Provision dedicated throughput for this {{collectionName}}",
"provisionDedicatedThroughputTooltip": "You can optionally provision dedicated throughput for a {{collectionName}} within a database that has throughput provisioned. This dedicated throughput amount will not be shared with other {{collectionNamePlural}} 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.",
"uniqueKeysPlaceholderMongo": "Comma separated paths e.g. firstName,address.zipCode",
"uniqueKeysPlaceholderSql": "Comma separated paths e.g. /firstName,/address/zipCode",
"addUniqueKey": "Add unique key",
"enableAnalyticalStore": "Enable analytical store",
"disableAnalyticalStore": "Disable analytical store",
"on": "On",
"analyticalStoreSynapseLinkRequired": "Azure Synapse Link is required for creating an analytical store {{collectionName}}. Enable Synapse Link for this Cosmos DB account.",
"enable": "Enable",
"containerVectorPolicy": "Container Vector Policy",
"containerFullTextSearchPolicy": "Container Full Text Search Policy",
"advanced": "Advanced",
"mongoIndexingTooltip": "The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development.",
"createWildcardIndex": "Create a Wildcard Index on all fields",
"legacySdkCheckbox": "My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)",
"legacySdkInfo": "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.",
"indexingOnInfo": "All properties in your documents will be indexed by default for flexible and efficient queries.",
"indexingOffInfo": "Indexing will be turned off. Recommended if you don't need to run queries or only have key value operations.",
"indexingOffWarning": "By creating this container with indexing turned off, you will not be able to make any indexing policy changes. Indexing changes are only allowed on a container with a indexing policy.",
"acknowledgeSpendErrorMonthly": "Please acknowledge the estimated monthly spend.",
"acknowledgeSpendErrorDaily": "Please acknowledge the estimated daily spend.",
"unshardedMaxRuError": "Unsharded collections support up to 10,000 RUs",
"acknowledgeShareThroughputError": "Please acknowledge the estimated cost of this dedicated throughput.",
"vectorPolicyError": "Please fix errors in container vector policy",
"fullTextSearchPolicyError": "Please fix errors in container full text search policy",
"addingSampleDataSet": "Adding sample data set"
},
"addCollectionUtility": {
"shardKeyTooltip": "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It's critical to choose a field that will evenly distribute your data.",
"partitionKeyTooltip": "The {{partitionKeyName}} 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.",
"partitionKeyTooltipSqlSuffix": " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.",
"shardKeyLabel": "Shard key",
"partitionKeyLabel": "Partition key",
"shardKeyPlaceholder": "e.g., categoryId",
"partitionKeyPlaceholderDefault": "e.g., /address",
"partitionKeyPlaceholderFirst": "Required - first partition key e.g., /TenantId",
"partitionKeyPlaceholderSecond": "second partition key e.g., /UserId",
"partitionKeyPlaceholderThird": "third partition key e.g., /SessionId",
"partitionKeyPlaceholderGraph": "e.g., /address/zipCode",
"uniqueKeysTooltip": "Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key.",
"uniqueKeysLabel": "Unique keys",
"analyticalStoreLabel": "Analytical Store",
"analyticalStoreTooltip": "Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.",
"analyticalStoreDescription": "Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.",
"vectorPolicyTooltip": "Describe any properties in your data that contain vectors, so that they can be made available for similarity queries."
},
"settings": {
"pageOptions": "Page Options",
"pageOptionsDescription": "Choose Custom to specify a fixed amount of query results to show, or choose Unlimited to show as many query results per page.",
"queryResultsPerPage": "Query results per page",
"queryResultsPerPageTooltip": "Enter the number of query results that should be shown per page.",
"customQueryItemsPerPage": "Custom query items per page",
"custom": "Custom",
"unlimited": "Unlimited",
"entraIdRbac": "Enable Entra ID RBAC",
"entraIdRbacDescription": "Choose Automatic to enable Entra ID RBAC automatically. True/False to force enable/disable Entra ID RBAC.",
"true": "True",
"false": "False",
"regionSelection": "Region Selection",
"regionSelectionDescription": "Changes region the Cosmos Client uses to access account.",
"selectRegion": "Select Region",
"selectRegionTooltip": "Changes the account endpoint used to perform client operations.",
"globalDefault": "Global (Default)",
"readWrite": "(Read/Write)",
"read": "(Read)",
"queryTimeout": "Query Timeout",
"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.",
"enableQueryTimeout": "Enable query timeout",
"queryTimeoutMs": "Query timeout (ms)",
"automaticallyCancelQuery": "Automatically cancel query after timeout",
"ruLimit": "RU Limit",
"ruLimitDescription": "If a query exceeds a configured RU limit, the query will be aborted.",
"enableRuLimit": "Enable RU limit",
"ruLimitLabel": "RU Limit (RU)",
"defaultQueryResults": "Default Query Results View",
"defaultQueryResultsDescription": "Select the default view to use when displaying query results.",
"retrySettings": "Retry Settings",
"retrySettingsDescription": "Retry policy associated with throttled requests during CosmosDB queries.",
"maxRetryAttempts": "Max retry attempts",
"maxRetryAttemptsTooltip": "Max number of retries to be performed for a request. Default value 9.",
"fixedRetryInterval": "Fixed retry interval (ms)",
"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.",
"maxWaitTime": "Max wait time (s)",
"maxWaitTimeTooltip": "Max wait time in seconds to wait for a request while the retries are happening. Default value 30 seconds.",
"enableContainerPagination": "Enable container pagination",
"enableContainerPaginationDescription": "Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.",
"enableCrossPartitionQuery": "Enable cross-partition query",
"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.",
"maxDegreeOfParallelism": "Max degree of parallelism",
"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.",
"maxDegreeOfParallelismQuery": "Query up to the max degree of parallelism.",
"priorityLevel": "Priority Level",
"priorityLevelDescription": "Sets the priority level for data-plane requests from Data Explorer when using Priority-Based Execution. If \"None\" is selected, Data Explorer will not specify priority level, and the server-side default priority level will be used.",
"displayGremlinQueryResults": "Display Gremlin query results as:",
"displayGremlinQueryResultsDescription": "Select Graph to automatically visualize the query results as a Graph or JSON to display the results as JSON.",
"graph": "Graph",
"json": "JSON",
"graphAutoVisualization": "Graph Auto-visualization",
"enableSampleDatabase": "Enable sample database",
"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.",
"enableSampleDbAriaLabel": "Enable sample db for query exploration",
"guidRepresentation": "Guid Representation",
"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.",
"advancedSettings": "Advanced Settings",
"ignorePartitionKey": "Ignore partition key on document update",
"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.",
"clearHistory": "Clear History",
"clearHistoryConfirm": "Are you sure you want to proceed?",
"clearHistoryDescription": "This action will clear the all customizations for this account in this browser, including:",
"clearHistoryTabLayout": "Reset your customized tab layout, including the splitter positions",
"clearHistoryTableColumns": "Erase your table column preferences, including any custom columns",
"clearHistoryFilters": "Clear your filter history",
"clearHistoryRegion": "Reset region selection to global",
"increaseValueBy1000": "Increase value by 1000",
"decreaseValueBy1000": "Decrease value by 1000",
"none": "None",
"low": "Low",
"high": "High",
"automatic": "Automatic",
"enhancedQueryControl": "Enhanced query control",
"enableQueryControl": "Enable query control",
"explorerVersion": "Explorer Version",
"accountId": "Account ID",
"sessionId": "Session ID",
"popupsDisabledError": "We were unable to establish authorization for this account, due to pop-ups being disabled in the browser.\nPlease enable pop-ups for this site and click on \"Login for Entra ID\" button",
"failedToAcquireTokenError": "Failed to acquire authorization token automatically. Please click on \"Login for Entra ID\" button to enable Entra ID RBAC operations"
},
"saveQuery": {
"panelTitle": "Save Query",
"setupCostMessage": "For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called \u201c{{databaseName}}\u201d. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.",
"completeSetup": "Complete setup",
"noQueryNameError": "No query name specified",
"invalidQueryContentError": "Invalid query content specified",
"failedToSaveQueryError": "Failed to save query {{queryName}}",
"failedToSetupContainerError": "Failed to setup a container for saved queries",
"accountNotSetupError": "Failed to save query: account not setup to save queries",
"name": "Name"
},
"loadQuery": {
"noFileSpecifiedError": "No file specified",
"failedToLoadQueryError": "Failed to load query",
"failedToLoadQueryFromFileError": "Failed to load query from file {{fileName}}",
"selectFilesToOpen": "Select a query document",
"browseFiles": "Browse"
},
"executeStoredProcedure": {
"enterInputParameters": "Enter input parameters (if any)",
"key": "Key",
"param": "Param",
"partitionKeyValue": "Partition key value",
"value": "Value",
"addNewParam": "Add New Param",
"addParam": "Add param",
"deleteParam": "Delete param",
"invalidParamError": "Invalid param specified: {{invalidParam}}",
"invalidParamConsoleError": "Invalid param specified: {{invalidParam}} is not a valid literal value",
"stringType": "String",
"customType": "Custom"
},
"uploadItems": {
"noFilesSpecifiedError": "No files were specified. Please input at least one file.",
"selectJsonFiles": "Select JSON Files",
"selectJsonFilesTooltip": "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.",
"fileNameColumn": "FILE NAME",
"statusColumn": "STATUS",
"uploadStatus": "{{numSucceeded}} created, {{numThrottled}} throttled, {{numFailed}} errors",
"uploadedFiles": "Uploaded files"
},
"copyNotebook": {
"copyFailedError": "Failed to copy {{name}} to {{destination}}",
"uploadFailedError": "Failed to upload {{name}}",
"location": "Location",
"locationAriaLabel": "Location",
"selectLocation": "Select a notebook location to copy",
"name": "Name"
},
"publishNotebook": {
"publishFailedError": "Failed to publish {{notebookName}} to gallery",
"publishDescription": "When published, this notebook will appear in the Azure Cosmos DB notebooks public gallery. Make sure you have removed any sensitive data or output before publishing.",
"publishPrompt": "Would you like to publish and share \"{{name}}\" to the gallery?",
"coverImage": "Cover image",
"coverImageUrl": "Cover image url",
"name": "Name",
"description": "Description",
"tags": "Tags",
"tagsPlaceholder": "Optional tag 1, Optional tag 2",
"preview": "Preview",
"urlType": "URL",
"customImage": "Custom Image",
"takeScreenshot": "Take Screenshot",
"useFirstDisplayOutput": "Use First Display Output",
"failedToCaptureOutput": "Failed to capture first output",
"outputDoesNotExist": "Output does not exist for any of the cells.",
"failedToConvertError": "Failed to convert {{fileName}} to base64 format",
"failedToUploadError": "Failed to upload {{fileName}}"
},
"changePartitionKey": {
"failedToStartError": "Failed to start data transfer job",
"suboptimalPartitionKeyError": "Warning: The system has detected that your collection may be using a suboptimal partition key",
"description": "When changing a container\u2019s partition key, you will need to create a destination container with the correct partition key. You may also select an existing destination container.",
"sourceContainerId": "Source {{collectionName}} id",
"destinationContainerId": "Destination {{collectionName}} id",
"collectionIdTooltip": "Unique identifier for the {{collectionName}} and used for id-based routing through REST and all SDKs.",
"collectionIdPlaceholder": "e.g., {{collectionName}}1",
"collectionIdAriaLabel": "{{collectionName}} id, Example {{collectionName}}1",
"existingContainers": "Existing Containers",
"partitionKeyWarning": "The destination container must not already exist. Data Explorer will create a new destination container for you."
},
"cassandraAddCollection": {
"keyspaceLabel": "Keyspace name",
"keyspaceTooltip": "Select an existing keyspace or enter a new keyspace id.",
"tableIdLabel": "Enter CQL command to create the table.",
"enterTableId": "Enter table Id",
"tableSchemaAriaLabel": "Table schema",
"provisionDedicatedThroughput": "Provision dedicated throughput for this table",
"provisionDedicatedThroughputTooltip": "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."
},
"tables": {
"addProperty": "Add Property",
"addRow": "Add Row",
"addEntity": "Add Entity",
"back": "back",
"nullFieldsWarning": "Warning: Null fields will not be displayed for editing.",
"propertyEmptyError": "{{property}} cannot be empty. Please input a value for {{property}}",
"whitespaceError": "{{property}} cannot have whitespace. Please input a value for {{property}} without whitespace",
"propertyTypeEmptyError": "Property type cannot be empty. Please select a type from the dropdown for property {{property}}"
},
"tableQuerySelect": {
"selectColumns": "Select the columns that you want to query.",
"availableColumns": "Available Columns"
},
"tableColumnSelection": {
"selectColumns": "Select which columns to display in your view of items in your container.",
"searchFields": "Search fields",
"reset": "Reset",
"partitionKeySuffix": " (partition key)"
},
"newVertex": {
"addProperty": "Add Property"
},
"addGlobalSecondaryIndex": {
"globalSecondaryIndexId": "Global secondary index container id",
"globalSecondaryIndexIdPlaceholder": "e.g., indexbyEmailId",
"projectionQuery": "Projection query",
"projectionQueryPlaceholder": "SELECT c.email, c.accountId FROM c",
"projectionQueryTooltip": "Learn more about defining global secondary indexes.",
"disabledTitle": "A global secondary index is already being created. Please wait for it to complete before creating another one."
},
"stringInput": {
"inputMismatchError": "Input {{input}} does not match the selected {{selectedId}}"
},
"panelInfo": {
"information": "Information",
"moreDetails": "More details"
}
},
"controls": {
"settings": {
"tabTitles": {
"scale": "Scale",
"conflictResolution": "Conflict Resolution",
"settings": "Settings",
"indexingPolicy": "Indexing Policy",
"partitionKeys": "Partition Keys",
"partitionKeysPreview": "Partition Keys (preview)",
"computedProperties": "Computed Properties",
"containerPolicies": "Container Policies",
"throughputBuckets": "Throughput Buckets",
"globalSecondaryIndexPreview": "Global Secondary Index (Preview)",
"maskingPolicyPreview": "Masking Policy (preview)"
},
"mongoNotifications": {
"selectTypeWarning": "Please select a type for each index.",
"enterFieldNameError": "Please enter a field name.",
"wildcardPathError": "Wildcard path is not present in the field name. Use a pattern like "
},
"partitionKey": {
"shardKey": "Shard key",
"partitionKey": "Partition key",
"shardKeyTooltip": "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It's critical to choose a field that will evenly distribute your data.",
"partitionKeyTooltip": "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.",
"sqlPartitionKeyTooltipSuffix": " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.",
"partitionKeySubtext": "For small workloads, the item ID is a suitable choice for the partition key.",
"mongoPlaceholder": "e.g., categoryId",
"gremlinPlaceholder": "e.g., /address",
"sqlFirstPartitionKey": "Required - first partition key e.g., /TenantId",
"sqlSecondPartitionKey": "second partition key e.g., /UserId",
"sqlThirdPartitionKey": "third partition key e.g., /SessionId",
"defaultPlaceholder": "e.g., /address/zipCode"
},
"costEstimate": {
"title": "Cost estimate*",
"howWeCalculate": "How we calculate this",
"updatedCostPerMonth": "Updated cost per month",
"currentCostPerMonth": "Current cost per month",
"perRu": "/RU"
},
"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.",
"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,",
"ttlWarningLinkText": "Time to Live (TTL) in Azure Cosmos DB",
"unsavedIndexingPolicy": "indexing policy",
"unsavedDataMaskingPolicy": "data masking policy",
"unsavedComputedProperties": "computed properties",
"unsavedEditorWarningPrefix": "You have not saved the latest changes made to your",
"unsavedEditorWarningSuffix": ". Please click save to confirm the changes.",
"updateDelayedApplyWarning": "You are about to request an increase in throughput beyond the pre-allocated capacity. This operation will take some time to complete.",
"scalingUpDelayMessage": "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.",
"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:",
"instantScaleOption": "You can instantly scale up to {{instantMaximumThroughput}} RU/s.",
"asyncScaleOption": "You can asynchronously scale up to any value under {{maximumThroughput}} RU/s in 4-6 hours.",
"quotaMaxOption": "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.",
"belowMinimumMessage": "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.",
"saveThroughputWarning": "Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below before saving your changes",
"currentAutoscaleThroughput": "Current autoscale throughput:",
"targetAutoscaleThroughput": "Target autoscale throughput:",
"currentManualThroughput": "Current manual throughput:",
"targetManualThroughput": "Target manual 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.",
"databaseLabel": "Database:",
"containerLabel": "Container:",
"applyShortDelayMessage": "A request to increase the throughput is currently in progress. This operation will take some time to complete.",
"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.",
"throughputCapError": "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 {{newTotalThroughput}} RU/s. Change total throughput limit in cost management.",
"throughputIncrementError": "Throughput value must be in increments of 1000"
},
"conflictResolution": {
"lwwDefault": "Last Write Wins (default)",
"customMergeProcedure": "Merge Procedure (custom)",
"mode": "Mode",
"conflictResolverProperty": "Conflict Resolver Property",
"storedProcedure": "Stored procedure",
"lwwTooltip": "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.",
"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",
"customTooltipConflictsFeed": " conflicts feed",
"customTooltipSuffix": ". You can update/re-register the stored procedure at any time."
},
"changeFeed": {
"label": "Change feed log retention policy",
"tooltip": "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."
},
"mongoIndexing": {
"disclaimer": "For queries that filter on multiple properties, create multiple single field indexes instead of a compound index.",
"disclaimerCompoundIndexesLink": " Compound indexes ",
"disclaimerSuffix": "are only used for sorting query results. If you need to add a compound index, you can create one using the Mongo shell.",
"compoundNotSupported": "Collections with compound indexes are not yet supported in the indexing editor. To modify indexing policy for this collection, use the Mongo Shell.",
"aadError": "To use the indexing policy editor, please login to the",
"aadErrorLink": "azure portal.",
"refreshingProgress": "Refreshing index transformation progress",
"canMakeMoreChangesZero": "You can make more indexing changes once the current index transformation is complete. ",
"refreshToCheck": "Refresh to check if it has completed.",
"canMakeMoreChangesProgress": "You can make more indexing changes once the current index transformation has completed. It is {{progress}}% complete. ",
"refreshToCheckProgress": "Refresh to check the progress.",
"definitionColumn": "Definition",
"typeColumn": "Type",
"dropIndexColumn": "Drop Index",
"addIndexBackColumn": "Add index back",
"deleteIndexButton": "Delete index Button",
"addBackIndexButton": "Add back Index Button",
"currentIndexes": "Current index(es)",
"indexesToBeDropped": "Index(es) to be dropped",
"indexFieldName": "Index Field Name",
"indexType": "Index Type",
"selectIndexType": "Select an index type",
"undoButton": "Undo Button"
},
"subSettings": {
"timeToLive": "Time to Live",
"ttlOff": "Off",
"ttlOnNoDefault": "On (no default)",
"ttlOn": "On",
"seconds": "second(s)",
"timeToLiveInSeconds": "Time to live in seconds",
"analyticalStorageTtl": "Analytical Storage Time to Live",
"geospatialConfiguration": "Geospatial Configuration",
"geography": "Geography",
"geometry": "Geometry",
"uniqueKeys": "Unique keys",
"mongoTtlMessage": "To enable time-to-live (TTL) for your collection/documents,",
"mongoTtlLinkText": "create a TTL index",
"partitionKeyTooltipTemplate": "This {{partitionKeyName}} is used to distribute data across multiple partitions for scalability. The value \"{{partitionKeyValue}}\" determines how documents are partitioned.",
"largePartitionKeyEnabled": "Large {{partitionKeyName}} has been enabled.",
"hierarchicalPartitioned": "Hierarchically partitioned container.",
"nonHierarchicalPartitioned": "Non-hierarchically partitioned container."
},
"scale": {
"freeTierInfo": "With free tier, you will get the first {{ru}} RU/s and {{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 {{ru}} RU/s.",
"freeTierLearnMore": "Learn more.",
"throughputRuS": "Throughput (RU/s)",
"autoScaleCustomSettings": "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.",
"keyspaceSharedThroughput": "This table shared throughput is configured at the keyspace"
},
"partitionKeyEditor": {
"changePartitionKey": "Change {{partitionKeyName}}",
"currentPartitionKey": "Current {{partitionKeyName}}",
"partitioning": "Partitioning",
"hierarchical": "Hierarchical",
"nonHierarchical": "Non-hierarchical",
"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.",
"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.",
"changeButton": "Change",
"changeJob": "{{partitionKeyName}} change job",
"cancelButton": "Cancel",
"documentsProcessed": "({{processedCount}} of {{totalCount}} documents processed)"
},
"computedProperties": {
"ariaLabel": "Computed properties",
"learnMorePrefix": "about how to define computed properties and how to use them."
},
"indexingPolicy": {
"ariaLabel": "Indexing Policy"
},
"dataMasking": {
"ariaLabel": "Data Masking Policy",
"validationFailed": "Validation failed:",
"includedPathsRequired": "includedPaths is required",
"includedPathsMustBeArray": "includedPaths must be an array",
"excludedPathsMustBeArray": "excludedPaths must be an array if provided"
},
"containerPolicy": {
"vectorPolicy": "Vector Policy",
"fullTextPolicy": "Full Text Policy",
"createFullTextPolicy": "Create new full text search policy"
},
"globalSecondaryIndex": {
"indexesDefined": "This container has the following indexes defined for it.",
"learnMoreSuffix": "about how to define global secondary indexes and how to use them.",
"jsonAriaLabel": "Global Secondary Index JSON",
"addIndex": "Add index",
"settingsTitle": "Global Secondary Index Settings",
"sourceContainer": "Source container",
"indexDefinition": "Global secondary index definition"
},
"indexingPolicyRefresh": {
"refreshFailed": "Refreshing index transformation progress failed"
},
"throughputInput": {
"autoscale": "Autoscale",
"manual": "Manual",
"minimumRuS": "Minimum RU/s",
"maximumRuS": "Maximum RU/s",
"x10Equals": "x 10 =",
"storageCapacity": "Storage capacity",
"fixed": "Fixed",
"unlimited": "Unlimited",
"instant": "Instant",
"fourToSixHrs": "4-6 hrs",
"autoscaleDescription": "Based on usage, your {{resourceType}} throughput will scale from",
"freeTierWarning": "Billing will apply if you provision more than {{ru}} RU/s of manual throughput, or if the resource scales beyond {{ru}} RU/s with autoscale.",
"capacityCalculator": "Estimate your required RU/s with",
"capacityCalculatorLink": " capacity calculator",
"fixedStorageNote": "When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.",
"min": "min",
"max": "max"
},
"throughputBuckets": {
"label": "Throughput Buckets",
"bucketLabel": "Bucket {{id}}",
"dataExplorerQueryBucket": " (Data Explorer Query Bucket)",
"active": "Active",
"inactive": "Inactive"
}
},
"dialog": {
"defaultCloseButton": "Close"
}
}
}

View File

@@ -0,0 +1,295 @@
{
"common": {
"ok": "Aceptar",
"cancel": "Cancelar",
"close": "Cerrar",
"save": "Guardar",
"delete": "Eliminar",
"update": "Actualizar",
"discard": "Descartar",
"execute": "Ejecutar",
"loading": "Cargando",
"loadingEllipsis": "Cargando…",
"next": "Siguiente",
"previous": "Anterior",
"yes": "Sí",
"no": "No",
"result": "Resultado",
"learnMore": "Más información",
"getStarted": "Comenzar",
"retry": "Reintentar",
"apply": "Aplicar",
"refresh": "Actualizar",
"copy": "Copiar",
"create": "Crear",
"confirm": "Confirmar",
"open": "Abrir",
"rename": "Cambiar nombre",
"download": "Descargar",
"upload": "Cargar",
"connect": "Conectar",
"remove": "Quitar",
"increaseValueBy1": "Aumentar valor en 1",
"decreaseValueBy1": "Disminuir valor en 1"
},
"splashScreen": {
"title": {
"default": "Le presentamos Azure Cosmos DB",
"postgres": "Le damos la bienvenida a Azure Cosmos DB for PostgreSQL",
"vcoreMongo": "Le damos la bienvenida a Azure DocumentDB (con compatibilidad con MongoDB)"
},
"subtitle": {
"default": "Servicio de base de datos multimodelo distribuido globalmente para cualquier escala",
"getStarted": "Introducción a nuestros conjuntos de datos de ejemplo, documentación y herramientas adicionales."
},
"quickStart": {
"title": "Inicio rápido",
"description": "Iniciar un tutorial de inicio rápido para empezar a trabajar con datos de ejemplo"
},
"newCollection": {
"title": "Nuevo elemento: {{collectionName}}",
"description": "Creación de un nuevo contenedor para el almacenamiento y el rendimiento"
},
"samplesGallery": {
"title": "Galería de ejemplos de Azure Cosmos DB",
"description": "Descubra ejemplos que muestran patrones de aplicaciones inteligentes y escalables. Pruebe una ahora para ver la rapidez con la que puede pasar del concepto al código con Cosmos DB"
},
"connectCard": {
"title": "Conectar",
"description": "¿Prefiere usar sus propias herramientas? Busque la cadena de conexión que necesita para conectarse.",
"pgAdmin": {
"title": "Conexión con pgAdmin",
"description": "¿Prefiere pgAdmin? Busque las cadenas de conexión aquí"
},
"vsCode": {
"title": "Conectar con VS Code",
"description": "Consulta y administración de los clústeres de MongoDB y DocumentDB en Visual Studio Code"
}
},
"shell": {
"postgres": {
"title": "PostgreSQL Shell",
"description": "Creación de una tabla e interacción con datos mediante la interfaz de shell de PostgreSQL"
},
"vcoreMongo": {
"title": "Mongo Shell",
"description": "Creación de una colección e interacción con datos mediante la interfaz de shell de MongoDB"
}
},
"teachingBubble": {
"newToPostgres": {
"headline": "¿Es nuevo en Cosmos DB PGSQL?",
"body": "Le damos la bienvenida Si no está familiarizado con Cosmos DB PGSQL y necesita ayuda para empezar, aquí puede encontrar datos de ejemplo y consultarlos."
},
"resetPassword": {
"headline": "Crear la contraseña",
"body": "Si aún no ha cambiado la contraseña, cámbiela ahora."
},
"coachMark": {
"headline": "Empezar con el ejemplo {{collectionName}}",
"body": "Se le guiará para crear un contenedor de ejemplo con datos de ejemplo y, a continuación, le ofreceremos un paseo por el explorador de datos. También puede cancelar la reserva de esta excursión y explorarla por su cuenta."
}
},
"sections": {
"recents": "Recientes",
"clearRecents": "Borrar recientes",
"top3": "3 cosas principales que debes saber",
"learningResources": "Recursos de aprendizaje",
"nextSteps": "Siguientes pasos",
"tipsAndLearnMore": "Sugerencias y más información",
"notebook": "Notebook",
"needHelp": "¿Necesita ayuda?"
},
"top3Items": {
"sql": {
"advancedModeling": {
"title": "Patrones de modelado avanzados",
"description": "Obtenga información acerca de las estrategias avanzadas para optimizar la base de datos."
},
"partitioning": {
"title": "procedimientos recomendados de creación de particiones",
"description": "Aprenda a aplicar estrategias de creación de particiones y modelo de datos."
},
"resourcePlanning": {
"title": "Planear los requisitos de recursos",
"description": "Conozca las distintas opciones de configuración."
}
},
"mongo": {
"whatIsMongo": {
"title": "¿Qué es MongoDB API?",
"description": "Comprenda Azure Cosmos DB de MongoDB y sus características."
},
"features": {
"title": "Características y sintaxis",
"description": "Descubra las ventajas y características"
},
"migrate": {
"title": "Migrar los datos",
"description": "Pasos previos a la migración para mover datos"
}
},
"cassandra": {
"buildJavaApp": {
"title": "Compilación de una aplicación java",
"description": "Cree una aplicación java mediante un SDK."
},
"partitioning": {
"title": "procedimientos recomendados de creación de particiones",
"description": "Obtenga información acerca de cómo funciona la creación de particiones."
},
"requestUnits": {
"title": "Unidades de solicitud (RU)",
"description": "Descripción de los cargos de RU."
}
},
"gremlin": {
"dataModeling": {
"title": "Modelado de datos",
"description": "Recomendaciones de modelado de datos de grafos"
},
"partitioning": {
"title": "procedimientos recomendados de creación de particiones",
"description": "Obtenga información sobre cómo funciona la creación de particiones"
},
"queryData": {
"title": "Consultar datos",
"description": "Consulta de datos con Gremlin"
}
},
"tables": {
"whatIsTable": {
"title": "¿Cuál es el Table API?",
"description": "Descripción de Azure Cosmos DB para Table y sus características"
},
"migrate": {
"title": "Migrar los datos",
"description": "Obtenga información sobre cómo migrar sus datos"
},
"faq": {
"title": "Azure Cosmos DB para preguntas más frecuentes sobre tablas",
"description": "Preguntas comunes sobre Azure Cosmos DB para table"
}
}
},
"learningResources": {
"shortcuts": {
"title": "Explorador de datos métodos abreviados de teclado",
"description": "Obtenga información acerca de los métodos abreviados de teclado para navegar por Explorador de datos."
},
"liveTv": {
"title": "Conozca los aspectos básicos",
"description": "Vea Azure Cosmos DB introducción a programas de TV en vivo y vídeos de procedimientos."
},
"sql": {
"sdk": {
"title": "Introducción al uso de un SDK",
"description": "Obtenga información acerca del SDK de Azure Cosmos DB."
},
"migrate": {
"title": "Migrar los datos",
"description": "Migre datos mediante servicios de Azure y soluciones de código abierto."
}
},
"mongo": {
"nodejs": {
"title": "Compilación de una aplicación con Node.js",
"description": "Cree una aplicación Node.js."
},
"gettingStarted": {
"title": "Guía de introducción",
"description": "Conozca los conceptos básicos para empezar."
}
},
"cassandra": {
"createContainer": {
"title": "Crear un contenedor",
"description": "Conozca las opciones de creación de un contenedor."
},
"throughput": {
"title": "Aprovisionar rendimiento",
"description": "Aprenda a configurar el rendimiento."
}
},
"gremlin": {
"getStarted": {
"title": "Introducción ",
"description": "Creación, consulta y recorrido mediante la consola de Gremlin"
},
"importData": {
"title": "Importar datos del gráfico",
"description": "Información sobre los datos de ingesta masiva mediante BulkExecutor"
}
},
"tables": {
"dotnet": {
"title": "Compilación de una aplicación .NET",
"description": "Cómo acceder a Azure Cosmos DB para Table desde una aplicación .NET."
},
"java": {
"title": "Compilación de una aplicación java",
"description": "Creación de una Azure Cosmos DB para la aplicación Table con el SDK de Java "
}
}
},
"nextStepItems": {
"postgres": {
"dataModeling": "Modelado de datos",
"distributionColumn": "Cómo elegir una columna de distribución",
"buildApps": "Compilación de aplicaciones con Python, Java y Django"
},
"vcoreMongo": {
"migrateData": "Migrar datos",
"vectorSearch": "Creación de aplicaciones de inteligencia artificial con Vector Search",
"buildApps": "Compilación de aplicaciones con Nodejs"
}
},
"learnMoreItems": {
"postgres": {
"performanceTuning": "Ajuste de rendimiento",
"diagnosticQueries": "Consultas de diagnóstico útiles",
"sqlReference": "Referencia de SQL distribuida"
},
"vcoreMongo": {
"vectorSearch": "Vector de búsqueda",
"textIndexing": "Indización de texto",
"troubleshoot": "Solucionar problemas comunes"
}
},
"fabric": {
"buildTitle": "Compilación de la base de datos",
"useTitle": "Uso de la base de datos",
"newContainer": {
"title": "Nuevo contenedor",
"description": "Creación de un contenedor de destino para almacenar los datos"
},
"sampleData": {
"title": "Datos de ejemplo",
"description": "Carga de datos de ejemplo en la base de datos"
},
"sampleVectorData": {
"title": "Datos vectoriales de ejemplo",
"description": "Carga de datos vectoriales de ejemplo con text-embedding-ada-002"
},
"appDevelopment": {
"title": "Desarrollo de aplicaciones",
"description": "Empiece aquí para usar un SDK para compilar sus aplicaciones"
},
"sampleGallery": {
"title": "Galería de ejemplos",
"description": "Obtener ejemplos de un extremo a otro del mundo real"
}
},
"sampleDataDialog": {
"title": "Datos de ejemplo",
"startButton": "Inicio",
"createPrompt": "Cree un contenedor \"{{containerName}}\" e importe datos de ejemplo en él. Esto podría tardar unos minutos.",
"creatingContainer": "Creando contenedor \"{{containerName}}\"...",
"importingData": "Importando datos en \"{{containerName}}\"...",
"success": "Se ha creado correctamente \"{{containerName}}\" con datos de ejemplo.",
"errorContainerExists": "El contenedor \"{{containerName}}\" de la base de datos \"{{databaseName}}\" ya existe. Elimínelo y vuelva a intentarlo.",
"errorCreateContainer": "Error al crear el contenedor: {{error}}",
"errorImportData": "No se pudieron importar los datos: {{error}}"
}
}
}

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