mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-03-05 07:45:25 +00:00
Compare commits
25 Commits
users/aisa
...
loc_batch2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6237ff795a | ||
|
|
766cc5b2e9 | ||
|
|
4f9ccba4b3 | ||
|
|
6f078e2aa5 | ||
|
|
9af97c16ef | ||
|
|
c343bad630 | ||
|
|
9579d1270b | ||
|
|
9496cf83ee | ||
|
|
dbe26654f1 | ||
|
|
b478f2732c | ||
|
|
204444b878 | ||
|
|
2e5c355479 | ||
|
|
5832170b2b | ||
|
|
cc26e2800e | ||
|
|
39ac7cf3f2 | ||
|
|
ed3a79f880 | ||
|
|
68ba19d406 | ||
|
|
81ad13508b | ||
|
|
3f959e2235 | ||
|
|
67250f0f6b | ||
|
|
df6312038a | ||
|
|
e6c27f39be | ||
|
|
937989a47d | ||
|
|
f166ef9c66 | ||
|
|
b83f54e253 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,6 +17,7 @@ Contracts/*
|
||||
failure.png
|
||||
screenshots/*
|
||||
GettingStarted-ignore*.ipynb
|
||||
src/Localization/Keys.generated.ts
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
|
||||
14319
package-lock.json
generated
14319
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
69
package.json
69
package.json
@@ -4,7 +4,7 @@
|
||||
"description": "Cosmos Explorer",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/arm-cosmosdb": "16.4.0",
|
||||
"@azure/cosmos": "4.7.0",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "4.5.0",
|
||||
@@ -14,12 +14,12 @@
|
||||
"@fluentui/react": "8.119.0",
|
||||
"@fluentui/react-components": "9.54.2",
|
||||
"@jupyterlab/services": "6.0.2",
|
||||
"@jupyterlab/terminal": "3.0.3",
|
||||
"@jupyterlab/terminal": "3.6.8",
|
||||
"@microsoft/applicationinsights-web": "2.6.1",
|
||||
"@nteract/commutable": "7.5.1",
|
||||
"@nteract/connected-components": "6.8.2",
|
||||
"@nteract/core": "15.1.9",
|
||||
"@nteract/data-explorer": "8.0.3",
|
||||
"@nteract/data-explorer": "8.2.12",
|
||||
"@nteract/directory-listing": "2.0.6",
|
||||
"@nteract/dropdown-menu": "1.0.1",
|
||||
"@nteract/editor": "10.1.12",
|
||||
@@ -31,7 +31,7 @@
|
||||
"@nteract/monaco-editor": "3.2.2",
|
||||
"@nteract/octicons": "2.0.0",
|
||||
"@nteract/outputs": "3.0.9",
|
||||
"@nteract/presentational-components": "3.0.7",
|
||||
"@nteract/presentational-components": "3.4.12",
|
||||
"@nteract/stateful-components": "1.7.0",
|
||||
"@nteract/styles": "2.0.2",
|
||||
"@nteract/transform-geojson": "5.1.8",
|
||||
@@ -39,25 +39,26 @@
|
||||
"@nteract/transform-plotly": "6.1.6",
|
||||
"@nteract/transform-vdom": "4.0.11",
|
||||
"@nteract/transform-vega": "7.0.6",
|
||||
"@octokit/request": "8.4.1",
|
||||
"@octokit/rest": "17.9.2",
|
||||
"@phosphor/widgets": "1.9.3",
|
||||
"@testing-library/jest-dom": "6.4.6",
|
||||
"@types/lodash": "4.14.171",
|
||||
"@types/mkdirp": "1.0.1",
|
||||
"@types/node-fetch": "2.5.7",
|
||||
"@types/node-fetch": "2.6.13",
|
||||
"@xmldom/xmldom": "0.7.13",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/xterm": "5.5.0",
|
||||
"allotment": "1.20.2",
|
||||
"applicationinsights": "1.8.0",
|
||||
"bootstrap": "3.4.1",
|
||||
"canvas": "2.11.2",
|
||||
"canvas": "3.2.1",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"clipboard-copy": "4.0.1",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"crossroads": "0.12.2",
|
||||
"css-element-queries": "1.1.1",
|
||||
"d3": "7.8.5",
|
||||
"d3": "7.9.0",
|
||||
"datatables.net-colreorder-dt": "1.7.0",
|
||||
"datatables.net-dt": "1.13.8",
|
||||
"date-fns": "1.29.0",
|
||||
@@ -70,7 +71,8 @@
|
||||
"html2canvas": "1.0.0-rc.5",
|
||||
"i18next": "23.11.5",
|
||||
"i18next-browser-languagedetector": "6.0.1",
|
||||
"i18next-http-backend": "1.0.23",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"iframe-resizer-react": "1.1.0",
|
||||
"immer": "9.0.6",
|
||||
"immutable": "4.0.0-rc.12",
|
||||
@@ -79,12 +81,15 @@
|
||||
"jquery-typeahead": "2.11.1",
|
||||
"jquery-ui-dist": "1.13.2",
|
||||
"knockout": "3.5.1",
|
||||
"loader-utils": "2.0.3",
|
||||
"lodash": "4.17.23",
|
||||
"lodash-es": "4.17.23",
|
||||
"min-document": "2.19.1",
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.44.0",
|
||||
"ms": "2.1.3",
|
||||
"nanoid": "3.3.8",
|
||||
"p-retry": "6.2.1",
|
||||
"patch-package": "8.0.0",
|
||||
"patch-package": "8.0.1",
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"post-robot": "10.0.42",
|
||||
"q": "1.5.1",
|
||||
@@ -103,7 +108,6 @@
|
||||
"react-youtube": "9.0.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rx-jupyter": "5.5.12",
|
||||
"sanitize-html": "2.3.3",
|
||||
"shell-quote": "1.7.3",
|
||||
"styled-components": "5.0.1",
|
||||
"swr": "0.4.0",
|
||||
@@ -111,16 +115,30 @@
|
||||
"tinykeys": "2.1.0",
|
||||
"underscore": "1.12.1",
|
||||
"utility-types": "3.10.0",
|
||||
"web-vitals": "4.2.4",
|
||||
"uuid": "9.0.0",
|
||||
"web-vitals": "4.2.4",
|
||||
"ws": "8.17.1",
|
||||
"zustand": "3.5.0"
|
||||
},
|
||||
"overrides": {
|
||||
"d3-color": "3.1.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"less-vars-loader": {
|
||||
"json5": "1.0.2"
|
||||
},
|
||||
"trim": "0.0.3",
|
||||
"@octokit/plugin-paginate-rest": "9.2.2",
|
||||
"@octokit/request-error": "5.1.1",
|
||||
"@octokit/request": "8.4.1",
|
||||
"prismjs": "1.30.0",
|
||||
"sanitize-html": "2.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@playwright/test": "1.49.1",
|
||||
"@playwright/test": "1.55.1",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@types/applicationinsights-js": "1.0.7",
|
||||
"@types/codemirror": "0.0.56",
|
||||
@@ -134,7 +152,7 @@
|
||||
"@types/hasher": "0.0.31",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/jquery": "3.5.29",
|
||||
"@types/node": "12.11.1",
|
||||
"@types/node": "18.19.0",
|
||||
"@types/post-robot": "10.0.1",
|
||||
"@types/q": "1.5.1",
|
||||
"@types/react": "17.0.44",
|
||||
@@ -145,7 +163,7 @@
|
||||
"@types/react-window": "1.8.8",
|
||||
"@types/sanitize-html": "1.27.2",
|
||||
"@types/sinon": "2.3.3",
|
||||
"@types/styled-components": "5.1.1",
|
||||
"@types/styled-components": "5.1.32",
|
||||
"@types/underscore": "1.7.36",
|
||||
"@types/youtube-player": "5.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.4",
|
||||
@@ -153,6 +171,7 @@
|
||||
"@webpack-cli/serve": "2.0.5",
|
||||
"babel-jest": "29.7.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"brace-expansion": "1.1.12",
|
||||
"buffer": "5.1.0",
|
||||
"case-sensitive-paths-webpack-plugin": "2.4.0",
|
||||
"create-file-webpack": "1.0.2",
|
||||
@@ -170,6 +189,7 @@
|
||||
"html-inline-css-webpack-plugin": "1.11.2",
|
||||
"html-loader": "5.0.0",
|
||||
"html-webpack-plugin": "5.5.3",
|
||||
"i18next-resources-for-ts": "2.0.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-canvas-mock": "2.5.2",
|
||||
"jest-circus": "29.7.0",
|
||||
@@ -177,7 +197,8 @@
|
||||
"jest-html-loader": "1.0.0",
|
||||
"jest-react-hooks-shallow": "1.5.1",
|
||||
"jest-trx-results-processor": "3.0.2",
|
||||
"less": "3.8.1",
|
||||
"js-yaml": "3.14.2",
|
||||
"less": "4.5.1",
|
||||
"less-loader": "11.1.3",
|
||||
"less-vars-loader": "1.1.0",
|
||||
"mini-css-extract-plugin": "2.1.0",
|
||||
@@ -195,14 +216,17 @@
|
||||
"typedoc": "0.26.2",
|
||||
"typescript": "4.9.5",
|
||||
"url-loader": "4.1.1",
|
||||
"wait-on": "4.0.2",
|
||||
"webpack": "5.88.2",
|
||||
"webpack-bundle-analyzer": "4.9.1",
|
||||
"values-to-keys": "1.1.0",
|
||||
"wait-on": "9.0.3",
|
||||
"webpack": "5.104.1",
|
||||
"webpack-bundle-analyzer": "5.2.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "4.15.2"
|
||||
"webpack-dev-server": "5.2.3",
|
||||
"ws": "8.17.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"postinstall": "patch-package && npm run generate:i18n-keys",
|
||||
"prestart": "npm run generate:i18n-keys",
|
||||
"start": "webpack serve --mode development",
|
||||
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
|
||||
"build:dataExplorer:ci": "npm run build:ci",
|
||||
@@ -229,6 +253,7 @@
|
||||
"strict:find": "node ./strict-null-checks/find.js",
|
||||
"strict:add": "node ./strict-null-checks/auto-add.js",
|
||||
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks",
|
||||
"generate:i18n-keys": "node utils/generateI18nKeys.mjs",
|
||||
"generateARMClients": "npx ts-node utils/armClientGenerator/generator.ts"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
@@ -26,15 +26,6 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
launchOptions: {
|
||||
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "firefox",
|
||||
use: {
|
||||
|
||||
587
preview/package-lock.json
generated
587
preview/package-lock.json
generated
@@ -8,9 +8,10 @@
|
||||
"name": "cosmos-explorer-preview",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.3",
|
||||
"express": "^4.21.2",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"body-parser": "^2.2.2",
|
||||
"express": "^5.2.1",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
"node": "^20.19.5",
|
||||
"node-fetch": "^2.6.1",
|
||||
"path-to-regexp": "^0.1.12"
|
||||
@@ -18,8 +19,7 @@
|
||||
},
|
||||
"node_modules/@types/http-proxy": {
|
||||
"version": "1.17.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz",
|
||||
"integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -29,50 +29,40 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
"mime-types": "^3.0.0",
|
||||
"negotiator": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.13.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^1.0.5",
|
||||
"debug": "^4.4.3",
|
||||
"http-errors": "^2.0.0",
|
||||
"iconv-lite": "^0.7.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.14.1",
|
||||
"raw-body": "^3.0.1",
|
||||
"type-is": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -83,8 +73,7 @@
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
@@ -95,8 +84,7 @@
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
@@ -109,13 +97,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
||||
"integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
@@ -127,20 +117,22 @@
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"license": "MIT"
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -160,18 +152,9 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -195,24 +178,21 @@
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
@@ -238,104 +218,77 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
"content-disposition": "^1.0.0",
|
||||
"content-type": "^1.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"cookie-signature": "^1.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"finalhandler": "^2.1.0",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"merge-descriptors": "^2.0.0",
|
||||
"mime-types": "^3.0.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"once": "^1.4.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"proxy-addr": "^2.0.7",
|
||||
"qs": "^6.14.0",
|
||||
"range-parser": "^1.2.1",
|
||||
"router": "^2.2.0",
|
||||
"send": "^1.1.0",
|
||||
"serve-static": "^2.2.0",
|
||||
"statuses": "^2.0.1",
|
||||
"type-is": "^2.0.1",
|
||||
"vary": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "2.0.1",
|
||||
"unpipe": "~1.0.0"
|
||||
"debug": "^4.4.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"parseurl": "^1.3.3",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.3",
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
@@ -353,25 +306,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
@@ -393,8 +344,7 @@
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
@@ -405,8 +355,7 @@
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -416,8 +365,7 @@
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -427,8 +375,7 @@
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
@@ -437,17 +384,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy": {
|
||||
@@ -480,8 +432,7 @@
|
||||
},
|
||||
"node_modules/http-proxy-middleware/node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
@@ -491,8 +442,7 @@
|
||||
},
|
||||
"node_modules/http-proxy-middleware/node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@@ -502,16 +452,14 @@
|
||||
},
|
||||
"node_modules/http-proxy-middleware/node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-middleware/node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
@@ -522,8 +470,7 @@
|
||||
},
|
||||
"node_modules/http-proxy-middleware/node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
@@ -532,14 +479,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
@@ -570,62 +521,58 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"license": "MIT",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"license": "MIT",
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"license": "MIT",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
@@ -633,32 +580,27 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node": {
|
||||
"version": "20.19.5",
|
||||
"resolved": "https://registry.npmjs.org/node/-/node-20.19.5.tgz",
|
||||
"integrity": "sha512-9fJOHEP8AVrwpbhlUxnbudW8IbkseQVxl4yNQyI/rDfP+gNwKEmfPtBc/Luyf677i5Y0HKIBHiApiB9S9vvxKw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"node-bin-setup": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node": "bin/node"
|
||||
"node": "bin/node.exe"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-bin-setup": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/node-bin-setup/-/node-bin-setup-1.1.4.tgz",
|
||||
"integrity": "sha512-vWNHOne0ZUavArqPP5LJta50+S8R261Fr5SvGul37HbEDcowvLjwdvd0ZeSr0r2lTSrPxl6okq9QUw8BFGiAxA=="
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"license": "MIT",
|
||||
@@ -677,10 +619,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node/node_modules/node-bin-setup": {
|
||||
"version": "1.1.4",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@@ -698,6 +643,14 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -708,8 +661,7 @@
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
@@ -740,11 +692,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"version": "6.14.1",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
@@ -762,40 +713,46 @@
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.7.0",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"is-promise": "^4.0.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"path-to-regexp": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/router/node_modules/path-to-regexp": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
||||
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@@ -803,61 +760,46 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "2.0.1"
|
||||
"debug": "^4.4.3",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.1",
|
||||
"mime-types": "^3.0.2",
|
||||
"ms": "^2.1.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"range-parser": "^1.2.1",
|
||||
"statuses": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/debug/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/send/node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
|
||||
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.19.0"
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"parseurl": "^1.3.3",
|
||||
"send": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
@@ -866,8 +808,7 @@
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
@@ -884,8 +825,7 @@
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
@@ -899,8 +839,7 @@
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -916,8 +855,7 @@
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
@@ -933,8 +871,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"license": "MIT",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -951,11 +890,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"license": "MIT",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
"content-type": "^1.0.5",
|
||||
"media-typer": "^1.1.0",
|
||||
"mime-types": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
@@ -968,13 +909,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
@@ -993,6 +927,11 @@
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
"keywords": [],
|
||||
"author": "Microsoft Corporation",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.20.3",
|
||||
"express": "^4.21.2",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"body-parser": "^2.2.2",
|
||||
"express": "^5.2.1",
|
||||
"follow-redirects": "^1.15.6",
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
"node": "^20.19.5",
|
||||
"node-fetch": "^2.6.1",
|
||||
"path-to-regexp": "^0.1.12"
|
||||
|
||||
11
src/@types/i18next.d.ts
vendored
Normal file
11
src/@types/i18next.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import "i18next";
|
||||
import Resources from "../Localization/en/Resources.json";
|
||||
|
||||
declare module "i18next" {
|
||||
interface CustomTypeOptions {
|
||||
defaultNS: "Resources";
|
||||
resources: {
|
||||
Resources: typeof Resources;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
||||
import { isExpectedError } from "../Metrics/ErrorClassification";
|
||||
import { scenarioMonitor } from "../Metrics/ScenarioMonitor";
|
||||
import { userContext } from "../UserContext";
|
||||
import { ARMError } from "../Utils/arm/request";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
@@ -31,6 +33,12 @@ export const handleError = (
|
||||
|
||||
// checks for errors caused by firewall and sends them to portal to handle
|
||||
sendNotificationForError(errorMessage, errorCode);
|
||||
|
||||
// Mark expected failures for health metrics (auth, firewall, permissions, etc.)
|
||||
// This ensures timeouts with expected failures emit healthy instead of unhealthy
|
||||
if (isExpectedError(error)) {
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
}
|
||||
};
|
||||
|
||||
export const getErrorMessage = (error: string | Error = ""): string => {
|
||||
|
||||
@@ -38,7 +38,7 @@ export function queryIterator(databaseId: string, collection: Collection, query:
|
||||
let continuationToken: string;
|
||||
return {
|
||||
fetchNext: () => {
|
||||
return queryDocuments(databaseId, collection, false, query).then((response) => {
|
||||
return queryDocuments(databaseId, collection, false, query, continuationToken).then((response) => {
|
||||
continuationToken = response.continuationToken;
|
||||
const headers: { [key: string]: string | number } = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { MinimalQueryIterator, nextPage } from "../IteratorUtilities";
|
||||
|
||||
// Redact sensitive information from BadRequest errors with specific codes
|
||||
export const redactSyntaxErrorMessage = (error: unknown): unknown => {
|
||||
const codesToRedact = ["SC1001", "SC2001"];
|
||||
const codesToRedact = ["SC1001", "SC2001", "SC1010"];
|
||||
|
||||
try {
|
||||
// Handle error objects with a message property
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -275,8 +275,7 @@ export interface DataMaskingPolicy {
|
||||
startPosition: number;
|
||||
length: number;
|
||||
}>;
|
||||
excludedPaths: string[];
|
||||
isPolicyEnabled: boolean;
|
||||
excludedPaths?: string[];
|
||||
}
|
||||
|
||||
export interface MaterializedView {
|
||||
|
||||
@@ -395,6 +395,14 @@ describe("CopyJobUtils", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for different completion percentage", () => {
|
||||
const jobs1 = [createMockJob("job1", "Running")];
|
||||
const jobs2 = [{ ...createMockJob("job1", "Running"), CompletionPercentage: 75 }];
|
||||
|
||||
const result = CopyJobUtils.isEqual(jobs1, jobs2);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for empty arrays", () => {
|
||||
const result = CopyJobUtils.isEqual([], []);
|
||||
expect(result).toBe(true);
|
||||
|
||||
@@ -142,7 +142,7 @@ export function isEqual(prevJobs: CopyJobType[], newJobs: CopyJobType[]): boolea
|
||||
if (!newJob) {
|
||||
return false;
|
||||
}
|
||||
return prevJob.Status === newJob.Status;
|
||||
return prevJob.Status === newJob.Status && prevJob.CompletionPercentage === newJob.CompletionPercentage;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -30,7 +30,6 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
|
||||
dataMaskingPolicy: {
|
||||
includedPaths: [],
|
||||
excludedPaths: ["/excludedPath"],
|
||||
isPolicyEnabled: true,
|
||||
},
|
||||
indexes: [],
|
||||
}),
|
||||
@@ -307,12 +306,10 @@ describe("SettingsComponent", () => {
|
||||
dataMaskingContent: {
|
||||
includedPaths: [],
|
||||
excludedPaths: ["/excludedPath"],
|
||||
isPolicyEnabled: true,
|
||||
},
|
||||
dataMaskingContentBaseline: {
|
||||
includedPaths: [],
|
||||
excludedPaths: [],
|
||||
isPolicyEnabled: false,
|
||||
},
|
||||
isDataMaskingDirty: true,
|
||||
});
|
||||
@@ -326,7 +323,6 @@ describe("SettingsComponent", () => {
|
||||
expect(wrapper.state("dataMaskingContentBaseline")).toEqual({
|
||||
includedPaths: [],
|
||||
excludedPaths: ["/excludedPath"],
|
||||
isPolicyEnabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -340,7 +336,6 @@ describe("SettingsComponent", () => {
|
||||
const invalidPolicy: InvalidPolicy = {
|
||||
includedPaths: "invalid",
|
||||
excludedPaths: [],
|
||||
isPolicyEnabled: false,
|
||||
};
|
||||
// Use type assertion since we're deliberately testing with invalid data
|
||||
settingsComponentInstance["onDataMaskingContentChange"](invalidPolicy as unknown as DataModels.DataMaskingPolicy);
|
||||
@@ -349,7 +344,6 @@ describe("SettingsComponent", () => {
|
||||
expect(wrapper.state("dataMaskingContent")).toEqual({
|
||||
includedPaths: "invalid",
|
||||
excludedPaths: [],
|
||||
isPolicyEnabled: false,
|
||||
});
|
||||
expect(wrapper.state("dataMaskingValidationErrors")).toEqual(["includedPaths must be an array"]);
|
||||
|
||||
@@ -364,7 +358,6 @@ describe("SettingsComponent", () => {
|
||||
},
|
||||
],
|
||||
excludedPaths: ["/excludedPath"],
|
||||
isPolicyEnabled: true,
|
||||
};
|
||||
|
||||
settingsComponentInstance["onDataMaskingContentChange"](validPolicy);
|
||||
@@ -388,7 +381,6 @@ describe("SettingsComponent", () => {
|
||||
},
|
||||
],
|
||||
excludedPaths: ["/excludedPath1"],
|
||||
isPolicyEnabled: false,
|
||||
};
|
||||
|
||||
const modifiedPolicy = {
|
||||
@@ -401,7 +393,6 @@ describe("SettingsComponent", () => {
|
||||
},
|
||||
],
|
||||
excludedPaths: ["/excludedPath2"],
|
||||
isPolicyEnabled: true,
|
||||
};
|
||||
|
||||
// Set initial state
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { isCapabilityEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||
import * as React from "react";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
@@ -70,6 +70,7 @@ import {
|
||||
getMongoNotification,
|
||||
getTabTitle,
|
||||
hasDatabaseSharedThroughput,
|
||||
isDataMaskingEnabled,
|
||||
isDirty,
|
||||
parseConflictResolutionMode,
|
||||
parseConflictResolutionProcedure,
|
||||
@@ -686,22 +687,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)) {
|
||||
if (newDataMasking.includedPaths === undefined || newDataMasking.includedPaths === null) {
|
||||
validationErrors.push("includedPaths is required");
|
||||
} else if (!Array.isArray(newDataMasking.includedPaths)) {
|
||||
validationErrors.push("includedPaths must be an array");
|
||||
}
|
||||
if (!Array.isArray(newDataMasking.excludedPaths)) {
|
||||
validationErrors.push("excludedPaths must be an array");
|
||||
}
|
||||
if (typeof newDataMasking.isPolicyEnabled !== "boolean") {
|
||||
validationErrors.push("isPolicyEnabled must be a boolean");
|
||||
if (newDataMasking.excludedPaths !== undefined && !Array.isArray(newDataMasking.excludedPaths)) {
|
||||
validationErrors.push("excludedPaths must be an array if provided");
|
||||
}
|
||||
|
||||
this.setState({
|
||||
@@ -842,7 +835,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();
|
||||
@@ -1073,8 +1065,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 +1455,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,
|
||||
|
||||
@@ -53,7 +53,6 @@ describe("DataMaskingComponent", () => {
|
||||
},
|
||||
],
|
||||
excludedPaths: [],
|
||||
isPolicyEnabled: false,
|
||||
};
|
||||
|
||||
let changeContentCallback: () => void;
|
||||
@@ -78,7 +77,7 @@ describe("DataMaskingComponent", () => {
|
||||
<DataMaskingComponent
|
||||
{...mockProps}
|
||||
dataMaskingContent={samplePolicy}
|
||||
dataMaskingContentBaseline={{ ...samplePolicy, isPolicyEnabled: true }}
|
||||
dataMaskingContentBaseline={{ ...samplePolicy, excludedPaths: ["/excluded"] }}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -123,7 +122,7 @@ describe("DataMaskingComponent", () => {
|
||||
});
|
||||
|
||||
it("resets content when shouldDiscardDataMasking is true", async () => {
|
||||
const baselinePolicy = { ...samplePolicy, isPolicyEnabled: true };
|
||||
const baselinePolicy = { ...samplePolicy, excludedPaths: ["/excluded"] };
|
||||
|
||||
const wrapper = mount(
|
||||
<DataMaskingComponent
|
||||
@@ -159,7 +158,7 @@ describe("DataMaskingComponent", () => {
|
||||
wrapper.update();
|
||||
|
||||
// Update baseline to trigger componentDidUpdate
|
||||
const newBaseline = { ...samplePolicy, isPolicyEnabled: true };
|
||||
const newBaseline = { ...samplePolicy, excludedPaths: ["/excluded"] };
|
||||
wrapper.setProps({ dataMaskingContentBaseline: newBaseline });
|
||||
|
||||
expect(mockProps.onDataMaskingDirtyChange).toHaveBeenCalledWith(true);
|
||||
@@ -174,7 +173,6 @@ describe("DataMaskingComponent", () => {
|
||||
const invalidPolicy: Record<string, unknown> = {
|
||||
includedPaths: "not an array",
|
||||
excludedPaths: [] as string[],
|
||||
isPolicyEnabled: "not a boolean",
|
||||
};
|
||||
|
||||
mockGetValue.mockReturnValue(JSON.stringify(invalidPolicy));
|
||||
@@ -197,7 +195,7 @@ describe("DataMaskingComponent", () => {
|
||||
wrapper.update();
|
||||
|
||||
// First change
|
||||
const modifiedPolicy1 = { ...samplePolicy, isPolicyEnabled: true };
|
||||
const modifiedPolicy1 = { ...samplePolicy, excludedPaths: ["/path1"] };
|
||||
mockGetValue.mockReturnValue(JSON.stringify(modifiedPolicy1));
|
||||
changeContentCallback();
|
||||
expect(mockProps.onDataMaskingDirtyChange).toHaveBeenCalledWith(true);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
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 { 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 +22,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> {
|
||||
@@ -140,7 +130,7 @@ export class DataMaskingComponent extends React.Component<DataMaskingComponentPr
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (!isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking)) {
|
||||
if (!isDataMaskingEnabled(this.props.dataMaskingContent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import * as Constants from "../../../Common/Constants";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
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 +90,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) {
|
||||
|
||||
@@ -68,7 +68,6 @@ export const collection = {
|
||||
dataMaskingPolicy: ko.observable<DataModels.DataMaskingPolicy>({
|
||||
includedPaths: [],
|
||||
excludedPaths: ["/excludedPath"],
|
||||
isPolicyEnabled: true,
|
||||
}),
|
||||
readSettings: () => {
|
||||
return;
|
||||
|
||||
@@ -604,6 +604,58 @@ exports[`SettingsComponent renders 1`] = `
|
||||
/>
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerButtonProps={
|
||||
{
|
||||
"data-test": "settings-tab-header/DataMaskingTab",
|
||||
}
|
||||
}
|
||||
headerText="Masking Policy (preview)"
|
||||
itemKey="DataMaskingTab"
|
||||
key="DataMaskingTab"
|
||||
style={
|
||||
{
|
||||
"backgroundColor": "var(--colorNeutralBackground1)",
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
"marginTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"backgroundColor": "var(--colorNeutralBackground1)",
|
||||
"color": "var(--colorNeutralForeground1)",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<DataMaskingComponent
|
||||
dataMaskingContent={
|
||||
{
|
||||
"excludedPaths": [
|
||||
"/excludedPath",
|
||||
],
|
||||
"includedPaths": [],
|
||||
}
|
||||
}
|
||||
dataMaskingContentBaseline={
|
||||
{
|
||||
"excludedPaths": [
|
||||
"/excludedPath",
|
||||
],
|
||||
"includedPaths": [],
|
||||
}
|
||||
}
|
||||
onDataMaskingContentChange={[Function]}
|
||||
onDataMaskingDirtyChange={[Function]}
|
||||
resetShouldDiscardDataMasking={[Function]}
|
||||
shouldDiscardDataMasking={false}
|
||||
validationErrors={[]}
|
||||
/>
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerButtonProps={
|
||||
{
|
||||
|
||||
@@ -113,7 +113,7 @@ export class ContainerSampleGenerator {
|
||||
? await createMongoDocument(collection.databaseId, collection, shardKey, doc)
|
||||
: await createDocument(collection, doc);
|
||||
} catch (error) {
|
||||
NotificationConsoleUtils.logConsoleError(error);
|
||||
NotificationConsoleUtils.logConsoleError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -329,7 +329,10 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
}
|
||||
|
||||
private static extractHeaderStatus(consoleData: ConsoleData) {
|
||||
return consoleData?.message.split(":\n")[0];
|
||||
if (!consoleData?.message || typeof consoleData.message !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
return consoleData.message.split(":\n")[0];
|
||||
}
|
||||
|
||||
private onConsoleWasExpanded = (): void => {
|
||||
|
||||
@@ -6,6 +6,8 @@ import { DocumentAddRegular, LinkMultipleRegular, OpenRegular } from "@fluentui/
|
||||
import { SampleDataConfiguration, SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
|
||||
import { SampleDataFile } from "Explorer/SplashScreen/SampleUtil";
|
||||
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
|
||||
import * as React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
@@ -159,8 +161,8 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
const getSplashScreenButtons = (): JSX.Element => {
|
||||
const buttons: FabricHomeScreenButtonProps[] = [
|
||||
{
|
||||
title: "New container",
|
||||
description: "Create a destination container to store your data",
|
||||
title: t(Keys.splashScreen.fabric.newContainer.title),
|
||||
description: t(Keys.splashScreen.fabric.newContainer.description),
|
||||
icon: <DocumentAddRegular />,
|
||||
onClick: () => {
|
||||
const databaseId = isFabricNative() ? userContext.fabricContext?.databaseName : undefined;
|
||||
@@ -168,8 +170,8 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Sample Data",
|
||||
description: "Load sample data in your database",
|
||||
title: t(Keys.splashScreen.fabric.sampleData.title),
|
||||
description: t(Keys.splashScreen.fabric.sampleData.description),
|
||||
icon: <img src={CosmosDbBlackIcon} alt={"Azure Cosmos DB icon"} aria-hidden="true" />,
|
||||
onClick: () => {
|
||||
setSelectedSampleDataConfiguration({
|
||||
@@ -181,8 +183,8 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Sample Vector Data",
|
||||
description: "Load sample vector data with text-embedding-ada-002",
|
||||
title: t(Keys.splashScreen.fabric.sampleVectorData.title),
|
||||
description: t(Keys.splashScreen.fabric.sampleVectorData.description),
|
||||
icon: <img src={AzureOpenAiIcon} alt={"Azure Open AI icon"} aria-hidden="true" />,
|
||||
onClick: () => {
|
||||
setSelectedSampleDataConfiguration({
|
||||
@@ -194,14 +196,14 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "App development",
|
||||
description: "Start here to use an SDK to build your apps",
|
||||
title: t(Keys.splashScreen.fabric.appDevelopment.title),
|
||||
description: t(Keys.splashScreen.fabric.appDevelopment.description),
|
||||
icon: <LinkMultipleRegular />,
|
||||
onClick: () => window.open("https://aka.ms/cosmosdbfabricsdk", "_blank"),
|
||||
},
|
||||
{
|
||||
title: "Sample Gallery",
|
||||
description: "Get real-world end-to-end samples",
|
||||
title: t(Keys.splashScreen.fabric.sampleGallery.title),
|
||||
description: t(Keys.splashScreen.fabric.sampleGallery.description),
|
||||
icon: <img src={GithubIcon} alt={"GitHub icon"} aria-hidden="true" />,
|
||||
onClick: () => window.open("https://aka.ms/CosmosFabricSamplesGallery", "_blank"),
|
||||
},
|
||||
@@ -222,7 +224,9 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
);
|
||||
};
|
||||
|
||||
const title = isFabricNativeReadOnly() ? "Use your database" : "Build your database";
|
||||
const title = isFabricNativeReadOnly()
|
||||
? t(Keys.splashScreen.fabric.useTitle)
|
||||
: t(Keys.splashScreen.fabric.buildTitle);
|
||||
return (
|
||||
<>
|
||||
<CosmosFluentProvider className={styles.homeContainer}>
|
||||
@@ -238,9 +242,9 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
{getSplashScreenButtons()}
|
||||
{
|
||||
<div className={styles.footer}>
|
||||
Need help?{" "}
|
||||
{t(Keys.splashScreen.sections.needHelp)}{" "}
|
||||
<Link href="https://learn.microsoft.com/fabric/database/cosmos-db/overview" target="_blank">
|
||||
Learn more <OpenRegular />
|
||||
{t(Keys.common.learnMore)} <OpenRegular />
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
} from "@fluentui/react-components";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { checkContainerExists, createContainer, importData, SampleDataFile } from "Explorer/SplashScreen/SampleUtil";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
|
||||
@@ -59,7 +61,7 @@ export const SampleDataImportDialog: React.FC<{
|
||||
setStatus("creating");
|
||||
const databaseName = props.sampleDataConfiguration.databaseName;
|
||||
if (checkContainerExists(databaseName, containerName)) {
|
||||
const msg = `The container "${containerName}" in database "${databaseName}" already exists. Please delete it and retry.`;
|
||||
const msg = t(Keys.splashScreen.sampleDataDialog.errorContainerExists, { containerName, databaseName });
|
||||
setStatus("error");
|
||||
setErrorMessage(msg);
|
||||
return;
|
||||
@@ -75,7 +77,11 @@ export const SampleDataImportDialog: React.FC<{
|
||||
);
|
||||
} catch (error) {
|
||||
setStatus("error");
|
||||
setErrorMessage(`Failed to create container: ${error instanceof Error ? error.message : String(error)}`);
|
||||
setErrorMessage(
|
||||
t(Keys.splashScreen.sampleDataDialog.errorCreateContainer, {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -86,7 +92,11 @@ export const SampleDataImportDialog: React.FC<{
|
||||
setStatus("completed");
|
||||
} catch (error) {
|
||||
setStatus("error");
|
||||
setErrorMessage(`Failed to import data: ${error instanceof Error ? error.message : String(error)}`);
|
||||
setErrorMessage(
|
||||
t(Keys.splashScreen.sampleDataDialog.errorImportData, {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -112,14 +122,26 @@ export const SampleDataImportDialog: React.FC<{
|
||||
const renderContent = () => {
|
||||
switch (status) {
|
||||
case "idle":
|
||||
return `Create a container "${containerName}" and import sample data into it. This may take a few minutes.`;
|
||||
return t(Keys.splashScreen.sampleDataDialog.createPrompt, { containerName });
|
||||
|
||||
case "creating":
|
||||
return <Spinner size="small" labelPosition="above" label={`Creating container "${containerName}"...`} />;
|
||||
return (
|
||||
<Spinner
|
||||
size="small"
|
||||
labelPosition="above"
|
||||
label={t(Keys.splashScreen.sampleDataDialog.creatingContainer, { containerName })}
|
||||
/>
|
||||
);
|
||||
case "importing":
|
||||
return <Spinner size="small" labelPosition="above" label={`Importing data into "${containerName}"...`} />;
|
||||
return (
|
||||
<Spinner
|
||||
size="small"
|
||||
labelPosition="above"
|
||||
label={t(Keys.splashScreen.sampleDataDialog.importingData, { containerName })}
|
||||
/>
|
||||
);
|
||||
case "completed":
|
||||
return `Successfully created "${containerName}" with sample data.`;
|
||||
return t(Keys.splashScreen.sampleDataDialog.success, { containerName });
|
||||
case "error":
|
||||
return (
|
||||
<div style={{ color: "red" }}>
|
||||
@@ -132,14 +154,14 @@ export const SampleDataImportDialog: React.FC<{
|
||||
const getButtonLabel = () => {
|
||||
switch (status) {
|
||||
case "idle":
|
||||
return "Start";
|
||||
return t(Keys.splashScreen.sampleDataDialog.startButton);
|
||||
case "creating":
|
||||
case "importing":
|
||||
return "Close";
|
||||
return t(Keys.common.close);
|
||||
case "completed":
|
||||
return "Close";
|
||||
return t(Keys.common.close);
|
||||
case "error":
|
||||
return "Close";
|
||||
return t(Keys.common.close);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -147,7 +169,7 @@ export const SampleDataImportDialog: React.FC<{
|
||||
<Dialog open={props.open} onOpenChange={(event, data) => props.setOpen(data.open)}>
|
||||
<DialogSurface>
|
||||
<DialogBody>
|
||||
<DialogTitle>Sample Data</DialogTitle>
|
||||
<DialogTitle>{t(Keys.splashScreen.sampleDataDialog.title)}</DialogTitle>
|
||||
<DialogContent>
|
||||
<div className={styles.dialogContent}>{renderContent()}</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -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'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>
|
||||
|
||||
@@ -18,6 +18,8 @@ import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
|
||||
import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Keys } from "../../Localization/Keys.generated";
|
||||
import { t } from "../../Localization/t";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
@@ -57,7 +59,7 @@ export default class ConflictsTab extends TabsBase {
|
||||
|
||||
private _documentsIterator: MinimalQueryIterator;
|
||||
private _container: Explorer;
|
||||
private _acceptButtonLabel: ko.Observable<string> = ko.observable("Save");
|
||||
private _acceptButtonLabel: ko.Observable<string> = ko.observable(t(Keys.common.save));
|
||||
|
||||
constructor(options: ViewModels.ConflictsTabOptions) {
|
||||
super(options);
|
||||
@@ -213,9 +215,9 @@ export default class ConflictsTab extends TabsBase {
|
||||
this.selectedConflictContent.subscribe((newContent: string) => this._onEditorContentChange(newContent));
|
||||
|
||||
this.conflictOperation.subscribe((newOperationType: string) => {
|
||||
let operationLabel = "Save";
|
||||
let operationLabel = t(Keys.common.save);
|
||||
if (newOperationType === Constants.ConflictOperationType.Replace) {
|
||||
operationLabel = "Update";
|
||||
operationLabel = t(Keys.common.update);
|
||||
}
|
||||
|
||||
this._acceptButtonLabel(operationLabel);
|
||||
@@ -229,7 +231,7 @@ export default class ConflictsTab extends TabsBase {
|
||||
this._documentsIterator = this.createIterator();
|
||||
await this.loadNextPage();
|
||||
} catch (error) {
|
||||
useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error));
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.conflicts.refreshGridFailed), getErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,11 +259,11 @@ export default class ConflictsTab extends TabsBase {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkCancelModalDialog(
|
||||
"Unsaved changes",
|
||||
"Changes will be lost. Do you want to continue?",
|
||||
"OK",
|
||||
t(Keys.tabs.conflicts.unsavedChanges),
|
||||
t(Keys.tabs.conflicts.changesWillBeLost),
|
||||
t(Keys.common.ok),
|
||||
async () => await this.resolveConflict(),
|
||||
"Cancel",
|
||||
t(Keys.common.cancel),
|
||||
undefined,
|
||||
);
|
||||
} else {
|
||||
@@ -332,7 +334,7 @@ export default class ConflictsTab extends TabsBase {
|
||||
} catch (error) {
|
||||
this.isExecutionError(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
useDialog.getState().showOkModalDialog("Resolve conflict failed", errorMessage);
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.conflicts.resolveConflictFailed), errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.ResolveConflict,
|
||||
{
|
||||
@@ -386,7 +388,7 @@ export default class ConflictsTab extends TabsBase {
|
||||
} catch (error) {
|
||||
this.isExecutionError(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
useDialog.getState().showOkModalDialog("Delete conflict failed", errorMessage);
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.conflicts.deleteConflictFailed), errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.DeleteConflict,
|
||||
{
|
||||
@@ -617,7 +619,7 @@ export default class ConflictsTab extends TabsBase {
|
||||
}
|
||||
|
||||
if (this.discardButton.visible()) {
|
||||
const label = "Discard";
|
||||
const label = t(Keys.common.discard);
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
@@ -630,7 +632,7 @@ export default class ConflictsTab extends TabsBase {
|
||||
}
|
||||
|
||||
if (this.deleteButton.visible()) {
|
||||
const label = "Delete";
|
||||
const label = t(Keys.common.delete);
|
||||
buttons.push({
|
||||
iconSrc: DeleteIcon,
|
||||
iconAlt: label,
|
||||
|
||||
@@ -41,6 +41,8 @@ import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||
import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { QueryConstants } from "Shared/Constants";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
@@ -349,7 +351,7 @@ export const getTabsButtons = ({
|
||||
}
|
||||
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
const label = !isPreferredApiMongoDB ? "New Item" : "New Document";
|
||||
const label = !isPreferredApiMongoDB ? t(Keys.tabs.documents.newItem) : t(Keys.tabs.documents.newDocument);
|
||||
if (getNewDocumentButtonState(editorState).visible) {
|
||||
buttons.push({
|
||||
iconSrc: NewDocumentIcon,
|
||||
@@ -368,7 +370,7 @@ export const getTabsButtons = ({
|
||||
}
|
||||
|
||||
if (getSaveNewDocumentButtonState(editorState).visible) {
|
||||
const label = "Save";
|
||||
const label = t(Keys.common.save);
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
@@ -386,7 +388,7 @@ export const getTabsButtons = ({
|
||||
}
|
||||
|
||||
if (getDiscardNewDocumentChangesButtonState(editorState).visible) {
|
||||
const label = "Discard";
|
||||
const label = t(Keys.common.discard);
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
@@ -403,7 +405,7 @@ export const getTabsButtons = ({
|
||||
}
|
||||
|
||||
if (getSaveExistingDocumentButtonState(editorState).visible) {
|
||||
const label = "Update";
|
||||
const label = t(Keys.common.update);
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
@@ -421,7 +423,7 @@ export const getTabsButtons = ({
|
||||
}
|
||||
|
||||
if (getDiscardExistingDocumentChangesButtonState(editorState).visible) {
|
||||
const label = "Discard";
|
||||
const label = t(Keys.common.discard);
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
@@ -438,7 +440,7 @@ export const getTabsButtons = ({
|
||||
}
|
||||
|
||||
if (selectedRows.size > 0) {
|
||||
const label = "Delete";
|
||||
const label = t(Keys.common.delete);
|
||||
buttons.push({
|
||||
iconSrc: DeleteDocumentIcon,
|
||||
iconAlt: label,
|
||||
@@ -453,7 +455,7 @@ export const getTabsButtons = ({
|
||||
}
|
||||
|
||||
if (!isPreferredApiMongoDB) {
|
||||
const label = "Upload Item";
|
||||
const label = t(Keys.tabs.documents.uploadItem);
|
||||
buttons.push({
|
||||
id: UPLOAD_BUTTON_ID,
|
||||
iconSrc: UploadIcon,
|
||||
@@ -737,17 +739,18 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
} else if (result.statusCode >= 400) {
|
||||
newFailed.push(result.documentId);
|
||||
logConsoleError(
|
||||
`Failed to delete document ${result.documentId.id()} with status code ${result.statusCode}`,
|
||||
t(Keys.tabs.documents.deleteDocumentFailedLog, {
|
||||
documentId: result.documentId.id(),
|
||||
statusCode: result.statusCode,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
logConsoleInfo(`Successfully deleted ${newSuccessful.length} document(s)`);
|
||||
logConsoleInfo(t(Keys.tabs.documents.deleteSuccessLog, { count: newSuccessful.length }));
|
||||
|
||||
if (newThrottled.length > 0) {
|
||||
logConsoleError(
|
||||
`Failed to delete ${newThrottled.length} document(s) due to "Request too large" (429) error. Retrying...`,
|
||||
);
|
||||
logConsoleError(t(Keys.tabs.documents.deleteThrottledLog, { count: newThrottled.length }));
|
||||
}
|
||||
|
||||
// Update result of the bulk delete: method is called again, because the state variables changed
|
||||
@@ -789,7 +792,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
);
|
||||
let partitionKeyProperties = useMemo(() => {
|
||||
return partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) =>
|
||||
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""),
|
||||
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, "").replace(/["]+/g, ""),
|
||||
);
|
||||
}, [partitionKeyPropertyHeaders]);
|
||||
|
||||
@@ -917,11 +920,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkCancelModalDialog(
|
||||
"Unsaved changes",
|
||||
"Your unsaved changes will be lost. Do you want to continue?",
|
||||
"OK",
|
||||
t(Keys.tabs.documents.unsavedChanges),
|
||||
t(Keys.tabs.documents.unsavedChangesMessage),
|
||||
t(Keys.common.ok),
|
||||
onDiscard,
|
||||
"Cancel",
|
||||
t(Keys.common.cancel),
|
||||
onCancelDiscard,
|
||||
);
|
||||
} else {
|
||||
@@ -1011,7 +1014,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
(error) => {
|
||||
onExecutionErrorChange(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
useDialog.getState().showOkModalDialog("Create document failed", errorMessage);
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.createDocumentFailed), errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.CreateDocument,
|
||||
{
|
||||
@@ -1097,7 +1100,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
selectedDocumentId.partitionKeyValue = originalPartitionKeyValue;
|
||||
onExecutionErrorChange(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
useDialog.getState().showOkModalDialog("Update document failed", errorMessage);
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.updateDocumentFailed), errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.UpdateDocument,
|
||||
{
|
||||
@@ -1174,7 +1177,12 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
// Remove the check for systemKey, remove call to deleteNoSqlDocument(). deleteNoSqlDocuments() should
|
||||
// always be called for NoSQL.
|
||||
deletePromise = deleteNoSqlDocument(_collection, toDeleteDocumentIds[0]).then(() => {
|
||||
useDialog.getState().showOkModalDialog("Delete document", "Document successfully deleted.");
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
t(Keys.tabs.documents.deleteDocumentDialogTitle),
|
||||
t(Keys.tabs.documents.documentDeleted),
|
||||
);
|
||||
return [toDeleteDocumentIds[0]];
|
||||
});
|
||||
// ----------------------------------------------------------------------------------------------------
|
||||
@@ -1251,17 +1259,20 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
"Delete documents",
|
||||
`Some documents failed to delete due to a rate limiting error. Please try again later. To prevent this in the future, consider increasing the throughput on your container or database.`,
|
||||
t(Keys.tabs.documents.deleteDocumentsDialogTitle),
|
||||
t(Keys.tabs.documents.throttlingError),
|
||||
{
|
||||
linkText: "Learn More",
|
||||
linkText: t(Keys.common.learnMore),
|
||||
linkUrl: MONGO_THROTTLING_DOC_URL,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog("Delete documents", `Deleting document(s) failed (${error.message})`);
|
||||
.showOkModalDialog(
|
||||
t(Keys.tabs.documents.deleteDocumentsDialogTitle),
|
||||
t(Keys.tabs.documents.deleteFailed, { error: error.message }),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -1275,21 +1286,21 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
const isPlural = selectedRows.size > 1;
|
||||
const documentName = !isPreferredApiMongoDB
|
||||
? isPlural
|
||||
? `the selected ${selectedRows.size} items`
|
||||
: "the selected item"
|
||||
? t(Keys.tabs.documents.selectedItems, { count: selectedRows.size })
|
||||
: t(Keys.tabs.documents.selectedItem)
|
||||
: isPlural
|
||||
? `the selected ${selectedRows.size} documents`
|
||||
: "the selected document";
|
||||
const msg = `Are you sure you want to delete ${documentName}?`;
|
||||
? t(Keys.tabs.documents.selectedDocuments, { count: selectedRows.size })
|
||||
: t(Keys.tabs.documents.selectedDocument);
|
||||
const msg = t(Keys.tabs.documents.confirmDelete, { documentName });
|
||||
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkCancelModalDialog(
|
||||
"Confirm delete",
|
||||
t(Keys.tabs.documents.confirmDeleteTitle),
|
||||
msg,
|
||||
"Delete",
|
||||
t(Keys.common.delete),
|
||||
() => deleteDocuments(Array.from(selectedRows).map((index) => documentIds[index as number])),
|
||||
"Cancel",
|
||||
t(Keys.common.cancel),
|
||||
undefined,
|
||||
);
|
||||
}, [deleteDocuments, documentIds, isPreferredApiMongoDB, selectedRows]);
|
||||
@@ -1470,7 +1481,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
const partitionKey = _partitionKey || (_collection && _collection.partitionKey);
|
||||
const partitionKeyPropertyHeaders = _collection?.partitionKeyPropertyHeaders || partitionKey?.paths;
|
||||
const partitionKeyProperties = partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) =>
|
||||
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""),
|
||||
partitionKeyPropertyHeader
|
||||
.replace(/[/]+/g, ".")
|
||||
.substring(1)
|
||||
.replace(/[']+/g, "")
|
||||
.replace(/["]+/g, ""),
|
||||
);
|
||||
|
||||
return newDocumentId(rawDocument, partitionKeyProperties, partitionKeyValue);
|
||||
@@ -1819,8 +1834,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
|
||||
const partitionKeyProperty = partitionKeyProperties?.[0];
|
||||
if (partitionKeyProperty !== "_id" && !_hasShardKeySpecified(documentContent)) {
|
||||
const message = `The document is lacking the shard property: ${partitionKeyProperty}`;
|
||||
useDialog.getState().showOkModalDialog("Create document failed", message);
|
||||
const message = t(Keys.tabs.documents.missingShardProperty, { partitionKeyProperty });
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.createDocumentFailed), message);
|
||||
onExecutionErrorChange(true);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.CreateDocument,
|
||||
@@ -1831,7 +1846,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
},
|
||||
startKey,
|
||||
);
|
||||
Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab");
|
||||
Logger.logError(t(Keys.tabs.documents.missingShardKeyLog), "MongoDocumentsTab");
|
||||
throw new Error("Document without shard key");
|
||||
}
|
||||
|
||||
@@ -1874,7 +1889,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
(error) => {
|
||||
onExecutionErrorChange(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
useDialog.getState().showOkModalDialog("Create document failed", errorMessage);
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.createDocumentFailed), errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.CreateDocument,
|
||||
{
|
||||
@@ -1945,7 +1960,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
(error) => {
|
||||
onExecutionErrorChange(true);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
useDialog.getState().showOkModalDialog("Update document failed", errorMessage);
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.updateDocumentFailed), errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.UpdateDocument,
|
||||
{
|
||||
@@ -2054,7 +2069,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error));
|
||||
useDialog.getState().showOkModalDialog(t(Keys.tabs.documents.refreshGridFailed), getErrorMessage(error));
|
||||
}
|
||||
},
|
||||
[createIterator, filterContent],
|
||||
@@ -2066,18 +2081,17 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
* @returns 429 warning message
|
||||
*/
|
||||
const get429WarningMessageNoSql = (): string => {
|
||||
let message = 'Some delete requests failed due to a "Request too large" exception (429)';
|
||||
let message = t(Keys.tabs.documents.requestTooLargeBase);
|
||||
|
||||
if (bulkDeleteOperation.count === bulkDeleteProcess.successfulIds.length) {
|
||||
message += ", but were successfully retried.";
|
||||
message += ", " + t(Keys.tabs.documents.retriedSuccessfully);
|
||||
} else if (bulkDeleteMode === "inProgress" || bulkDeleteMode === "aborting") {
|
||||
message += ". Retrying now.";
|
||||
message += ". " + t(Keys.tabs.documents.retryingNow);
|
||||
} else {
|
||||
message += ".";
|
||||
}
|
||||
|
||||
return (message +=
|
||||
" To prevent this in the future, consider increasing the throughput on your container or database.");
|
||||
return (message += " " + t(Keys.tabs.documents.increaseThroughputTip));
|
||||
};
|
||||
|
||||
const onColumnSelectionChange = (newSelectedColumnIds: string[]): void => {
|
||||
@@ -2124,7 +2138,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
const nonBlankLastFilters = lastFilterContents.filter((filter) => filter.trim() !== "");
|
||||
if (nonBlankLastFilters.length > 0) {
|
||||
options.push({
|
||||
label: "Saved filters",
|
||||
label: t(Keys.tabs.documents.savedFilters),
|
||||
options: nonBlankLastFilters,
|
||||
});
|
||||
}
|
||||
@@ -2153,14 +2167,14 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
dropdownOptions={getFilterChoices()}
|
||||
placeholder={
|
||||
isPreferredApiMongoDB
|
||||
? "Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents."
|
||||
: "Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents."
|
||||
? t(Keys.tabs.documents.mongoFilterPlaceholder)
|
||||
: t(Keys.tabs.documents.sqlFilterPlaceholder)
|
||||
}
|
||||
title="Type a query predicate or choose one from the list."
|
||||
title={t(Keys.tabs.documents.filterTooltip)}
|
||||
value={filterContent}
|
||||
onChange={updateFilterContent}
|
||||
onKeyDown={onFilterKeyDown}
|
||||
bottomLink={{ text: "Learn more", url: DATA_EXPLORER_DOC_URL }}
|
||||
bottomLink={{ text: t(Keys.common.learnMore), url: DATA_EXPLORER_DOC_URL }}
|
||||
/>
|
||||
<Button
|
||||
appearance="primary"
|
||||
@@ -2176,10 +2190,12 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
}
|
||||
}}
|
||||
disabled={isExecuting && isPreferredApiMongoDB}
|
||||
aria-label={!isExecuting || isPreferredApiMongoDB ? "Apply filter" : "Cancel"}
|
||||
aria-label={
|
||||
!isExecuting || isPreferredApiMongoDB ? t(Keys.tabs.documents.applyFilter) : t(Keys.common.cancel)
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
{!isExecuting || isPreferredApiMongoDB ? "Apply Filter" : "Cancel"}
|
||||
{!isExecuting || isPreferredApiMongoDB ? t(Keys.tabs.documents.applyFilter) : t(Keys.common.cancel)}
|
||||
</Button>
|
||||
</div>
|
||||
<Allotment
|
||||
@@ -2223,14 +2239,14 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
</div>
|
||||
{tableContainerSizePx?.width >= calculateOffset(selectedColumnIds.length) + 200 && (
|
||||
<div
|
||||
title="Refresh"
|
||||
title={t(Keys.common.refresh)}
|
||||
className={styles.refreshBtn}
|
||||
role="button"
|
||||
onClick={() => refreshDocumentsGrid(false)}
|
||||
aria-label="Refresh"
|
||||
aria-label={t(Keys.common.refresh)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<img src={RefreshIcon} alt="Refresh" />
|
||||
<img src={RefreshIcon} alt={t(Keys.common.refresh)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -2243,7 +2259,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
onClick={() => loadNextPage(documentsIterator.iterator, false)}
|
||||
onKeyDown={onLoadMoreKeyInput}
|
||||
>
|
||||
Load more
|
||||
{t(Keys.tabs.documents.loadMore)}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
@@ -2255,7 +2271,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
language={"json"}
|
||||
content={selectedDocumentContent}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Document editor"}
|
||||
ariaLabel={t(Keys.tabs.documents.documentEditor)}
|
||||
lineNumbers={"on"}
|
||||
theme={"_theme"}
|
||||
onContentChanged={_onEditorContentChange}
|
||||
@@ -2263,7 +2279,9 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
/>
|
||||
)}
|
||||
{selectedRows.size > 1 && (
|
||||
<span style={{ margin: 10 }}>Number of selected documents: {selectedRows.size}</span>
|
||||
<span style={{ margin: 10 }}>
|
||||
{t(Keys.tabs.documents.numberOfSelectedDocuments, { count: selectedRows.size })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Allotment.Pane>
|
||||
@@ -2272,42 +2290,43 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
{bulkDeleteOperation && (
|
||||
<ProgressModalDialog
|
||||
isOpen={isBulkDeleteDialogOpen}
|
||||
dismissText="Abort"
|
||||
dismissText={t(Keys.tabs.documents.abort)}
|
||||
onDismiss={() => {
|
||||
setIsBulkDeleteDialogOpen(false);
|
||||
setBulkDeleteOperation(undefined);
|
||||
}}
|
||||
onCancel={() => setBulkDeleteMode("aborting")}
|
||||
title={`Deleting ${bulkDeleteOperation.count} document(s)`}
|
||||
message={`Successfully deleted ${bulkDeleteProcess.successfulIds.length} document(s).`}
|
||||
title={t(Keys.tabs.documents.deletingDocuments, { count: bulkDeleteOperation.count })}
|
||||
message={t(Keys.tabs.documents.deletedDocumentsSuccess, { count: bulkDeleteProcess.successfulIds.length })}
|
||||
maxValue={bulkDeleteOperation.count}
|
||||
value={bulkDeleteProcess.successfulIds.length}
|
||||
mode={bulkDeleteMode}
|
||||
>
|
||||
<div className={styles.deleteProgressContent}>
|
||||
{(bulkDeleteMode === "aborting" || bulkDeleteMode === "aborted") && (
|
||||
<div style={{ paddingBottom: tokens.spacingVerticalL }}>Deleting document(s) was aborted.</div>
|
||||
<div style={{ paddingBottom: tokens.spacingVerticalL }}>{t(Keys.tabs.documents.deleteAborted)}</div>
|
||||
)}
|
||||
{(bulkDeleteProcess.failedIds.length > 0 ||
|
||||
(bulkDeleteProcess.throttledIds.length > 0 && bulkDeleteMode !== "inProgress")) && (
|
||||
<MessageBar intent="error" style={{ marginBottom: tokens.spacingVerticalL }}>
|
||||
<MessageBarBody>
|
||||
<MessageBarTitle>Error</MessageBarTitle>
|
||||
Failed to delete{" "}
|
||||
{bulkDeleteMode === "inProgress"
|
||||
? bulkDeleteProcess.failedIds.length
|
||||
: bulkDeleteProcess.failedIds.length + bulkDeleteProcess.throttledIds.length}{" "}
|
||||
document(s).
|
||||
<MessageBarTitle>{t(Keys.tabs.documents.error)}</MessageBarTitle>
|
||||
{t(Keys.tabs.documents.failedToDeleteDocuments, {
|
||||
count:
|
||||
bulkDeleteMode === "inProgress"
|
||||
? bulkDeleteProcess.failedIds.length
|
||||
: bulkDeleteProcess.failedIds.length + bulkDeleteProcess.throttledIds.length,
|
||||
})}
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
)}
|
||||
{bulkDeleteProcess.hasBeenThrottled && (
|
||||
<MessageBar intent="warning">
|
||||
<MessageBarBody>
|
||||
<MessageBarTitle>Warning</MessageBarTitle>
|
||||
<MessageBarTitle>{t(Keys.tabs.documents.warning)}</MessageBarTitle>
|
||||
{get429WarningMessageNoSql()}{" "}
|
||||
<Link href={NO_SQL_THROTTLING_DOC_URL} target="_blank">
|
||||
Learn More
|
||||
{t(Keys.common.learnMore)}
|
||||
</Link>
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
|
||||
@@ -24,7 +24,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
|
||||
<InputDataList
|
||||
bottomLink={
|
||||
{
|
||||
"text": "Learn more",
|
||||
"text": "common.learnMore",
|
||||
"url": "https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer",
|
||||
}
|
||||
}
|
||||
@@ -44,20 +44,20 @@ 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."
|
||||
title="Type a query predicate or choose one from the list."
|
||||
placeholder="tabs.documents.sqlFilterPlaceholder"
|
||||
title="tabs.documents.filterTooltip"
|
||||
value=""
|
||||
/>
|
||||
<Button
|
||||
appearance="primary"
|
||||
aria-label="Apply filter"
|
||||
aria-label="tabs.documents.applyFilter"
|
||||
data-test="DocumentsTab/ApplyFilter"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
size="small"
|
||||
tabIndex={0}
|
||||
>
|
||||
Apply Filter
|
||||
tabs.documents.applyFilter
|
||||
</Button>
|
||||
</div>
|
||||
<Allotment
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { Component } from "react";
|
||||
import { configContext } from "../../../ConfigContext";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../UserContext";
|
||||
@@ -212,7 +214,7 @@ export default class MongoShellTabComponent extends Component<
|
||||
src={this.state.url}
|
||||
id={this.props.tabsBaseInstance.tabId}
|
||||
onLoad={(event) => this.setContentFocus(event)}
|
||||
title="Mongo Shell"
|
||||
title={t(Keys.tabs.mongoShell.title)}
|
||||
role="tabpanel"
|
||||
></iframe>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,8 @@ import { QueryTabStyles, useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles
|
||||
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
||||
import { useSelectedNode } from "Explorer/useSelectedNode";
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { Keys } from "Localization/Keys.generated";
|
||||
import { t } from "Localization/t";
|
||||
import { QueryConstants } from "Shared/Constants";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
@@ -315,7 +317,9 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
};
|
||||
|
||||
public onSaveQueryClick = (): void => {
|
||||
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />);
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(t(Keys.tabs.query.saveQuery), <SaveQueryPane explorer={this.props.collection.container} />);
|
||||
};
|
||||
|
||||
public launchQueryCopilotChat = (): void => {
|
||||
@@ -325,7 +329,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
public onSavedQueriesClick = (): void => {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this.props.collection.container} />);
|
||||
.openSidePanel(
|
||||
t(Keys.tabs.query.openSavedQueries),
|
||||
<BrowseQueriesPane explorer={this.props.collection.container} />,
|
||||
);
|
||||
};
|
||||
|
||||
public toggleResult(): void {
|
||||
@@ -473,7 +480,8 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
if (this.executeQueryButton.visible) {
|
||||
const label = this.state.selectedContent?.length > 0 ? "Execute Selection" : "Execute Query";
|
||||
const label =
|
||||
this.state.selectedContent?.length > 0 ? t(Keys.tabs.query.executeSelection) : t(Keys.tabs.query.executeQuery);
|
||||
buttons.push({
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: label,
|
||||
@@ -490,7 +498,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
|
||||
if (this.saveQueryButton.visible) {
|
||||
if (configContext.platform !== Platform.Fabric) {
|
||||
const label = "Save Query";
|
||||
const label = t(Keys.tabs.query.saveQuery);
|
||||
buttons.push({
|
||||
iconSrc: SaveQueryIcon,
|
||||
iconAlt: label,
|
||||
@@ -507,11 +515,11 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
|
||||
buttons.push({
|
||||
iconSrc: DownloadQueryIcon,
|
||||
iconAlt: "Download Query",
|
||||
iconAlt: t(Keys.tabs.query.downloadQuery),
|
||||
keyboardAction: KeyboardAction.DOWNLOAD_ITEM,
|
||||
onCommandClick: this.onDownloadQueryClick,
|
||||
commandButtonLabel: "Download Query",
|
||||
ariaLabel: "Download Query",
|
||||
commandButtonLabel: t(Keys.tabs.query.downloadQuery),
|
||||
ariaLabel: t(Keys.tabs.query.downloadQuery),
|
||||
hasPopup: false,
|
||||
disabled: !this.saveQueryButton.enabled,
|
||||
});
|
||||
@@ -568,7 +576,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
// }
|
||||
|
||||
if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) {
|
||||
const label = "Cancel query";
|
||||
const label = t(Keys.tabs.query.cancelQuery);
|
||||
buttons.push({
|
||||
iconSrc: CancelQueryIcon,
|
||||
iconAlt: label,
|
||||
@@ -589,23 +597,23 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
const verticalButton: CommandButtonComponentProps = {
|
||||
isSelected: this.state.queryResultsView === SplitterDirection.Vertical,
|
||||
iconSrc: this.state.queryResultsView === SplitterDirection.Vertical ? CheckIcon : undefined,
|
||||
commandButtonLabel: "Vertical",
|
||||
ariaLabel: "Vertical",
|
||||
commandButtonLabel: t(Keys.tabs.query.vertical),
|
||||
ariaLabel: t(Keys.tabs.query.vertical),
|
||||
onCommandClick: () => this._setViewLayout(SplitterDirection.Vertical),
|
||||
hasPopup: false,
|
||||
};
|
||||
const horizontalButton: CommandButtonComponentProps = {
|
||||
isSelected: this.state.queryResultsView === SplitterDirection.Horizontal,
|
||||
iconSrc: this.state.queryResultsView === SplitterDirection.Horizontal ? CheckIcon : undefined,
|
||||
commandButtonLabel: "Horizontal",
|
||||
ariaLabel: "Horizontal",
|
||||
commandButtonLabel: t(Keys.tabs.query.horizontal),
|
||||
ariaLabel: t(Keys.tabs.query.horizontal),
|
||||
onCommandClick: () => this._setViewLayout(SplitterDirection.Horizontal),
|
||||
hasPopup: false,
|
||||
};
|
||||
|
||||
return {
|
||||
commandButtonLabel: "View",
|
||||
ariaLabel: "View",
|
||||
commandButtonLabel: t(Keys.tabs.query.view),
|
||||
ariaLabel: t(Keys.tabs.query.view),
|
||||
hasPopup: true,
|
||||
children: [verticalButton, horizontalButton],
|
||||
};
|
||||
@@ -782,7 +790,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
||||
modelMarkers={this.state.modelMarkers}
|
||||
isReadOnly={false}
|
||||
wordWrap={"on"}
|
||||
ariaLabel={"Editing Query"}
|
||||
ariaLabel={t(Keys.tabs.query.editingQuery)}
|
||||
lineNumbers={"on"}
|
||||
theme={this.props.monacoTheme}
|
||||
onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
|
||||
|
||||
@@ -11,6 +11,8 @@ import { createStoredProcedure } from "../../../Common/dataAccess/createStoredPr
|
||||
import { ExecuteSprocResult } from "../../../Common/dataAccess/executeStoredProcedure";
|
||||
import { updateStoredProcedure } from "../../../Common/dataAccess/updateStoredProcedure";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Keys } from "../../../Localization/Keys.generated";
|
||||
import { t } from "../../../Localization/t";
|
||||
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
@@ -133,7 +135,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
this.collection = this.props.collection;
|
||||
this.executeResultsEditorId = `executestoredprocedureresults${this.props.scriptTabBaseInstance.tabId}`;
|
||||
this.executeLogsEditorId = `executestoredprocedurelogs${this.props.scriptTabBaseInstance.tabId}`;
|
||||
this.props.scriptTabBaseInstance.ariaLabel("Stored Procedure Body");
|
||||
this.props.scriptTabBaseInstance.ariaLabel(t(Keys.tabs.storedProcedure.body));
|
||||
|
||||
this.props.iStorProcTabComponentAccessor({
|
||||
onExecuteSprocsResultEvent: this.onExecuteSprocsResult.bind(this),
|
||||
@@ -318,7 +320,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
const label = "Save";
|
||||
const label = t(Keys.common.save);
|
||||
if (this.state.saveButton.visible) {
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
@@ -333,7 +335,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
}
|
||||
|
||||
if (this.state.updateButton.visible) {
|
||||
const label = "Update";
|
||||
const label = t(Keys.common.update);
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
@@ -347,7 +349,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
}
|
||||
|
||||
if (this.state.discardButton.visible) {
|
||||
const label = "Discard";
|
||||
const label = t(Keys.common.discard);
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
@@ -361,7 +363,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
}
|
||||
|
||||
if (this.state.executeButton.visible) {
|
||||
const label = "Execute";
|
||||
const label = t(Keys.common.execute);
|
||||
buttons.push({
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: label,
|
||||
@@ -519,7 +521,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
<div className="tab-pane flexContainer stored-procedure-tab" role="tabpanel">
|
||||
<div className="storedTabForm flexContainer">
|
||||
<div className="formTitleFirst">
|
||||
Stored Procedure Id
|
||||
{t(Keys.tabs.storedProcedure.id)}
|
||||
<span className="mandatoryStar" style={{ color: "#ff0707", fontSize: "14px", fontWeight: "bold" }}>
|
||||
*
|
||||
</span>
|
||||
@@ -531,28 +533,28 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
required
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
aria-label="Stored procedure id"
|
||||
placeholder="Enter the new stored procedure id"
|
||||
aria-label={t(Keys.tabs.storedProcedure.idAriaLabel)}
|
||||
placeholder={t(Keys.tabs.storedProcedure.idPlaceholder)}
|
||||
size={40}
|
||||
value={this.state.id}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => this.handleIdOnChange(event)}
|
||||
/>
|
||||
</span>
|
||||
<div className="spUdfTriggerHeader">Stored Procedure Body</div>
|
||||
<div className="spUdfTriggerHeader">{t(Keys.tabs.storedProcedure.body)}</div>
|
||||
<EditorReact
|
||||
language={"javascript"}
|
||||
content={this.state.sProcEditorContent}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Stored procedure body"}
|
||||
ariaLabel={t(Keys.tabs.storedProcedure.bodyAriaLabel)}
|
||||
lineNumbers={"on"}
|
||||
theme={"_theme"}
|
||||
onContentChanged={(newContent: string) => this.onChangeContent(newContent)}
|
||||
/>
|
||||
{this.state.hasResults && (
|
||||
<div className="results-container">
|
||||
<Pivot aria-label="Successful execution of stored procedure" style={{ height: "100%" }}>
|
||||
<Pivot aria-label={t(Keys.tabs.storedProcedure.successfulExecution)} style={{ height: "100%" }}>
|
||||
<PivotItem
|
||||
headerText="Result"
|
||||
headerText={t(Keys.common.result)}
|
||||
headerButtonProps={{
|
||||
"data-order": 1,
|
||||
"data-title": "Result",
|
||||
@@ -563,11 +565,11 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
language={"javascript"}
|
||||
content={this.state.resultData}
|
||||
isReadOnly={true}
|
||||
ariaLabel={"Execute stored procedure result"}
|
||||
ariaLabel={t(Keys.tabs.storedProcedure.resultAriaLabel)}
|
||||
/>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="console.log"
|
||||
headerText={t(Keys.tabs.storedProcedure.consoleLogTab)}
|
||||
headerButtonProps={{
|
||||
"data-order": 2,
|
||||
"data-title": "console.log",
|
||||
@@ -578,7 +580,7 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
language={"javascript"}
|
||||
content={this.state.logsData}
|
||||
isReadOnly={true}
|
||||
ariaLabel={"Execute stored procedure logs"}
|
||||
ariaLabel={t(Keys.tabs.storedProcedure.logsAriaLabel)}
|
||||
/>
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
@@ -586,16 +588,16 @@ export default class StoredProcedureTabComponent extends React.Component<
|
||||
)}
|
||||
{this.state.hasErrors && (
|
||||
<div className="errors-container">
|
||||
<div className="errors-header">Errors:</div>
|
||||
<div className="errors-header">{t(Keys.tabs.storedProcedure.errors)}</div>
|
||||
<div className="errorContent">
|
||||
<span className="errorMessage">{this.state.error}</span>
|
||||
<span className="errorDetailsLink">
|
||||
<a
|
||||
aria-label="Error details link"
|
||||
aria-label={t(Keys.tabs.storedProcedure.errorDetailsAriaLabel)}
|
||||
onClick={() => this.onErrorDetailsClick()}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) => this.onErrorDetailsKeyPress(event)}
|
||||
>
|
||||
More details
|
||||
{t(Keys.tabs.storedProcedure.moreDetails)}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,8 @@ import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"
|
||||
import { createTrigger } from "../../Common/dataAccess/createTrigger";
|
||||
import { updateTrigger } from "../../Common/dataAccess/updateTrigger";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Keys } from "../../Localization/Keys.generated";
|
||||
import { t } from "../../Localization/t";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
@@ -19,8 +21,8 @@ import { EditorReact } from "../Controls/Editor/EditorReact";
|
||||
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import TriggerTab from "./TriggerTab";
|
||||
const triggerTypeOptions: IDropdownOption[] = [
|
||||
{ key: "Pre", text: "Pre" },
|
||||
{ key: "Post", text: "Post" },
|
||||
{ key: "Pre", text: t(Keys.tabs.trigger.pre) },
|
||||
{ key: "Post", text: t(Keys.tabs.trigger.post) },
|
||||
];
|
||||
|
||||
const dropdownStyles: Partial<IDropdownStyles> = {
|
||||
@@ -147,10 +149,10 @@ const dropdownStyles: Partial<IDropdownStyles> = {
|
||||
};
|
||||
|
||||
const triggerOperationOptions: IDropdownOption[] = [
|
||||
{ key: "All", text: "All" },
|
||||
{ key: "Create", text: "Create" },
|
||||
{ key: "Delete", text: "Delete" },
|
||||
{ key: "Replace", text: "Replace" },
|
||||
{ key: "All", text: t(Keys.tabs.trigger.all) },
|
||||
{ key: "Create", text: t(Keys.tabs.trigger.operationCreate) },
|
||||
{ key: "Delete", text: t(Keys.tabs.trigger.operationDelete) },
|
||||
{ key: "Replace", text: t(Keys.tabs.trigger.operationReplace) },
|
||||
];
|
||||
|
||||
interface Ibutton {
|
||||
@@ -334,7 +336,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
const label = "Save";
|
||||
const label = t(Keys.common.save);
|
||||
if (this.saveButton.visible) {
|
||||
buttons.push({
|
||||
setState: this.setState,
|
||||
@@ -351,7 +353,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
}
|
||||
|
||||
if (this.updateButton.visible) {
|
||||
const label = "Update";
|
||||
const label = t(Keys.common.update);
|
||||
buttons.push({
|
||||
...this,
|
||||
iconSrc: SaveIcon,
|
||||
@@ -366,7 +368,7 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
}
|
||||
|
||||
if (this.discardButton.visible) {
|
||||
const label = "Discard";
|
||||
const label = t(Keys.common.discard);
|
||||
buttons.push({
|
||||
setState: this.setState,
|
||||
...this,
|
||||
@@ -415,14 +417,14 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
<div className="tab-pane flexContainer trigger-form" role="tabpanel">
|
||||
<TextField
|
||||
className="trigger-field"
|
||||
label="Trigger Id"
|
||||
label={t(Keys.tabs.trigger.id)}
|
||||
id="entityTimeId"
|
||||
autoFocus
|
||||
required
|
||||
type="text"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder="Enter the new trigger id"
|
||||
placeholder={t(Keys.tabs.trigger.idPlaceholder)}
|
||||
size={40}
|
||||
value={triggerId}
|
||||
readOnly={!isIdEditable}
|
||||
@@ -446,8 +448,8 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
}}
|
||||
/>
|
||||
<Dropdown
|
||||
placeholder="Trigger Type"
|
||||
label="Trigger Type"
|
||||
placeholder={t(Keys.tabs.trigger.type)}
|
||||
label={t(Keys.tabs.trigger.type)}
|
||||
options={triggerTypeOptions}
|
||||
selectedKey={triggerType}
|
||||
className="trigger-field"
|
||||
@@ -455,8 +457,8 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
<Dropdown
|
||||
placeholder="Trigger Operation"
|
||||
label="Trigger Operation"
|
||||
placeholder={t(Keys.tabs.trigger.operation)}
|
||||
label={t(Keys.tabs.trigger.operation)}
|
||||
selectedKey={triggerOperation}
|
||||
options={triggerOperationOptions}
|
||||
className="trigger-field"
|
||||
@@ -465,12 +467,12 @@ export class TriggerTabContent extends Component<TriggerTab, ITriggerTabContentS
|
||||
}
|
||||
styles={dropdownStyles}
|
||||
/>
|
||||
<Label className="trigger-field">Trigger Body</Label>
|
||||
<Label className="trigger-field">{t(Keys.tabs.trigger.body)}</Label>
|
||||
<EditorReact
|
||||
language={"json"}
|
||||
content={triggerBody}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Graph JSON"}
|
||||
ariaLabel={t(Keys.tabs.trigger.bodyAriaLabel)}
|
||||
onContentChanged={this.handleTriggerBodyChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,8 @@ import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"
|
||||
import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
|
||||
import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Keys } from "../../Localization/Keys.generated";
|
||||
import { t } from "../../Localization/t";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
@@ -82,7 +84,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
const label = "Save";
|
||||
const label = t(Keys.common.save);
|
||||
if (this.saveButton.visible) {
|
||||
buttons.push({
|
||||
...this,
|
||||
@@ -99,7 +101,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
}
|
||||
|
||||
if (this.updateButton.visible) {
|
||||
const label = "Update";
|
||||
const label = t(Keys.common.update);
|
||||
buttons.push({
|
||||
...this,
|
||||
iconSrc: SaveIcon,
|
||||
@@ -114,7 +116,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
}
|
||||
|
||||
if (this.discardButton.visible) {
|
||||
const label = "Discard";
|
||||
const label = t(Keys.common.discard);
|
||||
buttons.push({
|
||||
setState: this.setState,
|
||||
...this,
|
||||
@@ -265,7 +267,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
<FluentProvider theme={currentTheme}>
|
||||
<TextField
|
||||
className="trigger-field"
|
||||
label="User Defined Function Id"
|
||||
label={t(Keys.tabs.udf.id)}
|
||||
id="entityTimeId"
|
||||
autoFocus
|
||||
required
|
||||
@@ -273,7 +275,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
type="text"
|
||||
pattern={ValidCosmosDbIdInputPattern.source}
|
||||
title={ValidCosmosDbIdDescription}
|
||||
placeholder="Enter the new user defined function id"
|
||||
placeholder={t(Keys.tabs.udf.idPlaceholder)}
|
||||
size={40}
|
||||
value={udfId}
|
||||
onChange={this.handleUdfIdChange}
|
||||
@@ -299,12 +301,12 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
}}
|
||||
/>{" "}
|
||||
</FluentProvider>
|
||||
<Label className="trigger-field">User Defined Function Body</Label>
|
||||
<Label className="trigger-field">{t(Keys.tabs.udf.body)}</Label>
|
||||
<EditorReact
|
||||
language={"javascript"}
|
||||
content={udfBody}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"User defined function body"}
|
||||
ariaLabel={t(Keys.tabs.udf.bodyAriaLabel)}
|
||||
onContentChanged={this.handleUdfBodyChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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(() => {});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "./i18n";
|
||||
import React, { useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import Arrow from "../images/Arrow.svg";
|
||||
|
||||
14
src/Localization/LocProject.json
Normal file
14
src/Localization/LocProject.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Projects": [
|
||||
{
|
||||
"LanguageSet": "Azure_LanguagesExt",
|
||||
"LocItems": [
|
||||
{
|
||||
"SourceFile": "src\\Localization\\en\\Resources.json",
|
||||
"CopyOption": "LangIDOnPath",
|
||||
"OutputPath": "src\\Localization"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
13
src/Localization/cs/Resources.json
Normal file
13
src/Localization/cs/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/de/Resources.json
Normal file
13
src/Localization/de/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
416
src/Localization/en/Resources.json
Normal file
416
src/Localization/en/Resources.json
Normal file
@@ -0,0 +1,416 @@
|
||||
{
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/es/Resources.json
Normal file
13
src/Localization/es/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/fr/Resources.json
Normal file
13
src/Localization/fr/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "Bienvenue dans Azure Cosmos DB",
|
||||
"postgres": "Bienvenue à Azure Cosmos DB for PostgreSQL",
|
||||
"vcoreMongo": "Bienvenue à Azure DocumentDB (avec compatibilité MongoDB)"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "Service de base de données multimodèle, mondialement distribuée et disponible à toute échelle",
|
||||
"getStarted": "Commencez avec nos jeux de données d’exemple, notre documentation et nos outils supplémentaires."
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/hu/Resources.json
Normal file
13
src/Localization/hu/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "Üdvözli az Azure Cosmos DB",
|
||||
"postgres": "Üdvözli az Azure Cosmos DB for PostgreSQL",
|
||||
"vcoreMongo": "Üdvözli az Azure DocumentDB (MongoDB-kompatibilitással)"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "Globálisan elosztott, többmodelles adatbázis-szolgáltatás bármilyen mérethez",
|
||||
"getStarted": "Ismerje meg a minta adathalmazok, a dokumentáció és a további eszközök használatának első lépéseit."
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/id/Resources.json
Normal file
13
src/Localization/id/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "Selamat Datang di Azure Cosmos DB",
|
||||
"postgres": "Selamat datang di Azure Cosmos DB for PostgreSQL",
|
||||
"vcoreMongo": "Selamat datang di Azure DocumentDB (dengan kompatibilitas MongoDB)"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "Layanan database multimodel yang didistribusikan secara global untuk skala apa saja",
|
||||
"getStarted": "Mulai dengan himpunan data sampel, dokumentasi, dan alat tambahan kami."
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/it/Resources.json
Normal file
13
src/Localization/it/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "Benvenuto in Azure Cosmos DB",
|
||||
"postgres": "Benvenuti in Azure Cosmos DB for PostgreSQL",
|
||||
"vcoreMongo": "Benvenuti in Azure DocumentDB (con compatibilità MongoDB)"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "Servizio database multimodello distribuito a livello globale a qualsiasi livello di scalabilità",
|
||||
"getStarted": "Inizia con i nostri set di dati di esempio, la documentazione e gli strumenti aggiuntivi."
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/ja/Resources.json
Normal file
13
src/Localization/ja/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "Azure Cosmos DB へようこそ",
|
||||
"postgres": "Azure Cosmos DB for PostgreSQL へようこそ",
|
||||
"vcoreMongo": "Azure DocumentDB (MongoDB 互換) へようこそ"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "あらゆるスケールに対応するグローバル分散型のマルチモデル データベース サーバー",
|
||||
"getStarted": "サンプル データセット、ドキュメント、追加ツールを使用して開始してください。"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/ko/Resources.json
Normal file
13
src/Localization/ko/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "Azure Cosmos DB 시작",
|
||||
"postgres": "Azure Cosmos DB for PostgreSQL 시작",
|
||||
"vcoreMongo": "Azure DocumentDB 시작(MongoDB 호환성 포함)"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "모든 규모에 대해 전역적으로 분산된 다중 모델 데이터베이스 서비스",
|
||||
"getStarted": "샘플 데이터 세트, 설명서 및 추가 도구를 시작하세요."
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/nl/Resources.json
Normal file
13
src/Localization/nl/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "Welkom bij Azure Cosmos DB",
|
||||
"postgres": "Welkom bij Azure Cosmos DB for PostgreSQL",
|
||||
"vcoreMongo": "Welkom bij Azure DocumentDB (met MongoDB-compatibiliteit)"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "Wereldwijd gedistribueerde, multi-modeldatabase-service voor elke schaalgrootte",
|
||||
"getStarted": "Ga aan de slag met onze voorbeelddatasets, documentatie en extra tools."
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/pl/Resources.json
Normal file
13
src/Localization/pl/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "Azure Cosmos DB — Zapraszamy!",
|
||||
"postgres": "Azure Cosmos DB for PostgreSQL — Zapraszamy!",
|
||||
"vcoreMongo": "Witamy w usłudze Azure DocumentDB (ze zgodnością z bazą danych MongoDB)"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "Globalnie rozproszona, wielomodelowa usługa bazy danych na dowolną skalę",
|
||||
"getStarted": "Rozpocznij pracę z naszymi przykładowymi zestawami danych, dokumentacją i dodatkowymi narzędziami."
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/pt-BR/Resources.json
Normal file
13
src/Localization/pt-BR/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "Bem-vindo ao Azure Cosmos DB",
|
||||
"postgres": "Bem-vindo ao Azure Cosmos DB for PostgreSQL",
|
||||
"vcoreMongo": "Bem-vindo ao Azure DocumentDB (com compatibilidade com MongoDB)"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "Serviço de multimodelo de banco de dados globalmente distribuído para qualquer escala",
|
||||
"getStarted": "Comece com nossos conjuntos de dados de exemplo, documentação e ferramentas adicionais."
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/pt-PT/Resources.json
Normal file
13
src/Localization/pt-PT/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "Bem-vindo ao Azure Cosmos DB",
|
||||
"postgres": "Bem-vindo ao Azure Cosmos DB para PostgreSQL",
|
||||
"vcoreMongo": "Bem-vindo ao Azure DocumentDB (com compatibilidade do MongoDB)"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "Serviço de base de dados com múltiplos modelos distribuído globalmente para qualquer dimensionamento",
|
||||
"getStarted": "Comece a trabalhar com os nossos conjuntos de dados de exemplo, documentação e ferramentas adicionais."
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/ru/Resources.json
Normal file
13
src/Localization/ru/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "Вас приветствует Azure Cosmos DB",
|
||||
"postgres": "Добро пожаловать в Azure Cosmos DB for PostgreSQL",
|
||||
"vcoreMongo": "Добро пожаловать в Azure DocumentDB (с совместимостью с MongoDB)"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "Глобально распределенная многомодельная служба базы данных для использования в любом масштабе",
|
||||
"getStarted": "Начните работу с нашими примерами наборов данных, документацией и дополнительными инструментами."
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/sv/Resources.json
Normal file
13
src/Localization/sv/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "Välkommen till Azure Cosmos DB",
|
||||
"postgres": "Välkommen till Azure Cosmos DB for PostgreSQL",
|
||||
"vcoreMongo": "Välkommen till Azure DocumentDB (med MongoDB-kompatibilitet)"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "Globalt distribuerad databas för flera datamodeller oavsett skala",
|
||||
"getStarted": "Kom igång med våra exempeldatamängder, dokumentation och extra verktyg."
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/Localization/t.ts
Normal file
24
src/Localization/t.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import i18n from "../i18n";
|
||||
import type enResources from "./en/Resources.json";
|
||||
|
||||
/**
|
||||
* Derives a union of all dot-notation key paths from a nested JSON object type.
|
||||
* e.g. { buttons: { save: "Save" } } → "buttons.save"
|
||||
*/
|
||||
type NestedKeyOf<T, P extends string = ""> = {
|
||||
[K in keyof T & string]: T[K] extends Record<string, unknown>
|
||||
? NestedKeyOf<T[K], P extends "" ? K : `${P}.${K}`>
|
||||
: P extends ""
|
||||
? K
|
||||
: `${P}.${K}`;
|
||||
}[keyof T & string];
|
||||
|
||||
/** All valid translation keys derived from en/Resources.json */
|
||||
export type ResourceKey = NestedKeyOf<typeof enResources>;
|
||||
|
||||
/**
|
||||
* Type-safe translation function bound to the "Resources" namespace.
|
||||
* Use this everywhere—class components, functional components, and non-React code.
|
||||
*/
|
||||
export const t = (key: ResourceKey, options?: Record<string, unknown>): string =>
|
||||
(i18n.t as (key: string, options?: unknown) => string)(key, { ns: "Resources", ...options });
|
||||
13
src/Localization/tr/Resources.json
Normal file
13
src/Localization/tr/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "Azure Cosmos DB'ye hoş geldiniz",
|
||||
"postgres": "Azure Cosmos DB for PostgreSQL'e hoş geldiniz",
|
||||
"vcoreMongo": "Azure DocumentDB'ye (MongoDB uyumluluğu ile) hoş geldiniz"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "Her ölçeğe uygun, global olarak dağıtılan çok modelli veritabanı hizmeti",
|
||||
"getStarted": "Örnek veri kümelerimizi, belgelerimizi ve ek araçlarımızı kullanmaya başlayın."
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/zh-Hans/Resources.json
Normal file
13
src/Localization/zh-Hans/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "欢迎使用 Azure Cosmos DB",
|
||||
"postgres": "欢迎使用 Azure Cosmos DB for PostgreSQL",
|
||||
"vcoreMongo": "欢迎使用 Azure DocumentDB (具有 MongoDB 兼容性)"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "任何规模的全球分布式多模型数据库服务",
|
||||
"getStarted": "开始使用我们的示例数据集、文档和其他工具。"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Localization/zh-Hant/Resources.json
Normal file
13
src/Localization/zh-Hant/Resources.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "歡迎使用 Azure Cosmos DB",
|
||||
"postgres": "歡迎使用 Azure Cosmos DB for PostgreSQL",
|
||||
"vcoreMongo": "歡迎使用 Azure DocumentDB (具 MongoDB 相容性)"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "適用於任何規模的全域散發、多模型資料庫服務",
|
||||
"getStarted": "開始使用我們的樣本資料集、文件和其他工具。"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,9 @@ const App = (): JSX.Element => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [explorer]);
|
||||
|
||||
// Track interactive phase for both ContainerCopyPanel and DivExplorer paths
|
||||
useInteractive(MetricScenario.ApplicationLoad, !!config);
|
||||
|
||||
if (!explorer) {
|
||||
return <LoadingExplorer />;
|
||||
}
|
||||
@@ -145,7 +148,6 @@ const App = (): JSX.Element => {
|
||||
const DivExplorer: React.FC<{ explorer: Explorer }> = ({ explorer }) => {
|
||||
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
|
||||
const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel);
|
||||
useInteractive(MetricScenario.ApplicationLoad);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
182
src/Metrics/ErrorClassification.test.ts
Normal file
182
src/Metrics/ErrorClassification.test.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { ARMError } from "../Utils/arm/request";
|
||||
import { isExpectedError } from "./ErrorClassification";
|
||||
|
||||
describe("ErrorClassification", () => {
|
||||
describe("isExpectedError", () => {
|
||||
describe("ARMError with expected codes", () => {
|
||||
it("returns true for AuthorizationFailed code", () => {
|
||||
const error = new ARMError("Authorization failed");
|
||||
error.code = "AuthorizationFailed";
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for Forbidden code", () => {
|
||||
const error = new ARMError("Forbidden");
|
||||
error.code = "Forbidden";
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for Unauthorized code", () => {
|
||||
const error = new ARMError("Unauthorized");
|
||||
error.code = "Unauthorized";
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for InvalidAuthenticationToken code", () => {
|
||||
const error = new ARMError("Invalid token");
|
||||
error.code = "InvalidAuthenticationToken";
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for ExpiredAuthenticationToken code", () => {
|
||||
const error = new ARMError("Token expired");
|
||||
error.code = "ExpiredAuthenticationToken";
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for numeric 401 code", () => {
|
||||
const error = new ARMError("Unauthorized");
|
||||
error.code = 401;
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for numeric 403 code", () => {
|
||||
const error = new ARMError("Forbidden");
|
||||
error.code = 403;
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for unexpected ARM error code", () => {
|
||||
const error = new ARMError("Internal error");
|
||||
error.code = "InternalServerError";
|
||||
expect(isExpectedError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for numeric 500 code", () => {
|
||||
const error = new ARMError("Server error");
|
||||
error.code = 500;
|
||||
expect(isExpectedError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MSAL AuthError with expected errorCodes", () => {
|
||||
it("returns true for popup_window_error", () => {
|
||||
const error = { errorCode: "popup_window_error", message: "Popup blocked" };
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for interaction_required", () => {
|
||||
const error = { errorCode: "interaction_required", message: "User interaction required" };
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for user_cancelled", () => {
|
||||
const error = { errorCode: "user_cancelled", message: "User cancelled" };
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for consent_required", () => {
|
||||
const error = { errorCode: "consent_required", message: "Consent required" };
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for login_required", () => {
|
||||
const error = { errorCode: "login_required", message: "Login required" };
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for no_account_error", () => {
|
||||
const error = { errorCode: "no_account_error", message: "No account" };
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for unexpected MSAL error code", () => {
|
||||
const error = { errorCode: "unknown_error", message: "Unknown" };
|
||||
expect(isExpectedError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTTP status codes", () => {
|
||||
it("returns true for error with status 401", () => {
|
||||
const error = { status: 401, message: "Unauthorized" };
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for error with status 403", () => {
|
||||
const error = { status: 403, message: "Forbidden" };
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for error with status 500", () => {
|
||||
const error = { status: 500, message: "Internal Server Error" };
|
||||
expect(isExpectedError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for error with status 404", () => {
|
||||
const error = { status: 404, message: "Not Found" };
|
||||
expect(isExpectedError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Firewall error message pattern", () => {
|
||||
it("returns true for firewall error in Error message", () => {
|
||||
const error = new Error("Request blocked by firewall");
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for IP not allowed error", () => {
|
||||
const error = new Error("Client IP address is not allowed");
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for ip not allowed (no 'address')", () => {
|
||||
const error = new Error("Your ip not allowed to access this resource");
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for string error with firewall", () => {
|
||||
expect(isExpectedError("firewall rules prevent access")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for case-insensitive firewall match", () => {
|
||||
const error = new Error("FIREWALL blocked request");
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for unrelated error message", () => {
|
||||
const error = new Error("Database connection failed");
|
||||
expect(isExpectedError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge cases", () => {
|
||||
it("returns false for null", () => {
|
||||
expect(isExpectedError(null)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for undefined", () => {
|
||||
expect(isExpectedError(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for empty object", () => {
|
||||
expect(isExpectedError({})).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for plain Error without expected patterns", () => {
|
||||
const error = new Error("Something went wrong");
|
||||
expect(isExpectedError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for string without firewall pattern", () => {
|
||||
expect(isExpectedError("Generic error occurred")).toBe(false);
|
||||
});
|
||||
|
||||
it("handles error with multiple matching criteria", () => {
|
||||
// ARMError with both code and firewall message
|
||||
const error = new ARMError("Request blocked by firewall");
|
||||
error.code = "Forbidden";
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
109
src/Metrics/ErrorClassification.ts
Normal file
109
src/Metrics/ErrorClassification.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { ARMError } from "../Utils/arm/request";
|
||||
|
||||
/**
|
||||
* Expected error codes that should not mark scenarios as unhealthy.
|
||||
* These represent expected failures like auth issues, permission errors, and user actions.
|
||||
*/
|
||||
|
||||
// ARM error codes (string)
|
||||
const EXPECTED_ARM_ERROR_CODES: Set<string> = new Set([
|
||||
"AuthorizationFailed",
|
||||
"Forbidden",
|
||||
"Unauthorized",
|
||||
"AuthenticationFailed",
|
||||
"InvalidAuthenticationToken",
|
||||
"ExpiredAuthenticationToken",
|
||||
"AuthorizationPermissionMismatch",
|
||||
]);
|
||||
|
||||
// HTTP status codes that indicate expected failures
|
||||
const EXPECTED_HTTP_STATUS_CODES: Set<number> = new Set([
|
||||
401, // Unauthorized
|
||||
403, // Forbidden
|
||||
]);
|
||||
|
||||
// MSAL error codes (string)
|
||||
const EXPECTED_MSAL_ERROR_CODES: Set<string> = new Set([
|
||||
"popup_window_error",
|
||||
"interaction_required",
|
||||
"user_cancelled",
|
||||
"consent_required",
|
||||
"login_required",
|
||||
"no_account_error",
|
||||
"monitor_window_timeout",
|
||||
"empty_window_error",
|
||||
]);
|
||||
|
||||
// Firewall error message pattern (only case where we check message content)
|
||||
const FIREWALL_ERROR_PATTERN = /firewall|ip\s*(address)?\s*(is\s*)?not\s*allowed/i;
|
||||
|
||||
/**
|
||||
* Interface for MSAL AuthError-like objects
|
||||
*/
|
||||
interface MsalAuthError {
|
||||
errorCode?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for errors with HTTP status
|
||||
*/
|
||||
interface HttpError {
|
||||
status?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an error is an expected failure that should not mark the scenario as unhealthy.
|
||||
*
|
||||
* Expected failures include:
|
||||
* - Authentication/authorization errors (user not logged in, permissions)
|
||||
* - Firewall blocking errors
|
||||
* - User-cancelled operations
|
||||
*
|
||||
* @param error - The error to classify
|
||||
* @returns true if the error is expected and should not affect health metrics
|
||||
*/
|
||||
export function isExpectedError(error: unknown): boolean {
|
||||
if (!error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check ARMError code
|
||||
if (error instanceof ARMError && error.code !== undefined) {
|
||||
if (typeof error.code === "string" && EXPECTED_ARM_ERROR_CODES.has(error.code)) {
|
||||
return true;
|
||||
}
|
||||
if (typeof error.code === "number" && EXPECTED_HTTP_STATUS_CODES.has(error.code)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for MSAL AuthError (has errorCode property)
|
||||
const msalError = error as MsalAuthError;
|
||||
if (msalError.errorCode && typeof msalError.errorCode === "string") {
|
||||
if (EXPECTED_MSAL_ERROR_CODES.has(msalError.errorCode)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check HTTP status on generic errors
|
||||
const httpError = error as HttpError;
|
||||
if (httpError.status && typeof httpError.status === "number") {
|
||||
if (EXPECTED_HTTP_STATUS_CODES.has(httpError.status)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for firewall error in message (the only message-based check)
|
||||
if (error instanceof Error && error.message) {
|
||||
if (FIREWALL_ERROR_PATTERN.test(error.message)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for string errors with firewall pattern
|
||||
if (typeof error === "string" && FIREWALL_ERROR_PATTERN.test(error)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -15,6 +15,11 @@ export const reportUnhealthy = (scenario: MetricScenario, platform: Platform, ap
|
||||
send({ platform, api, scenario, healthy: false });
|
||||
|
||||
const send = async (event: MetricEvent): Promise<Response> => {
|
||||
// Skip metrics emission during local development
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
return Promise.resolve(new Response(null, { status: 200 }));
|
||||
}
|
||||
|
||||
const url = createUri(configContext?.PORTAL_BACKEND_ENDPOINT, RELATIVE_PATH);
|
||||
const authHeader = getAuthorizationHeader();
|
||||
|
||||
|
||||
231
src/Metrics/ScenarioMonitor.test.ts
Normal file
231
src/Metrics/ScenarioMonitor.test.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import MetricScenario, { reportHealthy, reportUnhealthy } from "./MetricEvents";
|
||||
import { ApplicationMetricPhase, CommonMetricPhase } from "./ScenarioConfig";
|
||||
import { scenarioMonitor } from "./ScenarioMonitor";
|
||||
|
||||
// Mock the MetricEvents module
|
||||
jest.mock("./MetricEvents", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
ApplicationLoad: "ApplicationLoad",
|
||||
DatabaseLoad: "DatabaseLoad",
|
||||
},
|
||||
reportHealthy: jest.fn().mockResolvedValue({ ok: true }),
|
||||
reportUnhealthy: jest.fn().mockResolvedValue({ ok: true }),
|
||||
}));
|
||||
|
||||
// Mock configContext
|
||||
jest.mock("../ConfigContext", () => ({
|
||||
configContext: {
|
||||
platform: "Portal",
|
||||
PORTAL_BACKEND_ENDPOINT: "https://test.portal.azure.com",
|
||||
},
|
||||
Platform: {
|
||||
Portal: "Portal",
|
||||
Hosted: "Hosted",
|
||||
Emulator: "Emulator",
|
||||
Fabric: "Fabric",
|
||||
},
|
||||
}));
|
||||
|
||||
describe("ScenarioMonitor", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Use legacy fake timers to avoid conflicts with performance API
|
||||
jest.useFakeTimers({ legacyFakeTimers: true });
|
||||
|
||||
// Ensure performance mock is available (setupTests.ts sets this but fake timers may override)
|
||||
if (typeof performance.mark !== "function") {
|
||||
Object.defineProperty(global, "performance", {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: {
|
||||
mark: jest.fn(),
|
||||
measure: jest.fn(),
|
||||
clearMarks: jest.fn(),
|
||||
clearMeasures: jest.fn(),
|
||||
getEntriesByName: jest.fn().mockReturnValue([{ startTime: 0 }]),
|
||||
getEntriesByType: jest.fn().mockReturnValue([]),
|
||||
now: jest.fn(() => Date.now()),
|
||||
timeOrigin: Date.now(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Reset userContext
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
});
|
||||
|
||||
// Reset the scenario monitor to clear any previous state
|
||||
scenarioMonitor.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Reset scenarios before switching to real timers
|
||||
scenarioMonitor.reset();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe("markExpectedFailure", () => {
|
||||
it("sets hasExpectedFailure flag on active scenarios", () => {
|
||||
// Start a scenario
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Mark expected failure
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
|
||||
// Let timeout fire - should emit healthy because of expected failure
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(reportHealthy).toHaveBeenCalledWith(MetricScenario.ApplicationLoad, configContext.platform, "SQL");
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets flag on multiple active scenarios", () => {
|
||||
// Start two scenarios
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
scenarioMonitor.start(MetricScenario.DatabaseLoad);
|
||||
|
||||
// Mark expected failure - should affect both
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
|
||||
// Let timeouts fire
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(reportHealthy).toHaveBeenCalledTimes(2);
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not affect already emitted scenarios", () => {
|
||||
// Start scenario
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Complete all phases to emit
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, CommonMetricPhase.Interactive);
|
||||
|
||||
// Now mark expected failure - should not change anything
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
|
||||
// Healthy was called when phases completed
|
||||
expect(reportHealthy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeout behavior", () => {
|
||||
it("emits unhealthy on timeout without expected failure", () => {
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Let timeout fire without marking expected failure
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(reportUnhealthy).toHaveBeenCalledWith(MetricScenario.ApplicationLoad, configContext.platform, "SQL");
|
||||
expect(reportHealthy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits healthy on timeout with expected failure", () => {
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Mark expected failure
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
|
||||
// Let timeout fire
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(reportHealthy).toHaveBeenCalledWith(MetricScenario.ApplicationLoad, configContext.platform, "SQL");
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits healthy even with partial phase completion and expected failure", () => {
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Complete one phase
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
|
||||
// Mark expected failure
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
|
||||
// Let timeout fire (Interactive phase not completed)
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(reportHealthy).toHaveBeenCalled();
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("failPhase behavior", () => {
|
||||
it("emits unhealthy immediately on unexpected failure", () => {
|
||||
scenarioMonitor.start(MetricScenario.DatabaseLoad);
|
||||
|
||||
// Fail a phase (simulating unexpected error)
|
||||
scenarioMonitor.failPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabasesFetched);
|
||||
|
||||
// Should emit unhealthy immediately, not wait for timeout
|
||||
expect(reportUnhealthy).toHaveBeenCalledWith(MetricScenario.DatabaseLoad, configContext.platform, "SQL");
|
||||
});
|
||||
|
||||
it("does not emit twice after failPhase and timeout", () => {
|
||||
scenarioMonitor.start(MetricScenario.DatabaseLoad);
|
||||
|
||||
// Fail a phase
|
||||
scenarioMonitor.failPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabasesFetched);
|
||||
|
||||
// Let timeout fire
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
// Should only have emitted once (from failPhase)
|
||||
expect(reportUnhealthy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("completePhase behavior", () => {
|
||||
it("emits healthy when all phases complete", () => {
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Complete all required phases
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, CommonMetricPhase.Interactive);
|
||||
|
||||
expect(reportHealthy).toHaveBeenCalledWith(MetricScenario.ApplicationLoad, configContext.platform, "SQL");
|
||||
});
|
||||
|
||||
it("does not emit until all phases complete", () => {
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Complete only one phase
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
|
||||
expect(reportHealthy).not.toHaveBeenCalled();
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("scenario isolation", () => {
|
||||
it("expected failure on one scenario does not affect others after completion", () => {
|
||||
// Start both scenarios
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
scenarioMonitor.start(MetricScenario.DatabaseLoad);
|
||||
|
||||
// Complete ApplicationLoad
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, CommonMetricPhase.Interactive);
|
||||
|
||||
// Now mark expected failure - should only affect DatabaseLoad
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
|
||||
// Let DatabaseLoad timeout
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
// ApplicationLoad emitted healthy on completion
|
||||
// DatabaseLoad emits healthy on timeout (expected failure)
|
||||
expect(reportHealthy).toHaveBeenCalledTimes(2);
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@ interface InternalScenarioContext {
|
||||
phases: Map<MetricPhase, PhaseContext>; // Track start/end for each phase
|
||||
timeoutId?: number;
|
||||
emitted: boolean;
|
||||
hasExpectedFailure: boolean; // Flag for expected failures (auth, firewall, etc.)
|
||||
}
|
||||
|
||||
class ScenarioMonitor {
|
||||
@@ -55,6 +56,13 @@ class ScenarioMonitor {
|
||||
});
|
||||
}
|
||||
|
||||
private devLog(msg: string) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[Metrics] ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
start(scenario: MetricScenario) {
|
||||
if (this.contexts.has(scenario)) {
|
||||
return;
|
||||
@@ -75,6 +83,7 @@ class ScenarioMonitor {
|
||||
failed: new Set<MetricPhase>(),
|
||||
phases: new Map<MetricPhase, PhaseContext>(),
|
||||
emitted: false,
|
||||
hasExpectedFailure: false,
|
||||
};
|
||||
|
||||
// Start all required phases at scenario start time
|
||||
@@ -84,6 +93,10 @@ class ScenarioMonitor {
|
||||
ctx.phases.set(phase, { startMarkName: phaseStartMarkName });
|
||||
});
|
||||
|
||||
this.devLog(
|
||||
`scenario_start: ${scenario} | phases=${config.requiredPhases.join(", ")} | timeout=${config.timeoutMs}ms`,
|
||||
);
|
||||
|
||||
traceMark(Action.MetricsScenario, {
|
||||
event: "scenario_start",
|
||||
scenario,
|
||||
@@ -91,7 +104,28 @@ class ScenarioMonitor {
|
||||
timeoutMs: config.timeoutMs,
|
||||
});
|
||||
|
||||
ctx.timeoutId = window.setTimeout(() => this.emit(ctx, false, true), config.timeoutMs);
|
||||
ctx.timeoutId = window.setTimeout(() => {
|
||||
const missingPhases = ctx.config.requiredPhases.filter((p) => !ctx.completed.has(p));
|
||||
|
||||
this.devLog(
|
||||
`timeout: ${scenario} | missing=[${missingPhases.join(", ")}] | completed=[${Array.from(ctx.completed).join(
|
||||
", ",
|
||||
)}] | documentHidden=${document.hidden} | hasExpectedFailure=${ctx.hasExpectedFailure}`,
|
||||
);
|
||||
|
||||
traceMark(Action.MetricsScenario, {
|
||||
event: "scenario_timeout",
|
||||
scenario,
|
||||
missingPhases: missingPhases.join(","),
|
||||
completedPhases: Array.from(ctx.completed).join(","),
|
||||
documentHidden: document.hidden,
|
||||
hasExpectedFailure: ctx.hasExpectedFailure,
|
||||
});
|
||||
|
||||
// If an expected failure occurred (auth, firewall, etc.), emit healthy instead of unhealthy
|
||||
const healthy = ctx.hasExpectedFailure;
|
||||
this.emit(ctx, healthy, true);
|
||||
}, config.timeoutMs);
|
||||
this.contexts.set(scenario, ctx);
|
||||
}
|
||||
|
||||
@@ -130,6 +164,12 @@ class ScenarioMonitor {
|
||||
const endTimeISO = endEntry ? new Date(navigationStart + endEntry.startTime).toISOString() : undefined;
|
||||
const durationMs = startEntry && endEntry ? endEntry.startTime - startEntry.startTime : undefined;
|
||||
|
||||
this.devLog(
|
||||
`phase_complete: ${scenario}.${phase} | ${
|
||||
durationMs !== null && durationMs !== undefined ? `${Math.round(durationMs)}ms` : "?"
|
||||
} | ${ctx.completed.size}/${ctx.config.requiredPhases.length} phases`,
|
||||
);
|
||||
|
||||
traceSuccess(Action.MetricsScenario, {
|
||||
event: "phase_complete",
|
||||
scenario,
|
||||
@@ -149,6 +189,13 @@ class ScenarioMonitor {
|
||||
return;
|
||||
}
|
||||
|
||||
// If an expected failure was flagged (auth, firewall, etc.), treat as success.
|
||||
if (ctx.hasExpectedFailure) {
|
||||
this.devLog(`phase_fail: ${scenario}.${phase} — expected failure, completing as healthy`);
|
||||
this.completePhase(scenario, phase);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark the explicitly failed phase
|
||||
performance.mark(`scenario_${scenario}_${phase}_failed`);
|
||||
ctx.failed.add(phase);
|
||||
@@ -163,6 +210,12 @@ class ScenarioMonitor {
|
||||
// Build a snapshot with failure info
|
||||
const failureSnapshot = this.buildSnapshot(ctx, { final: false, timedOut: false });
|
||||
|
||||
this.devLog(
|
||||
`phase_fail: ${scenario}.${phase} | failed=[${Array.from(ctx.failed).join(", ")}] | completed=[${Array.from(
|
||||
ctx.completed,
|
||||
).join(", ")}]`,
|
||||
);
|
||||
|
||||
traceFailure(Action.MetricsScenario, {
|
||||
event: "phase_fail",
|
||||
scenario,
|
||||
@@ -171,10 +224,28 @@ class ScenarioMonitor {
|
||||
completedPhases: Array.from(ctx.completed).join(","),
|
||||
});
|
||||
|
||||
// Emit unhealthy immediately
|
||||
// Emit unhealthy immediately for unexpected failures
|
||||
this.emit(ctx, false, false, failureSnapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks that an expected failure occurred (auth, firewall, permissions, etc.).
|
||||
* When the scenario times out with this flag set, it will emit healthy instead of unhealthy.
|
||||
* This is called automatically from handleError when an expected error is detected.
|
||||
*/
|
||||
markExpectedFailure() {
|
||||
// Set the flag on all active (non-emitted) scenarios
|
||||
this.contexts.forEach((ctx) => {
|
||||
if (!ctx.emitted) {
|
||||
ctx.hasExpectedFailure = true;
|
||||
traceMark(Action.MetricsScenario, {
|
||||
event: "expected_failure_marked",
|
||||
scenario: ctx.scenario,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private tryEmitIfReady(ctx: InternalScenarioContext) {
|
||||
const allDone = ctx.config.requiredPhases.every((p) => ctx.completed.has(p));
|
||||
if (!allDone) {
|
||||
@@ -234,6 +305,7 @@ class ScenarioMonitor {
|
||||
scenario: ctx.scenario,
|
||||
healthy,
|
||||
timedOut,
|
||||
documentHidden: document.hidden,
|
||||
platform,
|
||||
api,
|
||||
durationMs: finalSnapshot.durationMs,
|
||||
@@ -246,8 +318,22 @@ class ScenarioMonitor {
|
||||
ttfb: finalSnapshot.vitals?.ttfb,
|
||||
});
|
||||
|
||||
this.devLog(
|
||||
`scenario_end: ${ctx.scenario} | ${healthy ? "healthy" : "unhealthy"} | ${
|
||||
timedOut ? "timed out" : `${Math.round(finalSnapshot.durationMs)}ms`
|
||||
} | ${JSON.stringify({
|
||||
completedPhases: finalSnapshot.completed.join(", "),
|
||||
failedPhases: finalSnapshot.failedPhases?.join(", ") || "none",
|
||||
platform,
|
||||
api,
|
||||
phaseTimings: finalSnapshot.phaseTimings,
|
||||
vitals: finalSnapshot.vitals,
|
||||
})}`,
|
||||
);
|
||||
|
||||
// Call portal backend health metrics endpoint
|
||||
if (healthy && !timedOut) {
|
||||
// If healthy is true (either completed successfully or timeout with expected failure), report healthy
|
||||
if (healthy) {
|
||||
reportHealthy(ctx.scenario, platform, api);
|
||||
} else {
|
||||
reportUnhealthy(ctx.scenario, platform, api);
|
||||
@@ -302,6 +388,19 @@ class ScenarioMonitor {
|
||||
phaseTimings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all scenarios (for testing purposes only).
|
||||
* Clears all active contexts and their timeouts.
|
||||
*/
|
||||
reset() {
|
||||
this.contexts.forEach((ctx) => {
|
||||
if (ctx.timeoutId) {
|
||||
clearTimeout(ctx.timeoutId);
|
||||
}
|
||||
});
|
||||
this.contexts.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export const scenarioMonitor = new ScenarioMonitor();
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import React from "react";
|
||||
import MetricScenario from "./MetricEvents";
|
||||
import { useMetricScenario } from "./MetricScenarioProvider";
|
||||
import { scenarioMonitor } from "./ScenarioMonitor";
|
||||
import { ApplicationMetricPhase, CommonMetricPhase } from "./ScenarioConfig";
|
||||
|
||||
/**
|
||||
* Hook to automatically complete the Interactive phase when the component becomes interactive.
|
||||
* Uses requestAnimationFrame to complete after the browser has painted.
|
||||
*
|
||||
* Calls scenarioMonitor directly (not via React context) so that the effect dependencies
|
||||
* are only [scenario, enabled] — both stable primitives. This prevents re-renders from
|
||||
* cancelling the pending rAF due to an unstable context function reference.
|
||||
*/
|
||||
export function useInteractive(scenario: MetricScenario) {
|
||||
const { completePhase } = useMetricScenario();
|
||||
|
||||
export function useInteractive(scenario: MetricScenario, enabled = true) {
|
||||
React.useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
completePhase(scenario, CommonMetricPhase.Interactive);
|
||||
if (!enabled) {
|
||||
return undefined;
|
||||
}
|
||||
const id = requestAnimationFrame(() => {
|
||||
scenarioMonitor.completePhase(scenario, CommonMetricPhase.Interactive);
|
||||
});
|
||||
}, [scenario, completePhase]);
|
||||
return () => cancelAnimationFrame(id);
|
||||
}, [scenario, enabled]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,18 +28,20 @@ export function useInteractive(scenario: MetricScenario) {
|
||||
* Tracks tree rendering and completes Interactive phase.
|
||||
* Only completes DatabaseTreeRendered if the database fetch was successful.
|
||||
* Note: Scenario must be started before databases are fetched (in refreshExplorer).
|
||||
*
|
||||
* Calls scenarioMonitor directly (not via React context) for the same stability reason
|
||||
* as useInteractive — avoids effect re-runs from unstable context function references.
|
||||
*/
|
||||
export function useDatabaseLoadScenario(databaseTreeNodes: unknown[], fetchSucceeded: boolean) {
|
||||
const { completePhase } = useMetricScenario();
|
||||
const hasCompletedTreeRenderRef = React.useRef(false);
|
||||
|
||||
// Track DatabaseTreeRendered phase (only if fetch succeeded)
|
||||
React.useEffect(() => {
|
||||
if (!hasCompletedTreeRenderRef.current && fetchSucceeded) {
|
||||
hasCompletedTreeRenderRef.current = true;
|
||||
completePhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabaseTreeRendered);
|
||||
scenarioMonitor.completePhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabaseTreeRendered);
|
||||
}
|
||||
}, [databaseTreeNodes, fetchSucceeded, completePhase]);
|
||||
}, [databaseTreeNodes, fetchSucceeded]);
|
||||
|
||||
// Track Interactive phase
|
||||
useInteractive(MetricScenario.DatabaseLoad);
|
||||
|
||||
@@ -37,7 +37,7 @@ const requestFabricToken = async (): Promise<void> => {
|
||||
|
||||
scheduleRefreshFabricToken();
|
||||
} catch (error) {
|
||||
logConsoleError(error as string);
|
||||
logConsoleError(error instanceof Error ? error.message : String(error));
|
||||
throw error;
|
||||
} finally {
|
||||
lastRequestTimestamp = undefined;
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import { TFunction } from "i18next";
|
||||
import promiseRetry, { AbortError, Options } from "p-retry";
|
||||
import React from "react";
|
||||
import { WithTranslation } from "react-i18next";
|
||||
@@ -81,7 +80,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
private smartUiGeneratorClassName: string;
|
||||
private retryIntervalInMs: number;
|
||||
private retryOptions: Options;
|
||||
private translationFunction: TFunction;
|
||||
private translationFunction: (key: string) => string;
|
||||
|
||||
componentDidMount(): void {
|
||||
this.performRefresh().then(() => {
|
||||
@@ -119,7 +118,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
this.retryOptions = { forever: true, maxTimeout: this.retryIntervalInMs, minTimeout: this.retryIntervalInMs };
|
||||
|
||||
// translation function passed to SelfServeComponent
|
||||
this.translationFunction = this.props.t;
|
||||
this.translationFunction = this.props.t as (key: string) => string;
|
||||
}
|
||||
|
||||
private onError = (hasErrors: boolean): void => {
|
||||
|
||||
@@ -8,6 +8,8 @@ import * as Logger from "../Common/Logger";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { isExpectedError } from "../Metrics/ErrorClassification";
|
||||
import { scenarioMonitor } from "../Metrics/ScenarioMonitor";
|
||||
import { trace, traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { UserContext, userContext } from "../UserContext";
|
||||
|
||||
@@ -127,6 +129,10 @@ export async function acquireMsalTokenForAccount(
|
||||
acquireTokenType: silent ? "silent" : "interactive",
|
||||
errorMessage: JSON.stringify(error),
|
||||
});
|
||||
// Mark expected failure for health metrics so timeout emits healthy
|
||||
if (isExpectedError(error)) {
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
@@ -169,7 +175,10 @@ export async function acquireTokenWithMsal(
|
||||
acquireTokenType: "interactive",
|
||||
errorMessage: JSON.stringify(interactiveError),
|
||||
});
|
||||
|
||||
// Mark expected failure for health metrics so timeout emits healthy
|
||||
if (isExpectedError(interactiveError)) {
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
}
|
||||
throw interactiveError;
|
||||
}
|
||||
} else {
|
||||
@@ -178,7 +187,10 @@ export async function acquireTokenWithMsal(
|
||||
acquireTokenType: "silent",
|
||||
errorMessage: JSON.stringify(silentError),
|
||||
});
|
||||
|
||||
// Mark expected failure for health metrics so timeout emits healthy
|
||||
if (isExpectedError(silentError)) {
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
}
|
||||
throw silentError;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,12 @@ import * as sinon from "sinon";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import * as QueryUtils from "./QueryUtils";
|
||||
import { defaultQueryFields, extractPartitionKeyValues, getValueForPath } from "./QueryUtils";
|
||||
import {
|
||||
defaultQueryFields,
|
||||
extractPartitionKeyValues,
|
||||
getValueForPath,
|
||||
stripDoubleQuotesFromSegment,
|
||||
} from "./QueryUtils";
|
||||
|
||||
const documentContent = {
|
||||
"Volcano Name": "Adams",
|
||||
@@ -279,5 +284,97 @@ describe("Query Utils", () => {
|
||||
expect(partitionKeyValues.length).toBe(2);
|
||||
expect(partitionKeyValues).toEqual([null, {}]);
|
||||
});
|
||||
|
||||
it("should extract partition key value when path has enclosing double quotes", () => {
|
||||
const docWithSpecialKey = {
|
||||
id: "test-id",
|
||||
"partition-key": "some-value",
|
||||
};
|
||||
|
||||
const partitionKeyDefinition: PartitionKeyDefinition = {
|
||||
kind: PartitionKeyKind.Hash,
|
||||
paths: ['/"partition-key"'],
|
||||
};
|
||||
|
||||
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(docWithSpecialKey, partitionKeyDefinition);
|
||||
expect(partitionKeyValues.length).toBe(1);
|
||||
expect(partitionKeyValues[0]).toEqual("some-value");
|
||||
});
|
||||
|
||||
it("should extract nested partition key value when path segments have enclosing double quotes", () => {
|
||||
const docWithSpecialKey = {
|
||||
id: "test-id",
|
||||
"my-field": {
|
||||
"sub-field": 42,
|
||||
},
|
||||
};
|
||||
|
||||
const partitionKeyDefinition: PartitionKeyDefinition = {
|
||||
kind: PartitionKeyKind.Hash,
|
||||
paths: ['/"my-field"/"sub-field"'],
|
||||
};
|
||||
|
||||
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(docWithSpecialKey, partitionKeyDefinition);
|
||||
expect(partitionKeyValues.length).toBe(1);
|
||||
expect(partitionKeyValues[0]).toEqual(42);
|
||||
});
|
||||
|
||||
it("should return {} for missing double-quoted partition key", () => {
|
||||
const docWithSpecialKey = {
|
||||
id: "test-id",
|
||||
};
|
||||
|
||||
const partitionKeyDefinition: PartitionKeyDefinition = {
|
||||
kind: PartitionKeyKind.Hash,
|
||||
paths: ['/"partition-key"'],
|
||||
};
|
||||
|
||||
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(docWithSpecialKey, partitionKeyDefinition);
|
||||
expect(partitionKeyValues.length).toBe(1);
|
||||
expect(partitionKeyValues[0]).toEqual({});
|
||||
});
|
||||
|
||||
it("should handle multi-hash with mixed quoted and unquoted paths", () => {
|
||||
const doc = {
|
||||
id: "test-id",
|
||||
Country: "Japan",
|
||||
"partition-key": "hello",
|
||||
};
|
||||
|
||||
const partitionKeyDefinition: PartitionKeyDefinition = {
|
||||
kind: PartitionKeyKind.MultiHash,
|
||||
paths: ["/Country", '/"partition-key"'],
|
||||
};
|
||||
|
||||
const partitionKeyValues: PartitionKey[] = extractPartitionKeyValues(doc, partitionKeyDefinition);
|
||||
expect(partitionKeyValues.length).toBe(2);
|
||||
expect(partitionKeyValues).toEqual(["Japan", "hello"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stripDoubleQuotesFromSegment", () => {
|
||||
it("should strip enclosing double quotes", () => {
|
||||
expect(stripDoubleQuotesFromSegment('"partition-key"')).toBe("partition-key");
|
||||
});
|
||||
|
||||
it("should not strip if only opening quote", () => {
|
||||
expect(stripDoubleQuotesFromSegment('"partition-key')).toBe('"partition-key');
|
||||
});
|
||||
|
||||
it("should not strip if only closing quote", () => {
|
||||
expect(stripDoubleQuotesFromSegment('partition-key"')).toBe('partition-key"');
|
||||
});
|
||||
|
||||
it("should return empty string when stripping quotes from empty quoted string", () => {
|
||||
expect(stripDoubleQuotesFromSegment('""')).toBe("");
|
||||
});
|
||||
|
||||
it("should not modify unquoted segments", () => {
|
||||
expect(stripDoubleQuotesFromSegment("Country")).toBe("Country");
|
||||
});
|
||||
|
||||
it("should not strip single quotes", () => {
|
||||
expect(stripDoubleQuotesFromSegment("'partition-key'")).toBe("'partition-key'");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,6 +116,17 @@ export const queryPagesUntilContentPresent = async (
|
||||
return await doRequest(firstItemIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* Strips enclosing double quotes from a partition key path segment.
|
||||
* e.g., '"partition-key"' -> 'partition-key'
|
||||
*/
|
||||
export const stripDoubleQuotesFromSegment = (segment: string): string => {
|
||||
if (segment.length >= 2 && segment.charAt(0) === '"' && segment.charAt(segment.length - 1) === '"') {
|
||||
return segment.slice(1, -1);
|
||||
}
|
||||
return segment;
|
||||
};
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export const getValueForPath = (content: any, pathSegments: string[]): any => {
|
||||
if (pathSegments.length === 0) {
|
||||
@@ -146,7 +157,7 @@ export const extractPartitionKeyValues = (
|
||||
const partitionKeyValues: PartitionKey[] = [];
|
||||
|
||||
partitionKeyDefinition.paths.forEach((partitionKeyPath: string) => {
|
||||
const pathSegments: string[] = partitionKeyPath.substring(1).split("/");
|
||||
const pathSegments: string[] = partitionKeyPath.substring(1).split("/").map(stripDoubleQuotesFromSegment);
|
||||
const value = getValueForPath(documentContent, pathSegments);
|
||||
|
||||
if (value !== undefined) {
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import i18n from "i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
import resourcesToBackend from "i18next-resources-to-backend";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(resourcesToBackend((lng: string, ns: string) => import(`./Localization/${lng}/${ns}.json`)))
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: "en",
|
||||
defaultNS: "Resources",
|
||||
ns: ["Resources"],
|
||||
detection: { order: ["navigator", "cookie", "localStorage", "sessionStorage", "querystring", "htmlTag"] },
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
keySeparator: ".",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
formatSeparator: ",",
|
||||
},
|
||||
react: {
|
||||
|
||||
@@ -4,9 +4,14 @@ import Adapter from "enzyme-adapter-react-16";
|
||||
import "jest-canvas-mock";
|
||||
import enableHooks from "jest-react-hooks-shallow";
|
||||
import { TextDecoder, TextEncoder } from "util";
|
||||
import i18n from "./i18n";
|
||||
import enResources from "./Localization/en/Resources.json";
|
||||
configure({ adapter: new Adapter() });
|
||||
initializeIcons();
|
||||
|
||||
// Load English translations synchronously so t() returns real values in tests
|
||||
i18n.addResourceBundle("en", "Resources", enResources, true, true);
|
||||
|
||||
if (typeof window.URL.createObjectURL === "undefined") {
|
||||
Object.defineProperty(window.URL, "createObjectURL", { value: () => {} });
|
||||
}
|
||||
|
||||
@@ -2,20 +2,54 @@ import { Page } from "@playwright/test";
|
||||
|
||||
export async function setupCORSBypass(page: Page) {
|
||||
await page.route("**/api/mongo/explorer{,/**}", async (route) => {
|
||||
const request = route.request();
|
||||
const origin = request.headers()["origin"];
|
||||
|
||||
// If there's no origin, it's not a CORS request. Let it proceed without modification.
|
||||
if (!origin) {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
//// Handle preflight (OPTIONS) requests separately.
|
||||
// These should not be forwarded to the target server.
|
||||
if (request.method() === "OPTIONS") {
|
||||
await route.fulfill({
|
||||
status: 204, // No Content
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": origin,
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS,HEAD",
|
||||
"Access-Control-Request-Headers": "*, x-ms-continuation",
|
||||
"Access-Control-Max-Age": "86400", // Cache preflight response for 1 day
|
||||
Vary: "Origin",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the actual GET/POST request
|
||||
const response = await route.fetch({
|
||||
headers: {
|
||||
...route.request().headers(),
|
||||
...request.headers(),
|
||||
},
|
||||
});
|
||||
|
||||
const responseHeaders = response.headers();
|
||||
// Clean up any pre-existing CORS headers from the real response to avoid conflicts.
|
||||
delete responseHeaders["access-control-allow-origin"];
|
||||
delete responseHeaders["access-control-allow-credentials"];
|
||||
|
||||
await route.fulfill({
|
||||
status: response.status(),
|
||||
headers: {
|
||||
...response.headers(),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "*",
|
||||
...responseHeaders,
|
||||
"Access-Control-Allow-Origin": origin,
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS,HEAD",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
"Access-Control-Allow-Credentials": "*",
|
||||
"Access-Control-Expose-Headers": "x-ms-continuation,x-ms-request-charge,x-ms-session-token",
|
||||
Vary: "Origin",
|
||||
},
|
||||
body: await response.body(),
|
||||
});
|
||||
|
||||
@@ -250,7 +250,7 @@ class TreeNode {
|
||||
// Try three times to wait for the node to expand.
|
||||
for (let i = 0; i < RETRY_COUNT; i++) {
|
||||
try {
|
||||
await tree.waitFor({ state: "visible" });
|
||||
await tree.waitFor({ state: "visible", timeout: 30000 });
|
||||
// The tree has expanded, let's get out of here
|
||||
return true;
|
||||
} catch {
|
||||
|
||||
132
test/mongo/pagination.spec.ts
Normal file
132
test/mongo/pagination.spec.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { setupCORSBypass } from "../CORSBypass";
|
||||
import { DataExplorer, QueryTab, TestAccount, CommandBarButton, Editor } from "../fx";
|
||||
import { serializeMongoToJson } from "../testData";
|
||||
|
||||
const databaseId = "test-e2etests-mongo-pagination";
|
||||
const collectionId = "test-coll-mongo-pagination";
|
||||
let explorer: DataExplorer = null!;
|
||||
|
||||
test.setTimeout(5 * 60 * 1000);
|
||||
|
||||
test.describe("Test Mongo Pagination", () => {
|
||||
let queryTab: QueryTab;
|
||||
let queryEditor: Editor;
|
||||
|
||||
test.beforeEach("Open query tab", async ({ page }) => {
|
||||
await setupCORSBypass(page);
|
||||
explorer = await DataExplorer.open(page, TestAccount.MongoReadonly);
|
||||
|
||||
const containerNode = await explorer.waitForContainerNode(databaseId, collectionId);
|
||||
await containerNode.expand();
|
||||
|
||||
const containerMenuNode = await explorer.waitForContainerDocumentsNode(databaseId, collectionId);
|
||||
await containerMenuNode.openContextMenu();
|
||||
await containerMenuNode.contextMenuItem("New Query").click();
|
||||
|
||||
queryTab = explorer.queryTab("tab0");
|
||||
queryEditor = queryTab.editor();
|
||||
await queryEditor.locator.waitFor({ timeout: 30 * 1000 });
|
||||
await queryTab.executeCTA.waitFor();
|
||||
await explorer.frame.getByTestId("NotificationConsole/ExpandCollapseButton").click();
|
||||
await explorer.frame.getByTestId("NotificationConsole/Contents").waitFor();
|
||||
});
|
||||
|
||||
test("should execute a query and load more results", async ({ page }) => {
|
||||
const query = "{}";
|
||||
|
||||
await queryEditor.locator.click();
|
||||
await queryEditor.setText(query);
|
||||
|
||||
const executeQueryButton = explorer.commandBarButton(CommandBarButton.ExecuteQuery);
|
||||
await executeQueryButton.click();
|
||||
|
||||
// Wait for query execution to complete
|
||||
await expect(queryTab.resultsView).toBeVisible({ timeout: 60000 });
|
||||
await expect(queryTab.resultsEditor.locator).toBeAttached({ timeout: 30000 });
|
||||
|
||||
// Get initial results
|
||||
const resultText = await queryTab.resultsEditor.text();
|
||||
|
||||
if (!resultText || resultText.trim() === "" || resultText.trim() === "[]") {
|
||||
throw new Error("Query returned no results - the collection appears to be empty");
|
||||
}
|
||||
|
||||
const resultData = serializeMongoToJson(resultText);
|
||||
|
||||
if (resultData.length === 0) {
|
||||
throw new Error("Parsed results contain 0 documents - collection is empty");
|
||||
}
|
||||
|
||||
if (resultData.length < 100) {
|
||||
expect(resultData.length).toBeGreaterThan(0);
|
||||
return;
|
||||
}
|
||||
|
||||
expect(resultData.length).toBe(100);
|
||||
|
||||
// Pagination test
|
||||
let totalPagesLoaded = 1;
|
||||
const maxLoadMoreAttempts = 10;
|
||||
|
||||
for (let loadMoreAttempts = 0; loadMoreAttempts < maxLoadMoreAttempts; loadMoreAttempts++) {
|
||||
const loadMoreButton = queryTab.resultsView.getByText("Load more");
|
||||
|
||||
try {
|
||||
await expect(loadMoreButton).toBeVisible({ timeout: 5000 });
|
||||
} catch {
|
||||
// Load more button not visible - pagination complete
|
||||
break;
|
||||
}
|
||||
|
||||
const beforeClickText = await queryTab.resultsEditor.text();
|
||||
const beforeClickHash = Buffer.from(beforeClickText || "")
|
||||
.toString("base64")
|
||||
.substring(0, 50);
|
||||
|
||||
await loadMoreButton.click();
|
||||
|
||||
// Wait for content to update
|
||||
let editorContentChanged = false;
|
||||
for (let waitAttempt = 1; waitAttempt <= 3; waitAttempt++) {
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const currentEditorText = await queryTab.resultsEditor.text();
|
||||
const currentHash = Buffer.from(currentEditorText || "")
|
||||
.toString("base64")
|
||||
.substring(0, 50);
|
||||
|
||||
if (currentHash !== beforeClickHash) {
|
||||
editorContentChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (editorContentChanged) {
|
||||
totalPagesLoaded++;
|
||||
} else {
|
||||
// No content change detected, stop pagination
|
||||
break;
|
||||
}
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
// Final verification
|
||||
const finalIndicator = queryTab.resultsView.locator("text=/\\d+ - \\d+/");
|
||||
const finalIndicatorText = await finalIndicator.textContent();
|
||||
|
||||
if (finalIndicatorText) {
|
||||
const match = finalIndicatorText.match(/(\d+) - (\d+)/);
|
||||
if (match) {
|
||||
const totalDocuments = parseInt(match[2]);
|
||||
expect(totalDocuments).toBe(405);
|
||||
expect(totalPagesLoaded).toBe(5);
|
||||
} else {
|
||||
throw new Error(`Invalid results indicator format: ${finalIndicatorText}`);
|
||||
}
|
||||
} else {
|
||||
expect(totalPagesLoaded).toBe(5);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
TestAccount,
|
||||
waitForApiResponse,
|
||||
} from "../../fx";
|
||||
import { createMultipleTestContainers } from "../../testData";
|
||||
import { createMultipleTestContainers, TestContainerContext } from "../../testData";
|
||||
|
||||
test.describe("Container Copy - Offline Migration", () => {
|
||||
let contexts: TestContainerContext[];
|
||||
let page: Page;
|
||||
let wrapper: Locator;
|
||||
let panel: Locator;
|
||||
@@ -22,7 +23,7 @@ test.describe("Container Copy - Offline Migration", () => {
|
||||
let expectedCopyJobNameInitial: string;
|
||||
|
||||
test.beforeEach("Setup for offline migration test", async ({ browser }) => {
|
||||
await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 });
|
||||
contexts = await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 });
|
||||
|
||||
page = await browser.newPage();
|
||||
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
|
||||
@@ -33,6 +34,7 @@ test.describe("Container Copy - Offline Migration", () => {
|
||||
test.afterEach("Cleanup after offline migration test", async () => {
|
||||
await page.unroute(/.*/, (route) => route.continue());
|
||||
await page.close();
|
||||
await Promise.all(contexts.map((context) => context?.dispose()));
|
||||
});
|
||||
|
||||
test("Successfully create and manage offline migration copy job", async () => {
|
||||
|
||||
@@ -7,9 +7,10 @@ import {
|
||||
TestAccount,
|
||||
waitForApiResponse,
|
||||
} from "../../fx";
|
||||
import { createMultipleTestContainers } from "../../testData";
|
||||
import { createMultipleTestContainers, TestContainerContext } from "../../testData";
|
||||
|
||||
test.describe("Container Copy - Online Migration", () => {
|
||||
let contexts: TestContainerContext[];
|
||||
let page: Page;
|
||||
let wrapper: Locator;
|
||||
let panel: Locator;
|
||||
@@ -17,7 +18,7 @@ test.describe("Container Copy - Online Migration", () => {
|
||||
let targetAccountName: string;
|
||||
|
||||
test.beforeEach("Setup for online migration test", async ({ browser }) => {
|
||||
await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 });
|
||||
contexts = await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 });
|
||||
|
||||
page = await browser.newPage();
|
||||
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
|
||||
@@ -27,6 +28,7 @@ test.describe("Container Copy - Online Migration", () => {
|
||||
test.afterEach("Cleanup after online migration test", async () => {
|
||||
await page.unroute(/.*/, (route) => route.continue());
|
||||
await page.close();
|
||||
await Promise.all(contexts.map((context) => context?.dispose()));
|
||||
});
|
||||
|
||||
test("Successfully create and manage online migration copy job", async () => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { expect, test } from "@playwright/test";
|
||||
|
||||
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
|
||||
import { CosmosClient, PermissionMode } from "@azure/cosmos";
|
||||
import { AzureIdentityCredentialAdapter } from "@azure/ms-rest-js";
|
||||
import {
|
||||
DataExplorer,
|
||||
TestAccount,
|
||||
@@ -18,8 +17,7 @@ test("SQL account using Resource token", async ({ page }) => {
|
||||
test.skip(nosqlAccountRbacToken.length > 0, "Resource tokens not supported when using data plane RBAC.");
|
||||
|
||||
const credentials = getAzureCLICredentials();
|
||||
const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
|
||||
const armClient = new CosmosDBManagementClient(adaptedCredentials, subscriptionId);
|
||||
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
|
||||
const accountName = getAccountName(TestAccount.SQL);
|
||||
const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
|
||||
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName);
|
||||
|
||||
127
test/sql/scaleAndSettings/dataMasking.spec.ts
Normal file
127
test/sql/scaleAndSettings/dataMasking.spec.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { DataExplorer, TestAccount } from "../../fx";
|
||||
import { createTestSQLContainer, TestContainerContext } from "../../testData";
|
||||
|
||||
/**
|
||||
* Tests for Dynamic Data Masking (DDM) feature.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - Test account must have the EnableDynamicDataMasking capability enabled
|
||||
* - If the capability is not enabled, the DataMaskingTab will not be visible and tests will be skipped
|
||||
*
|
||||
* Important Notes:
|
||||
* - Tests focus on enabling DDM and modifying the masking policy configuration
|
||||
*/
|
||||
|
||||
let testContainer: TestContainerContext;
|
||||
let DATABASE_ID: string;
|
||||
let CONTAINER_ID: string;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
testContainer = await createTestSQLContainer();
|
||||
DATABASE_ID = testContainer.database.id;
|
||||
CONTAINER_ID = testContainer.container.id;
|
||||
});
|
||||
|
||||
// Clean up test database after all tests
|
||||
test.afterAll(async () => {
|
||||
if (testContainer) {
|
||||
await testContainer.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to navigate to Data Masking tab
|
||||
async function navigateToDataMaskingTab(page: Page, explorer: DataExplorer): Promise<boolean> {
|
||||
// Refresh the tree to see the newly created database
|
||||
const refreshButton = explorer.frame.getByTestId("Sidebar/RefreshButton");
|
||||
await refreshButton.click();
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Expand database and container nodes
|
||||
const databaseNode = await explorer.waitForNode(DATABASE_ID);
|
||||
await databaseNode.expand();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const containerNode = await explorer.waitForNode(`${DATABASE_ID}/${CONTAINER_ID}`);
|
||||
await containerNode.expand();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Click Scale & Settings or Settings (depending on container type)
|
||||
let settingsNode = explorer.frame.getByTestId(`TreeNode:${DATABASE_ID}/${CONTAINER_ID}/Scale & Settings`);
|
||||
const isScaleAndSettings = await settingsNode.isVisible().catch(() => false);
|
||||
|
||||
if (!isScaleAndSettings) {
|
||||
settingsNode = explorer.frame.getByTestId(`TreeNode:${DATABASE_ID}/${CONTAINER_ID}/Settings`);
|
||||
}
|
||||
|
||||
await settingsNode.click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check if Data Masking tab is available
|
||||
const dataMaskingTab = explorer.frame.getByTestId("settings-tab-header/DataMaskingTab");
|
||||
const isTabVisible = await dataMaskingTab.isVisible().catch(() => false);
|
||||
|
||||
if (!isTabVisible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await dataMaskingTab.click();
|
||||
await page.waitForTimeout(1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
test.describe("Data Masking under Scale & Settings", () => {
|
||||
test("Data Masking tab should be visible and show JSON editor", async ({ page }) => {
|
||||
const explorer = await DataExplorer.open(page, TestAccount.SQL);
|
||||
const isTabAvailable = await navigateToDataMaskingTab(page, explorer);
|
||||
|
||||
if (!isTabAvailable) {
|
||||
test.skip(
|
||||
true,
|
||||
"Data Masking tab is not available. Test account may not have EnableDynamicDataMasking capability.",
|
||||
);
|
||||
}
|
||||
|
||||
// Verify the Data Masking editor is visible
|
||||
const dataMaskingEditor = explorer.frame.locator(".settingsV2Editor");
|
||||
await expect(dataMaskingEditor).toBeVisible();
|
||||
});
|
||||
|
||||
test("Data Masking editor should contain default policy structure", async ({ page }) => {
|
||||
const explorer = await DataExplorer.open(page, TestAccount.SQL);
|
||||
const isTabAvailable = await navigateToDataMaskingTab(page, explorer);
|
||||
|
||||
if (!isTabAvailable) {
|
||||
test.skip(
|
||||
true,
|
||||
"Data Masking tab is not available. Test account may not have EnableDynamicDataMasking capability.",
|
||||
);
|
||||
}
|
||||
|
||||
// Verify the editor contains the expected JSON structure fields
|
||||
const editorContent = explorer.frame.locator(".settingsV2Editor");
|
||||
await expect(editorContent).toBeVisible();
|
||||
|
||||
// Check that the editor contains key policy fields (default policy has empty arrays)
|
||||
await expect(editorContent).toContainText("includedPaths");
|
||||
await expect(editorContent).toContainText("excludedPaths");
|
||||
});
|
||||
|
||||
test("Data Masking editor should have correct default policy values", async ({ page }) => {
|
||||
const explorer = await DataExplorer.open(page, TestAccount.SQL);
|
||||
const isTabAvailable = await navigateToDataMaskingTab(page, explorer);
|
||||
|
||||
if (!isTabAvailable) {
|
||||
test.skip(
|
||||
true,
|
||||
"Data Masking tab is not available. Test account may not have EnableDynamicDataMasking capability.",
|
||||
);
|
||||
}
|
||||
|
||||
const editorContent = explorer.frame.locator(".settingsV2Editor");
|
||||
await expect(editorContent).toBeVisible();
|
||||
|
||||
// Default policy should have empty includedPaths and excludedPaths arrays
|
||||
await expect(editorContent).toContainText("[]");
|
||||
});
|
||||
});
|
||||
@@ -249,4 +249,27 @@ export const documentTestCases: DocumentTestCase[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Single Double-Quoted Partition Key",
|
||||
databaseId: "e2etests-sql-readonly",
|
||||
containerId: "doubleQuotedPartitionKey",
|
||||
documents: [
|
||||
{
|
||||
documentId: "doubleQuotedPartitionKey",
|
||||
partitionKeys: [{ key: "/partition-key", value: "doubleQuotedValue" }],
|
||||
},
|
||||
{
|
||||
documentId: "doubleQuotedPartitionKey_empty_string",
|
||||
partitionKeys: [{ key: "/partition-key", value: "" }],
|
||||
},
|
||||
{
|
||||
documentId: "doubleQuotedPartitionKey_null",
|
||||
partitionKeys: [{ key: "/partition-key", value: null }],
|
||||
},
|
||||
{
|
||||
documentId: "doubleQuotedPartitionKey_missing",
|
||||
partitionKeys: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
|
||||
import { BulkOperationType, Container, CosmosClient, CosmosClientOptions, Database, JSONObject } from "@azure/cosmos";
|
||||
import { AzureIdentityCredentialAdapter } from "@azure/ms-rest-js";
|
||||
|
||||
import {
|
||||
BulkOperationType,
|
||||
Container,
|
||||
CosmosClient,
|
||||
CosmosClientOptions,
|
||||
Database,
|
||||
ErrorResponse,
|
||||
JSONObject,
|
||||
} from "@azure/cosmos";
|
||||
import { Buffer } from "node:buffer";
|
||||
import { webcrypto } from "node:crypto";
|
||||
import {
|
||||
generateUniqueName,
|
||||
getAccountName,
|
||||
@@ -12,6 +18,7 @@ import {
|
||||
subscriptionId,
|
||||
TestAccount,
|
||||
} from "./fx";
|
||||
globalThis.crypto = webcrypto as Crypto;
|
||||
|
||||
export interface TestItem {
|
||||
id: string;
|
||||
@@ -60,8 +67,9 @@ function createTestItems(): TestItem[] {
|
||||
|
||||
// Document IDs cannot contain '/', '\', or '#'
|
||||
function createSafeRandomString(byteLength: number): string {
|
||||
return crypto
|
||||
.randomBytes(byteLength)
|
||||
const bytes = new Uint8Array(byteLength);
|
||||
crypto.getRandomValues(bytes);
|
||||
return Buffer.from(bytes)
|
||||
.toString("base64")
|
||||
.replace(/[/\\#]/g, "_");
|
||||
}
|
||||
@@ -78,7 +86,14 @@ export class TestContainerContext {
|
||||
) {}
|
||||
|
||||
async dispose() {
|
||||
await this.database.delete();
|
||||
try {
|
||||
await this.database.delete();
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.code === 404) {
|
||||
return; // Resource already deleted, ignore
|
||||
}
|
||||
throw error; // Re-throw other errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,8 +119,7 @@ async function createCosmosClientForSQLAccount(
|
||||
accountType: TestAccount.SQL | TestAccount.SQLContainerCopyOnly = TestAccount.SQL,
|
||||
): Promise<{ armClient: CosmosDBManagementClient; client: CosmosClient }> {
|
||||
const credentials = getAzureCLICredentials();
|
||||
const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
|
||||
const armClient = new CosmosDBManagementClient(adaptedCredentials, subscriptionId);
|
||||
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
|
||||
const accountName = getAccountName(accountType);
|
||||
const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
|
||||
|
||||
@@ -232,12 +246,19 @@ export async function createTestSQLContainer({
|
||||
}
|
||||
|
||||
export const setPartitionKeys = (partitionKeys: PartitionKey[]) => {
|
||||
const result = {};
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
partitionKeys.forEach((partitionKey) => {
|
||||
const { key: keyPath, value: keyValue } = partitionKey;
|
||||
const cleanPath = keyPath.startsWith("/") ? keyPath.slice(1) : keyPath;
|
||||
const keys = cleanPath.split("/");
|
||||
const keys = cleanPath.split("/").map((segment) => {
|
||||
// Strip enclosing double quotes from partition key path segments
|
||||
// e.g., '"partition-key"' -> 'partition-key'
|
||||
if (segment.length >= 2 && segment.charAt(0) === '"' && segment.charAt(segment.length - 1) === '"') {
|
||||
return segment.slice(1, -1);
|
||||
}
|
||||
return segment;
|
||||
});
|
||||
let current = result;
|
||||
|
||||
keys.forEach((key, index) => {
|
||||
@@ -245,7 +266,7 @@ export const setPartitionKeys = (partitionKeys: PartitionKey[]) => {
|
||||
current[key] = keyValue;
|
||||
} else {
|
||||
current[key] = current[key] || {};
|
||||
current = current[key];
|
||||
current = current[key] as Record<string, unknown>;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
158
utils/deployment-status/package-lock.json
generated
158
utils/deployment-status/package-lock.json
generated
@@ -1,66 +1,180 @@
|
||||
{
|
||||
"name": "deployment-status",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@types/color-name": {
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "deployment-status",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"moment": "^2.30.1",
|
||||
"node-fetch": "^3.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/color-name": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
|
||||
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
|
||||
"requires": {
|
||||
"dependencies": {
|
||||
"@types/color-name": "^1.1.1",
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"requires": {
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"requires": {
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"has-flag": {
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.27.0",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
|
||||
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ=="
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"deprecated": "Use your platform's native DOMException instead",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"node_modules/node-fetch": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
|
||||
"integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
|
||||
"requires": {
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"moment": "^2.27.0",
|
||||
"node-fetch": "^2.6.1"
|
||||
"moment": "^2.30.1",
|
||||
"node-fetch": "^3.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
71
utils/generateI18nKeys.mjs
Normal file
71
utils/generateI18nKeys.mjs
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Generates src/Localization/Keys.generated.ts from en/Resources.json.
|
||||
*
|
||||
* Every leaf value becomes its dot-notation key path, with JSDoc annotations
|
||||
* showing the English translation so developers see real text on hover.
|
||||
*
|
||||
* Libraries:
|
||||
* - values-to-keys — replaces translation values with dot-path keys
|
||||
* - i18next-resources-for-ts (json2ts) — serialises objects as typed `as const` TS
|
||||
*
|
||||
* Usage: node utils/generateI18nKeys.mjs
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { json2ts } from "i18next-resources-for-ts";
|
||||
import { dirname, resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { replace } from "values-to-keys";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = resolve(__dirname, "..");
|
||||
const INPUT = resolve(ROOT, "src/Localization/en/Resources.json");
|
||||
const OUTPUT = resolve(ROOT, "src/Localization/Keys.generated.ts");
|
||||
|
||||
// ── helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Walk two parallel objects (keyed + original) and produce TS source
|
||||
* with JSDoc comments showing the English value at every leaf.
|
||||
*/
|
||||
function serialiseWithJSDoc(obj, orig, indent = 2) {
|
||||
const pad = " ".repeat(indent);
|
||||
const lines = ["{"];
|
||||
for (const key of Object.keys(obj)) {
|
||||
const val = obj[key];
|
||||
const origVal = orig[key];
|
||||
if (typeof val === "object" && val !== null) {
|
||||
lines.push(`${pad}${key}: ${serialiseWithJSDoc(val, origVal, indent + 2)},`);
|
||||
} else {
|
||||
lines.push(`${pad}/** ${origVal} */`);
|
||||
lines.push(`${pad}${key}: ${JSON.stringify(val)},`);
|
||||
}
|
||||
}
|
||||
lines.push(`${" ".repeat(Math.max(0, indent - 2))}}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// ── main ───────────────────────────────────────────────────────────
|
||||
|
||||
// Keep the original English values for JSDoc annotations
|
||||
const original = JSON.parse(readFileSync(INPUT, "utf-8"));
|
||||
|
||||
// Use values-to-keys to replace every leaf value with its dot-path key
|
||||
const keyed = replace(JSON.parse(readFileSync(INPUT, "utf-8")));
|
||||
|
||||
// Use json2ts to verify the shape is valid for `as const` export
|
||||
// (We still use our own serialiser because json2ts doesn't add JSDoc comments)
|
||||
json2ts(keyed); // validates structure; throws on malformed input
|
||||
|
||||
const banner = `\
|
||||
// -----------------------------------------------------------------
|
||||
// THIS FILE IS AUTO-GENERATED — DO NOT EDIT BY HAND
|
||||
// Regenerate with: npm run generate:i18n-keys
|
||||
// -----------------------------------------------------------------
|
||||
`;
|
||||
|
||||
const body = `export const Keys = ${serialiseWithJSDoc(keyed, original)} as const;\n`;
|
||||
|
||||
writeFileSync(OUTPUT, banner + body, "utf-8");
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Generated ${OUTPUT}`);
|
||||
@@ -30,7 +30,7 @@
|
||||
<clear />
|
||||
<add name="X-Xss-Protection" value="1; mode=block" />
|
||||
<add name="X-Content-Type-Options" value="nosniff" />
|
||||
<add name="Content-Security-Policy" value="frame-ancestors 'self' portal.azure.com *.portal.azure.com portal.azure.us portal.azure.cn portal.microsoftazure.de df.onecloud.azure-test.net *.fabric.microsoft.com *.powerbi.com *.analysis-df.windows.net dataexplorer-preview.azurewebsites.net" />
|
||||
<add name="Content-Security-Policy" value="frame-ancestors 'self' portal.azure.com *.portal.azure.com portal.azure.us portal.azure.cn portal.microsoftazure.de df.onecloud.azure-test.net *.fabric.microsoft.com *.powerbi.com *.analysis-df.windows.net dataexplorer-preview.azurewebsites.net portal.sovcloud-azure.fr portal.sovcloud-azure.de portal.sovcloud-azure.sg" />
|
||||
</customHeaders>
|
||||
<redirectHeaders>
|
||||
<clear />
|
||||
|
||||
@@ -303,51 +303,53 @@ module.exports = function (_env = {}, argv = {}) {
|
||||
}
|
||||
});
|
||||
|
||||
// Only handle OPTIONS for /api (preflight)
|
||||
if (server?.app) {
|
||||
server.app.options("/api", (_req, res) => res.sendStatus(200));
|
||||
server.app.options("/api/*", (_req, res) => res.sendStatus(200));
|
||||
}
|
||||
|
||||
return middlewares;
|
||||
},
|
||||
proxy: {
|
||||
"/api": {
|
||||
proxy: [
|
||||
{
|
||||
context: ["/api"],
|
||||
target: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
|
||||
changeOrigin: true,
|
||||
logLevel: "debug",
|
||||
bypass: (req, res) => {
|
||||
if (req.method === "OPTIONS") {
|
||||
res.statusCode = 200;
|
||||
res.send();
|
||||
}
|
||||
},
|
||||
},
|
||||
"/proxy": {
|
||||
{
|
||||
context: ["/proxy"],
|
||||
target: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: "debug",
|
||||
pathRewrite: { "^/proxy": "" },
|
||||
router: (req) => {
|
||||
let newTarget = req.headers["x-ms-proxy-target"];
|
||||
return newTarget;
|
||||
},
|
||||
router: (req) => req.headers["x-ms-proxy-target"],
|
||||
},
|
||||
"/_explorer": {
|
||||
{
|
||||
context: ["/_explorer"],
|
||||
target: process.env.EMULATOR_ENDPOINT || "https://localhost:8081/",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: "debug",
|
||||
},
|
||||
"/explorerProxy": {
|
||||
{
|
||||
context: ["/explorerProxy"],
|
||||
target: process.env.EMULATOR_ENDPOINT || "https://localhost:8081/",
|
||||
pathRewrite: { "^/explorerProxy": "" },
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: "debug",
|
||||
},
|
||||
[`/${AZURE_TENANT_ID}`]: {
|
||||
{
|
||||
context: [`/${AZURE_TENANT_ID}`],
|
||||
target: "https://login.microsoftonline.com/",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
logLevel: "debug",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
stats: "minimal",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user