Compare commits

..

8 Commits

Author SHA1 Message Date
Sindhu Balasubramanian
418b81c490 Fix timeout in tests 2026-02-04 23:10:47 -08:00
Sindhu Balasubramanian
25a89ebcb6 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2026-02-03 09:23:56 -08:00
Sindhu Balasubramanian
3f977df00d Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2026-01-13 10:25:55 -08:00
Sindhu Balasubramanian
865e9c906b Run npm format 2026-01-08 13:16:22 -08:00
Sindhu Balasubramanian
1c34425dd8 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2026-01-08 13:00:47 -08:00
Sindhu Balasubramanian
50a244e6f9 Add Mongo Pagination tests 2026-01-08 12:55:24 -08:00
Sindhu Balasubramanian
9dad75c2f9 Remove localhost 2025-12-30 23:21:56 -08:00
Sindhu Balasubramanian
876b531248 Add changes for Load more option to work 2025-12-30 22:54:42 -08:00
66 changed files with 11418 additions and 5705 deletions

1
.gitignore vendored
View File

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

14305
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/SubscriptionType";
import { isExpectedError } from "../Metrics/ErrorClassification";
import { scenarioMonitor } from "../Metrics/ScenarioMonitor";
import { userContext } from "../UserContext";
import { ARMError } from "../Utils/arm/request";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
@@ -33,12 +31,6 @@ 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 => {

View File

@@ -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", "SC1010"];
const codesToRedact = ["SC1001", "SC2001"];
try {
// Handle error objects with a message property

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -789,7 +789,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
);
let partitionKeyProperties = useMemo(() => {
return partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) =>
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, "").replace(/["]+/g, ""),
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""),
);
}, [partitionKeyPropertyHeaders]);
@@ -1470,11 +1470,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
const partitionKey = _partitionKey || (_collection && _collection.partitionKey);
const partitionKeyPropertyHeaders = _collection?.partitionKeyPropertyHeaders || partitionKey?.paths;
const partitionKeyProperties = partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) =>
partitionKeyPropertyHeader
.replace(/[/]+/g, ".")
.substring(1)
.replace(/[']+/g, "")
.replace(/["]+/g, ""),
partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""),
);
return newDocumentId(rawDocument, partitionKeyProperties, partitionKeyValue);

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
{
"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."
}
}
}

View File

@@ -1,13 +0,0 @@
{
"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."
}
}
}

View File

@@ -1,295 +0,0 @@
{
"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}}"
}
}
}

View File

@@ -1,13 +0,0 @@
{
"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."
}
}
}

View File

@@ -1,13 +0,0 @@
{
"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 dexemple, notre documentation et nos outils supplémentaires."
}
}
}

View File

@@ -1,13 +0,0 @@
{
"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."
}
}
}

View File

@@ -1,13 +0,0 @@
{
"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."
}
}
}

View File

@@ -1,13 +0,0 @@
{
"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."
}
}
}

View File

@@ -1,13 +0,0 @@
{
"splashScreen": {
"title": {
"default": "Azure Cosmos DB へようこそ",
"postgres": "Azure Cosmos DB for PostgreSQL へようこそ",
"vcoreMongo": "Azure DocumentDB (MongoDB 互換) へようこそ"
},
"subtitle": {
"default": "あらゆるスケールに対応するグローバル分散型のマルチモデル データベース サーバー",
"getStarted": "サンプル データセット、ドキュメント、追加ツールを使用して開始してください。"
}
}
}

View File

@@ -1,13 +0,0 @@
{
"splashScreen": {
"title": {
"default": "Azure Cosmos DB 시작",
"postgres": "Azure Cosmos DB for PostgreSQL 시작",
"vcoreMongo": "Azure DocumentDB 시작(MongoDB 호환성 포함)"
},
"subtitle": {
"default": "모든 규모에 대해 전역적으로 분산된 다중 모델 데이터베이스 서비스",
"getStarted": "샘플 데이터 세트, 설명서 및 추가 도구를 시작하세요."
}
}
}

View File

@@ -1,13 +0,0 @@
{
"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."
}
}
}

View File

@@ -1,13 +0,0 @@
{
"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."
}
}
}

View File

@@ -1,13 +0,0 @@
{
"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."
}
}
}

View File

@@ -1,13 +0,0 @@
{
"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."
}
}
}

View File

@@ -1,13 +0,0 @@
{
"splashScreen": {
"title": {
"default": "Вас приветствует Azure Cosmos DB",
"postgres": "Добро пожаловать в Azure Cosmos DB for PostgreSQL",
"vcoreMongo": "Добро пожаловать в Azure DocumentDB (с совместимостью с MongoDB)"
},
"subtitle": {
"default": "Глобально распределенная многомодельная служба базы данных для использования в любом масштабе",
"getStarted": "Начните работу с нашими примерами наборов данных, документацией и дополнительными инструментами."
}
}
}

View File

@@ -1,13 +0,0 @@
{
"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."
}
}
}

View File

@@ -1,24 +0,0 @@
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 });

View File

@@ -1,13 +0,0 @@
{
"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."
}
}
}

View File

@@ -1,13 +0,0 @@
{
"splashScreen": {
"title": {
"default": "欢迎使用 Azure Cosmos DB",
"postgres": "欢迎使用 Azure Cosmos DB for PostgreSQL",
"vcoreMongo": "欢迎使用 Azure DocumentDB (具有 MongoDB 兼容性)"
},
"subtitle": {
"default": "任何规模的全球分布式多模型数据库服务",
"getStarted": "开始使用我们的示例数据集、文档和其他工具。"
}
}
}

View File

@@ -1,13 +0,0 @@
{
"splashScreen": {
"title": {
"default": "歡迎使用 Azure Cosmos DB",
"postgres": "歡迎使用 Azure Cosmos DB for PostgreSQL",
"vcoreMongo": "歡迎使用 Azure DocumentDB (具 MongoDB 相容性)"
},
"subtitle": {
"default": "適用於任何規模的全域散發、多模型資料庫服務",
"getStarted": "開始使用我們的樣本資料集、文件和其他工具。"
}
}
}

View File

@@ -119,9 +119,6 @@ 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 />;
}
@@ -148,6 +145,7 @@ 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

View File

@@ -1,182 +0,0 @@
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);
});
});
});
});

View File

@@ -1,109 +0,0 @@
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;
}

View File

@@ -15,11 +15,6 @@ 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();

View File

@@ -1,231 +0,0 @@
/**
* @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();
});
});
});

View File

@@ -21,7 +21,6 @@ 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 {
@@ -56,13 +55,6 @@ 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;
@@ -83,7 +75,6 @@ class ScenarioMonitor {
failed: new Set<MetricPhase>(),
phases: new Map<MetricPhase, PhaseContext>(),
emitted: false,
hasExpectedFailure: false,
};
// Start all required phases at scenario start time
@@ -93,10 +84,6 @@ 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,
@@ -104,28 +91,7 @@ class ScenarioMonitor {
timeoutMs: 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);
ctx.timeoutId = window.setTimeout(() => this.emit(ctx, false, true), config.timeoutMs);
this.contexts.set(scenario, ctx);
}
@@ -164,12 +130,6 @@ 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,
@@ -189,13 +149,6 @@ 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);
@@ -210,12 +163,6 @@ 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,
@@ -224,28 +171,10 @@ class ScenarioMonitor {
completedPhases: Array.from(ctx.completed).join(","),
});
// Emit unhealthy immediately for unexpected failures
// Emit unhealthy immediately
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) {
@@ -305,7 +234,6 @@ class ScenarioMonitor {
scenario: ctx.scenario,
healthy,
timedOut,
documentHidden: document.hidden,
platform,
api,
durationMs: finalSnapshot.durationMs,
@@ -318,22 +246,8 @@ 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 is true (either completed successfully or timeout with expected failure), report healthy
if (healthy) {
if (healthy && !timedOut) {
reportHealthy(ctx.scenario, platform, api);
} else {
reportUnhealthy(ctx.scenario, platform, api);
@@ -388,19 +302,6 @@ 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();

View File

@@ -1,26 +1,20 @@
import React from "react";
import MetricScenario from "./MetricEvents";
import { scenarioMonitor } from "./ScenarioMonitor";
import { useMetricScenario } from "./MetricScenarioProvider";
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, enabled = true) {
export function useInteractive(scenario: MetricScenario) {
const { completePhase } = useMetricScenario();
React.useEffect(() => {
if (!enabled) {
return undefined;
}
const id = requestAnimationFrame(() => {
scenarioMonitor.completePhase(scenario, CommonMetricPhase.Interactive);
requestAnimationFrame(() => {
completePhase(scenario, CommonMetricPhase.Interactive);
});
return () => cancelAnimationFrame(id);
}, [scenario, enabled]);
}, [scenario, completePhase]);
}
/**
@@ -28,20 +22,18 @@ export function useInteractive(scenario: MetricScenario, enabled = true) {
* 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;
scenarioMonitor.completePhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabaseTreeRendered);
completePhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabaseTreeRendered);
}
}, [databaseTreeNodes, fetchSucceeded]);
}, [databaseTreeNodes, fetchSucceeded, completePhase]);
// Track Interactive phase
useInteractive(MetricScenario.DatabaseLoad);

View File

@@ -37,7 +37,7 @@ const requestFabricToken = async (): Promise<void> => {
scheduleRefreshFabricToken();
} catch (error) {
logConsoleError(error instanceof Error ? error.message : String(error));
logConsoleError(error as string);
throw error;
} finally {
lastRequestTimestamp = undefined;

View File

@@ -9,6 +9,7 @@ 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";
@@ -80,7 +81,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
private smartUiGeneratorClassName: string;
private retryIntervalInMs: number;
private retryOptions: Options;
private translationFunction: (key: string) => string;
private translationFunction: TFunction;
componentDidMount(): void {
this.performRefresh().then(() => {
@@ -118,7 +119,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 as (key: string) => string;
this.translationFunction = this.props.t;
}
private onError = (hasErrors: boolean): void => {

View File

@@ -8,8 +8,6 @@ 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";
@@ -129,10 +127,6 @@ 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 {
@@ -175,10 +169,7 @@ 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 {
@@ -187,10 +178,7 @@ 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;
}
}

View File

@@ -4,12 +4,7 @@ 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,
stripDoubleQuotesFromSegment,
} from "./QueryUtils";
import { defaultQueryFields, extractPartitionKeyValues, getValueForPath } from "./QueryUtils";
const documentContent = {
"Volcano Name": "Adams",
@@ -284,97 +279,5 @@ 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'");
});
});
});

View File

@@ -116,17 +116,6 @@ 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) {
@@ -157,7 +146,7 @@ export const extractPartitionKeyValues = (
const partitionKeyValues: PartitionKey[] = [];
partitionKeyDefinition.paths.forEach((partitionKeyPath: string) => {
const pathSegments: string[] = partitionKeyPath.substring(1).split("/").map(stripDoubleQuotesFromSegment);
const pathSegments: string[] = partitionKeyPath.substring(1).split("/");
const value = getValueForPath(documentContent, pathSegments);
if (value !== undefined) {

View File

@@ -1,21 +1,16 @@
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: {

View File

@@ -2,54 +2,20 @@ 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: {
...request.headers(),
...route.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: {
...responseHeaders,
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS,HEAD",
...response.headers(),
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Expose-Headers": "x-ms-continuation,x-ms-request-charge,x-ms-session-token",
Vary: "Origin",
"Access-Control-Allow-Credentials": "*",
},
body: await response.body(),
});

View File

@@ -250,7 +250,8 @@ 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", timeout: 30000 });
// Use a longer timeout (30s) since expanding may require loading children from the server
await tree.waitFor({ state: "visible", timeout: 30 * 1000 });
// The tree has expanded, let's get out of here
return true;
} catch {

View File

@@ -9,10 +9,9 @@ import {
TestAccount,
waitForApiResponse,
} from "../../fx";
import { createMultipleTestContainers, TestContainerContext } from "../../testData";
import { createMultipleTestContainers } from "../../testData";
test.describe("Container Copy - Offline Migration", () => {
let contexts: TestContainerContext[];
let page: Page;
let wrapper: Locator;
let panel: Locator;
@@ -23,7 +22,7 @@ test.describe("Container Copy - Offline Migration", () => {
let expectedCopyJobNameInitial: string;
test.beforeEach("Setup for offline migration test", async ({ browser }) => {
contexts = await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 });
await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 });
page = await browser.newPage();
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
@@ -34,7 +33,6 @@ 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 () => {

View File

@@ -7,10 +7,9 @@ import {
TestAccount,
waitForApiResponse,
} from "../../fx";
import { createMultipleTestContainers, TestContainerContext } from "../../testData";
import { createMultipleTestContainers } from "../../testData";
test.describe("Container Copy - Online Migration", () => {
let contexts: TestContainerContext[];
let page: Page;
let wrapper: Locator;
let panel: Locator;
@@ -18,7 +17,7 @@ test.describe("Container Copy - Online Migration", () => {
let targetAccountName: string;
test.beforeEach("Setup for online migration test", async ({ browser }) => {
contexts = await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 });
await createMultipleTestContainers({ accountType: TestAccount.SQLContainerCopyOnly, containerCount: 2 });
page = await browser.newPage();
({ wrapper, frame } = await ContainerCopy.open(page, TestAccount.SQLContainerCopyOnly));
@@ -28,7 +27,6 @@ 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 () => {

View File

@@ -2,6 +2,7 @@ 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,
@@ -17,7 +18,8 @@ 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 armClient = new CosmosDBManagementClient(credentials, subscriptionId);
const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
const armClient = new CosmosDBManagementClient(adaptedCredentials, subscriptionId);
const accountName = getAccountName(TestAccount.SQL);
const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName);

View File

@@ -29,7 +29,7 @@ test.describe("Database with Shared Throughput", () => {
});
test("Create database with shared manual throughput and verify Scale node in UI", async () => {
test.setTimeout(120000); // 2 minutes timeout
test.setTimeout(180000); // 3 minutes timeout
// Create database with shared manual throughput (400 RU/s)
dbContext = await createTestDB({ throughput: 400 });
@@ -47,6 +47,7 @@ test.describe("Database with Shared Throughput", () => {
});
test("Add container to shared database without dedicated throughput", async () => {
test.setTimeout(180000); // 3 minutes timeout
// Create database with shared manual throughput
dbContext = await createTestDB({ throughput: 400 });
@@ -100,6 +101,7 @@ test.describe("Database with Shared Throughput", () => {
});
test("Scale shared database manual throughput", async () => {
test.setTimeout(180000); // 3 minutes timeout
// Create database with shared manual throughput (400 RU/s)
dbContext = await createTestDB({ throughput: 400 });
@@ -125,6 +127,7 @@ test.describe("Database with Shared Throughput", () => {
});
test("Scale shared database from manual to autoscale", async () => {
test.setTimeout(180000); // 3 minutes timeout
// Create database with shared manual throughput (400 RU/s)
dbContext = await createTestDB({ throughput: 400 });
@@ -159,7 +162,7 @@ test.describe("Database with Shared Throughput", () => {
});
test("Create database with shared autoscale throughput and verify Scale node in UI", async () => {
test.setTimeout(120000); // 2 minutes timeout
test.setTimeout(180000); // 3 minutes timeout
// Create database with shared autoscale throughput (max 1000 RU/s)
dbContext = await createTestDB({ maxThroughput: 1000 });
@@ -178,6 +181,7 @@ test.describe("Database with Shared Throughput", () => {
});
test("Scale shared database autoscale throughput", async () => {
test.setTimeout(180000); // 3 minutes timeout
// Create database with shared autoscale throughput (max 1000 RU/s)
dbContext = await createTestDB({ maxThroughput: 1000 });
@@ -203,6 +207,7 @@ test.describe("Database with Shared Throughput", () => {
});
test("Scale shared database from autoscale to manual", async () => {
test.setTimeout(180000); // 3 minutes timeout
// Create database with shared autoscale throughput (max 1000 RU/s)
dbContext = await createTestDB({ maxThroughput: 1000 });

View File

@@ -249,27 +249,4 @@ 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: [],
},
],
},
];

View File

@@ -1,15 +1,9 @@
import crypto from "crypto";
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
import {
BulkOperationType,
Container,
CosmosClient,
CosmosClientOptions,
Database,
ErrorResponse,
JSONObject,
} from "@azure/cosmos";
import { Buffer } from "node:buffer";
import { webcrypto } from "node:crypto";
import { BulkOperationType, Container, CosmosClient, CosmosClientOptions, Database, JSONObject } from "@azure/cosmos";
import { AzureIdentityCredentialAdapter } from "@azure/ms-rest-js";
import {
generateUniqueName,
getAccountName,
@@ -18,7 +12,6 @@ import {
subscriptionId,
TestAccount,
} from "./fx";
globalThis.crypto = webcrypto as Crypto;
export interface TestItem {
id: string;
@@ -67,9 +60,8 @@ function createTestItems(): TestItem[] {
// Document IDs cannot contain '/', '\', or '#'
function createSafeRandomString(byteLength: number): string {
const bytes = new Uint8Array(byteLength);
crypto.getRandomValues(bytes);
return Buffer.from(bytes)
return crypto
.randomBytes(byteLength)
.toString("base64")
.replace(/[/\\#]/g, "_");
}
@@ -86,14 +78,7 @@ export class TestContainerContext {
) {}
async dispose() {
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
}
await this.database.delete();
}
}
@@ -119,7 +104,8 @@ async function createCosmosClientForSQLAccount(
accountType: TestAccount.SQL | TestAccount.SQLContainerCopyOnly = TestAccount.SQL,
): Promise<{ armClient: CosmosDBManagementClient; client: CosmosClient }> {
const credentials = getAzureCLICredentials();
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
const armClient = new CosmosDBManagementClient(adaptedCredentials, subscriptionId);
const accountName = getAccountName(accountType);
const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
@@ -246,19 +232,12 @@ export async function createTestSQLContainer({
}
export const setPartitionKeys = (partitionKeys: PartitionKey[]) => {
const result: Record<string, unknown> = {};
const result = {};
partitionKeys.forEach((partitionKey) => {
const { key: keyPath, value: keyValue } = partitionKey;
const cleanPath = keyPath.startsWith("/") ? keyPath.slice(1) : keyPath;
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;
});
const keys = cleanPath.split("/");
let current = result;
keys.forEach((key, index) => {
@@ -266,7 +245,7 @@ export const setPartitionKeys = (partitionKeys: PartitionKey[]) => {
current[key] = keyValue;
} else {
current[key] = current[key] || {};
current = current[key] as Record<string, unknown>;
current = current[key];
}
});
});

View File

@@ -1,180 +1,66 @@
{
"name": "deployment-status",
"version": "1.0.0",
"lockfileVersion": 3,
"lockfileVersion": 1,
"requires": true,
"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": {
"dependencies": {
"@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=="
},
"node_modules/ansi-styles": {
"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==",
"dependencies": {
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/chalk": {
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dependencies": {
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/color-convert": {
"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==",
"dependencies": {
"requires": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"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=="
},
"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": {
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"engines": {
"node": ">=8"
}
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"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": "*"
}
"moment": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ=="
},
"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"
}
"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-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": {
"supports-color": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
"integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
"dependencies": {
"requires": {
"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"
}
}
}

View File

@@ -11,7 +11,7 @@
"license": "ISC",
"dependencies": {
"chalk": "^4.1.0",
"moment": "^2.30.1",
"node-fetch": "^3.3.2"
"moment": "^2.27.0",
"node-fetch": "^2.6.1"
}
}

View File

@@ -1,71 +0,0 @@
/**
* 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}`);

View File

@@ -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 portal.sovcloud-azure.fr portal.sovcloud-azure.de portal.sovcloud-azure.sg" />
<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" />
</customHeaders>
<redirectHeaders>
<clear />

View File

@@ -303,53 +303,51 @@ 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: [
{
context: ["/api"],
proxy: {
"/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();
}
},
},
{
context: ["/proxy"],
"/proxy": {
target: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
changeOrigin: true,
secure: false,
logLevel: "debug",
pathRewrite: { "^/proxy": "" },
router: (req) => req.headers["x-ms-proxy-target"],
router: (req) => {
let newTarget = req.headers["x-ms-proxy-target"];
return newTarget;
},
},
{
context: ["/_explorer"],
"/_explorer": {
target: process.env.EMULATOR_ENDPOINT || "https://localhost:8081/",
changeOrigin: true,
secure: false,
logLevel: "debug",
},
{
context: ["/explorerProxy"],
"/explorerProxy": {
target: process.env.EMULATOR_ENDPOINT || "https://localhost:8081/",
pathRewrite: { "^/explorerProxy": "" },
changeOrigin: true,
secure: false,
logLevel: "debug",
},
{
context: [`/${AZURE_TENANT_ID}`],
[`/${AZURE_TENANT_ID}`]: {
target: "https://login.microsoftonline.com/",
changeOrigin: true,
secure: false,
logLevel: "debug",
},
],
},
},
stats: "minimal",
};