mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-02-10 04:24:15 +00:00
Compare commits
2 Commits
DE_Localiz
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f959e2235 | ||
|
|
67250f0f6b |
309
package-lock.json
generated
309
package-lock.json
generated
@@ -76,7 +76,6 @@
|
||||
"i18next": "23.11.5",
|
||||
"i18next-browser-languagedetector": "6.0.1",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"iframe-resizer-react": "1.1.0",
|
||||
"immer": "9.0.6",
|
||||
"immutable": "4.0.0-rc.12",
|
||||
@@ -178,7 +177,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",
|
||||
@@ -204,7 +202,6 @@
|
||||
"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",
|
||||
@@ -2714,9 +2711,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
|
||||
"integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -10409,231 +10409,16 @@
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.11.tgz",
|
||||
"integrity": "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3",
|
||||
"@swc/types": "^0.1.25"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/swc"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-darwin-arm64": "1.15.11",
|
||||
"@swc/core-darwin-x64": "1.15.11",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.15.11",
|
||||
"@swc/core-linux-arm64-gnu": "1.15.11",
|
||||
"@swc/core-linux-arm64-musl": "1.15.11",
|
||||
"@swc/core-linux-x64-gnu": "1.15.11",
|
||||
"@swc/core-linux-x64-musl": "1.15.11",
|
||||
"@swc/core-win32-arm64-msvc": "1.15.11",
|
||||
"@swc/core-win32-ia32-msvc": "1.15.11",
|
||||
"@swc/core-win32-x64-msvc": "1.15.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/helpers": ">=0.5.17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/helpers": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.11.tgz",
|
||||
"integrity": "sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-x64": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.11.tgz",
|
||||
"integrity": "sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.11.tgz",
|
||||
"integrity": "sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.11.tgz",
|
||||
"integrity": "sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-musl": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.11.tgz",
|
||||
"integrity": "sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.11.tgz",
|
||||
"integrity": "sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-musl": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.11.tgz",
|
||||
"integrity": "sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.11.tgz",
|
||||
"integrity": "sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.11.tgz",
|
||||
"integrity": "sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-x64-msvc": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.11.tgz",
|
||||
"integrity": "sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz",
|
||||
"integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
|
||||
"version": "0.5.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"node_modules/@swc/types": {
|
||||
"version": "0.1.25",
|
||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
|
||||
"integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3"
|
||||
}
|
||||
"version": "2.6.2",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "7.31.2",
|
||||
@@ -19403,57 +19188,6 @@
|
||||
"cross-fetch": "4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-resources-for-ts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next-resources-for-ts/-/i18next-resources-for-ts-2.0.0.tgz",
|
||||
"integrity": "sha512-RvATolbJlxrwpZh2+R7ZcNtg0ewmXFFx6rdu9i2bUEBvn6ThgA82rxDe3rJQa3hFS0SopX0qPaABqVDN3TUVpw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@swc/core": "^1.15.3",
|
||||
"chokidar": "^5.0.0",
|
||||
"yaml": "^2.8.2"
|
||||
},
|
||||
"bin": {
|
||||
"i18next-resources-for-ts": "bin/i18next-resources-for-ts.js"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-resources-for-ts/node_modules/chokidar": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"readdirp": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-resources-for-ts/node_modules/readdirp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-resources-to-backend": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.1.tgz",
|
||||
"integrity": "sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -29541,7 +29275,6 @@
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/regenerator-transform": {
|
||||
@@ -32207,15 +31940,6 @@
|
||||
"resolved": "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz",
|
||||
"integrity": "sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg=="
|
||||
},
|
||||
"node_modules/values-to-keys": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/values-to-keys/-/values-to-keys-1.1.0.tgz",
|
||||
"integrity": "sha512-3ErIYotgwYxBeBstuiaMVDSj6uUzoYnk4A4oM9NqKHrUzSmihvt0DqpQozFq3NWevgnz7hwkOZUXBthIuwxJaQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"dev": true,
|
||||
@@ -33912,17 +33636,14 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
|
||||
"integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
|
||||
10
package.json
10
package.json
@@ -71,7 +71,6 @@
|
||||
"i18next": "23.11.5",
|
||||
"i18next-browser-languagedetector": "6.0.1",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"iframe-resizer-react": "1.1.0",
|
||||
"immer": "9.0.6",
|
||||
"immutable": "4.0.0-rc.12",
|
||||
@@ -177,7 +176,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",
|
||||
@@ -203,7 +201,6 @@
|
||||
"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",
|
||||
@@ -213,11 +210,11 @@
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
"start": "npm run generate:i18n-keys && webpack serve --mode development",
|
||||
"start": "webpack serve --mode development",
|
||||
"dev": "echo \"WARNING: npm run dev has been deprecated\" && npm run build",
|
||||
"build:dataExplorer:ci": "npm run build:ci",
|
||||
"build": "npm run generate:i18n-keys && npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:prod && npm run copyToConsumers",
|
||||
"build:ci": "npm run generate:i18n-keys && npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:fast",
|
||||
"build": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:prod && npm run copyToConsumers",
|
||||
"build:ci": "npm run format:check && npm run lint && npm run compile && npm run compile:strict && npm run pack:fast",
|
||||
"pack:prod": "webpack --mode production",
|
||||
"pack:fast": "webpack --mode development --progress",
|
||||
"copyToConsumers": "node copyToConsumers",
|
||||
@@ -239,7 +236,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": {
|
||||
|
||||
11
src/@types/i18next.d.ts
vendored
11
src/@types/i18next.d.ts
vendored
@@ -1,11 +0,0 @@
|
||||
import "i18next";
|
||||
import Resources from "../Localization/en/Resources.json";
|
||||
|
||||
declare module "i18next" {
|
||||
interface CustomTypeOptions {
|
||||
defaultNS: "Resources";
|
||||
resources: {
|
||||
Resources: typeof Resources;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
||||
import { isExpectedError } from "../Metrics/ErrorClassification";
|
||||
import { scenarioMonitor } from "../Metrics/ScenarioMonitor";
|
||||
import { userContext } from "../UserContext";
|
||||
import { ARMError } from "../Utils/arm/request";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
@@ -31,6 +33,12 @@ export const handleError = (
|
||||
|
||||
// checks for errors caused by firewall and sends them to portal to handle
|
||||
sendNotificationForError(errorMessage, errorCode);
|
||||
|
||||
// Mark expected failures for health metrics (auth, firewall, permissions, etc.)
|
||||
// This ensures timeouts with expected failures emit healthy instead of unhealthy
|
||||
if (isExpectedError(error)) {
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
}
|
||||
};
|
||||
|
||||
export const getErrorMessage = (error: string | Error = ""): string => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "./i18n";
|
||||
import React, { useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import Arrow from "../images/Arrow.svg";
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// -----------------------------------------------------------------
|
||||
// THIS FILE IS AUTO-GENERATED — DO NOT EDIT BY HAND
|
||||
// Regenerate with: npm run generate:i18n-keys
|
||||
// -----------------------------------------------------------------
|
||||
export const Keys = {
|
||||
splashScreen: {
|
||||
title: {
|
||||
/** Welcome to Azure Cosmos DB */
|
||||
default: "splashScreen.title.default",
|
||||
/** Welcome to Azure Cosmos DB for PostgreSQL */
|
||||
postgres: "splashScreen.title.postgres",
|
||||
/** Welcome to Azure DocumentDB (with MongoDB compatibility) */
|
||||
vcoreMongo: "splashScreen.title.vcoreMongo",
|
||||
},
|
||||
subtitle: {
|
||||
/** Globally distributed, multi-model database service for any scale */
|
||||
default: "splashScreen.subtitle.default",
|
||||
/** Get started with our sample datasets, documentation, and additional tools. */
|
||||
getStarted: "splashScreen.subtitle.getStarted",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"splashScreen": {
|
||||
"title": {
|
||||
"default": "Willkommen bei Azure Cosmos DB",
|
||||
"postgres": "Willkommen bei Azure Cosmos DB für PostgreSQL",
|
||||
"vcoreMongo": "Willkommen bei Azure DocumentDB (mit MongoDB-Kompatibilität)"
|
||||
},
|
||||
"subtitle": {
|
||||
"default": "Global verteilter, multimodaler Datenbankdienst für jede Skalierung",
|
||||
"getStarted": "Beginnen Sie mit unseren Beispieldatensätzen, Dokumentation und zusätzlichen Tools."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
@@ -119,6 +119,9 @@ const App = (): JSX.Element => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [explorer]);
|
||||
|
||||
// Track interactive phase for both ContainerCopyPanel and DivExplorer paths
|
||||
useInteractive(MetricScenario.ApplicationLoad);
|
||||
|
||||
if (!explorer) {
|
||||
return <LoadingExplorer />;
|
||||
}
|
||||
@@ -145,7 +148,6 @@ const App = (): JSX.Element => {
|
||||
const DivExplorer: React.FC<{ explorer: Explorer }> = ({ explorer }) => {
|
||||
const isCarouselOpen = useCarousel((state) => state.shouldOpen);
|
||||
const isCopilotCarouselOpen = useCarousel((state) => state.showCopilotCarousel);
|
||||
useInteractive(MetricScenario.ApplicationLoad);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
182
src/Metrics/ErrorClassification.test.ts
Normal file
182
src/Metrics/ErrorClassification.test.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { ARMError } from "../Utils/arm/request";
|
||||
import { isExpectedError } from "./ErrorClassification";
|
||||
|
||||
describe("ErrorClassification", () => {
|
||||
describe("isExpectedError", () => {
|
||||
describe("ARMError with expected codes", () => {
|
||||
it("returns true for AuthorizationFailed code", () => {
|
||||
const error = new ARMError("Authorization failed");
|
||||
error.code = "AuthorizationFailed";
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for Forbidden code", () => {
|
||||
const error = new ARMError("Forbidden");
|
||||
error.code = "Forbidden";
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for Unauthorized code", () => {
|
||||
const error = new ARMError("Unauthorized");
|
||||
error.code = "Unauthorized";
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for InvalidAuthenticationToken code", () => {
|
||||
const error = new ARMError("Invalid token");
|
||||
error.code = "InvalidAuthenticationToken";
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for ExpiredAuthenticationToken code", () => {
|
||||
const error = new ARMError("Token expired");
|
||||
error.code = "ExpiredAuthenticationToken";
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for numeric 401 code", () => {
|
||||
const error = new ARMError("Unauthorized");
|
||||
error.code = 401;
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for numeric 403 code", () => {
|
||||
const error = new ARMError("Forbidden");
|
||||
error.code = 403;
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for unexpected ARM error code", () => {
|
||||
const error = new ARMError("Internal error");
|
||||
error.code = "InternalServerError";
|
||||
expect(isExpectedError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for numeric 500 code", () => {
|
||||
const error = new ARMError("Server error");
|
||||
error.code = 500;
|
||||
expect(isExpectedError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MSAL AuthError with expected errorCodes", () => {
|
||||
it("returns true for popup_window_error", () => {
|
||||
const error = { errorCode: "popup_window_error", message: "Popup blocked" };
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for interaction_required", () => {
|
||||
const error = { errorCode: "interaction_required", message: "User interaction required" };
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for user_cancelled", () => {
|
||||
const error = { errorCode: "user_cancelled", message: "User cancelled" };
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for consent_required", () => {
|
||||
const error = { errorCode: "consent_required", message: "Consent required" };
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for login_required", () => {
|
||||
const error = { errorCode: "login_required", message: "Login required" };
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for no_account_error", () => {
|
||||
const error = { errorCode: "no_account_error", message: "No account" };
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for unexpected MSAL error code", () => {
|
||||
const error = { errorCode: "unknown_error", message: "Unknown" };
|
||||
expect(isExpectedError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTTP status codes", () => {
|
||||
it("returns true for error with status 401", () => {
|
||||
const error = { status: 401, message: "Unauthorized" };
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for error with status 403", () => {
|
||||
const error = { status: 403, message: "Forbidden" };
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for error with status 500", () => {
|
||||
const error = { status: 500, message: "Internal Server Error" };
|
||||
expect(isExpectedError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for error with status 404", () => {
|
||||
const error = { status: 404, message: "Not Found" };
|
||||
expect(isExpectedError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Firewall error message pattern", () => {
|
||||
it("returns true for firewall error in Error message", () => {
|
||||
const error = new Error("Request blocked by firewall");
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for IP not allowed error", () => {
|
||||
const error = new Error("Client IP address is not allowed");
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for ip not allowed (no 'address')", () => {
|
||||
const error = new Error("Your ip not allowed to access this resource");
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for string error with firewall", () => {
|
||||
expect(isExpectedError("firewall rules prevent access")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for case-insensitive firewall match", () => {
|
||||
const error = new Error("FIREWALL blocked request");
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for unrelated error message", () => {
|
||||
const error = new Error("Database connection failed");
|
||||
expect(isExpectedError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge cases", () => {
|
||||
it("returns false for null", () => {
|
||||
expect(isExpectedError(null)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for undefined", () => {
|
||||
expect(isExpectedError(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for empty object", () => {
|
||||
expect(isExpectedError({})).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for plain Error without expected patterns", () => {
|
||||
const error = new Error("Something went wrong");
|
||||
expect(isExpectedError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for string without firewall pattern", () => {
|
||||
expect(isExpectedError("Generic error occurred")).toBe(false);
|
||||
});
|
||||
|
||||
it("handles error with multiple matching criteria", () => {
|
||||
// ARMError with both code and firewall message
|
||||
const error = new ARMError("Request blocked by firewall");
|
||||
error.code = "Forbidden";
|
||||
expect(isExpectedError(error)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
109
src/Metrics/ErrorClassification.ts
Normal file
109
src/Metrics/ErrorClassification.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { ARMError } from "../Utils/arm/request";
|
||||
|
||||
/**
|
||||
* Expected error codes that should not mark scenarios as unhealthy.
|
||||
* These represent expected failures like auth issues, permission errors, and user actions.
|
||||
*/
|
||||
|
||||
// ARM error codes (string)
|
||||
const EXPECTED_ARM_ERROR_CODES: Set<string> = new Set([
|
||||
"AuthorizationFailed",
|
||||
"Forbidden",
|
||||
"Unauthorized",
|
||||
"AuthenticationFailed",
|
||||
"InvalidAuthenticationToken",
|
||||
"ExpiredAuthenticationToken",
|
||||
"AuthorizationPermissionMismatch",
|
||||
]);
|
||||
|
||||
// HTTP status codes that indicate expected failures
|
||||
const EXPECTED_HTTP_STATUS_CODES: Set<number> = new Set([
|
||||
401, // Unauthorized
|
||||
403, // Forbidden
|
||||
]);
|
||||
|
||||
// MSAL error codes (string)
|
||||
const EXPECTED_MSAL_ERROR_CODES: Set<string> = new Set([
|
||||
"popup_window_error",
|
||||
"interaction_required",
|
||||
"user_cancelled",
|
||||
"consent_required",
|
||||
"login_required",
|
||||
"no_account_error",
|
||||
"monitor_window_timeout",
|
||||
"empty_window_error",
|
||||
]);
|
||||
|
||||
// Firewall error message pattern (only case where we check message content)
|
||||
const FIREWALL_ERROR_PATTERN = /firewall|ip\s*(address)?\s*(is\s*)?not\s*allowed/i;
|
||||
|
||||
/**
|
||||
* Interface for MSAL AuthError-like objects
|
||||
*/
|
||||
interface MsalAuthError {
|
||||
errorCode?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for errors with HTTP status
|
||||
*/
|
||||
interface HttpError {
|
||||
status?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an error is an expected failure that should not mark the scenario as unhealthy.
|
||||
*
|
||||
* Expected failures include:
|
||||
* - Authentication/authorization errors (user not logged in, permissions)
|
||||
* - Firewall blocking errors
|
||||
* - User-cancelled operations
|
||||
*
|
||||
* @param error - The error to classify
|
||||
* @returns true if the error is expected and should not affect health metrics
|
||||
*/
|
||||
export function isExpectedError(error: unknown): boolean {
|
||||
if (!error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check ARMError code
|
||||
if (error instanceof ARMError && error.code !== undefined) {
|
||||
if (typeof error.code === "string" && EXPECTED_ARM_ERROR_CODES.has(error.code)) {
|
||||
return true;
|
||||
}
|
||||
if (typeof error.code === "number" && EXPECTED_HTTP_STATUS_CODES.has(error.code)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for MSAL AuthError (has errorCode property)
|
||||
const msalError = error as MsalAuthError;
|
||||
if (msalError.errorCode && typeof msalError.errorCode === "string") {
|
||||
if (EXPECTED_MSAL_ERROR_CODES.has(msalError.errorCode)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check HTTP status on generic errors
|
||||
const httpError = error as HttpError;
|
||||
if (httpError.status && typeof httpError.status === "number") {
|
||||
if (EXPECTED_HTTP_STATUS_CODES.has(httpError.status)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for firewall error in message (the only message-based check)
|
||||
if (error instanceof Error && error.message) {
|
||||
if (FIREWALL_ERROR_PATTERN.test(error.message)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for string errors with firewall pattern
|
||||
if (typeof error === "string" && FIREWALL_ERROR_PATTERN.test(error)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -15,6 +15,11 @@ export const reportUnhealthy = (scenario: MetricScenario, platform: Platform, ap
|
||||
send({ platform, api, scenario, healthy: false });
|
||||
|
||||
const send = async (event: MetricEvent): Promise<Response> => {
|
||||
// Skip metrics emission during local development
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
return Promise.resolve(new Response(null, { status: 200 }));
|
||||
}
|
||||
|
||||
const url = createUri(configContext?.PORTAL_BACKEND_ENDPOINT, RELATIVE_PATH);
|
||||
const authHeader = getAuthorizationHeader();
|
||||
|
||||
|
||||
231
src/Metrics/ScenarioMonitor.test.ts
Normal file
231
src/Metrics/ScenarioMonitor.test.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import MetricScenario, { reportHealthy, reportUnhealthy } from "./MetricEvents";
|
||||
import { ApplicationMetricPhase, CommonMetricPhase } from "./ScenarioConfig";
|
||||
import { scenarioMonitor } from "./ScenarioMonitor";
|
||||
|
||||
// Mock the MetricEvents module
|
||||
jest.mock("./MetricEvents", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
ApplicationLoad: "ApplicationLoad",
|
||||
DatabaseLoad: "DatabaseLoad",
|
||||
},
|
||||
reportHealthy: jest.fn().mockResolvedValue({ ok: true }),
|
||||
reportUnhealthy: jest.fn().mockResolvedValue({ ok: true }),
|
||||
}));
|
||||
|
||||
// Mock configContext
|
||||
jest.mock("../ConfigContext", () => ({
|
||||
configContext: {
|
||||
platform: "Portal",
|
||||
PORTAL_BACKEND_ENDPOINT: "https://test.portal.azure.com",
|
||||
},
|
||||
Platform: {
|
||||
Portal: "Portal",
|
||||
Hosted: "Hosted",
|
||||
Emulator: "Emulator",
|
||||
Fabric: "Fabric",
|
||||
},
|
||||
}));
|
||||
|
||||
describe("ScenarioMonitor", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Use legacy fake timers to avoid conflicts with performance API
|
||||
jest.useFakeTimers({ legacyFakeTimers: true });
|
||||
|
||||
// Ensure performance mock is available (setupTests.ts sets this but fake timers may override)
|
||||
if (typeof performance.mark !== "function") {
|
||||
Object.defineProperty(global, "performance", {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: {
|
||||
mark: jest.fn(),
|
||||
measure: jest.fn(),
|
||||
clearMarks: jest.fn(),
|
||||
clearMeasures: jest.fn(),
|
||||
getEntriesByName: jest.fn().mockReturnValue([{ startTime: 0 }]),
|
||||
getEntriesByType: jest.fn().mockReturnValue([]),
|
||||
now: jest.fn(() => Date.now()),
|
||||
timeOrigin: Date.now(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Reset userContext
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
});
|
||||
|
||||
// Reset the scenario monitor to clear any previous state
|
||||
scenarioMonitor.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Reset scenarios before switching to real timers
|
||||
scenarioMonitor.reset();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe("markExpectedFailure", () => {
|
||||
it("sets hasExpectedFailure flag on active scenarios", () => {
|
||||
// Start a scenario
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Mark expected failure
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
|
||||
// Let timeout fire - should emit healthy because of expected failure
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(reportHealthy).toHaveBeenCalledWith(MetricScenario.ApplicationLoad, configContext.platform, "SQL");
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets flag on multiple active scenarios", () => {
|
||||
// Start two scenarios
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
scenarioMonitor.start(MetricScenario.DatabaseLoad);
|
||||
|
||||
// Mark expected failure - should affect both
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
|
||||
// Let timeouts fire
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(reportHealthy).toHaveBeenCalledTimes(2);
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not affect already emitted scenarios", () => {
|
||||
// Start scenario
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Complete all phases to emit
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, CommonMetricPhase.Interactive);
|
||||
|
||||
// Now mark expected failure - should not change anything
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
|
||||
// Healthy was called when phases completed
|
||||
expect(reportHealthy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeout behavior", () => {
|
||||
it("emits unhealthy on timeout without expected failure", () => {
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Let timeout fire without marking expected failure
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(reportUnhealthy).toHaveBeenCalledWith(MetricScenario.ApplicationLoad, configContext.platform, "SQL");
|
||||
expect(reportHealthy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits healthy on timeout with expected failure", () => {
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Mark expected failure
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
|
||||
// Let timeout fire
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(reportHealthy).toHaveBeenCalledWith(MetricScenario.ApplicationLoad, configContext.platform, "SQL");
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits healthy even with partial phase completion and expected failure", () => {
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Complete one phase
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
|
||||
// Mark expected failure
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
|
||||
// Let timeout fire (Interactive phase not completed)
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
expect(reportHealthy).toHaveBeenCalled();
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("failPhase behavior", () => {
|
||||
it("emits unhealthy immediately on unexpected failure", () => {
|
||||
scenarioMonitor.start(MetricScenario.DatabaseLoad);
|
||||
|
||||
// Fail a phase (simulating unexpected error)
|
||||
scenarioMonitor.failPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabasesFetched);
|
||||
|
||||
// Should emit unhealthy immediately, not wait for timeout
|
||||
expect(reportUnhealthy).toHaveBeenCalledWith(MetricScenario.DatabaseLoad, configContext.platform, "SQL");
|
||||
});
|
||||
|
||||
it("does not emit twice after failPhase and timeout", () => {
|
||||
scenarioMonitor.start(MetricScenario.DatabaseLoad);
|
||||
|
||||
// Fail a phase
|
||||
scenarioMonitor.failPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabasesFetched);
|
||||
|
||||
// Let timeout fire
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
// Should only have emitted once (from failPhase)
|
||||
expect(reportUnhealthy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("completePhase behavior", () => {
|
||||
it("emits healthy when all phases complete", () => {
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Complete all required phases
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, CommonMetricPhase.Interactive);
|
||||
|
||||
expect(reportHealthy).toHaveBeenCalledWith(MetricScenario.ApplicationLoad, configContext.platform, "SQL");
|
||||
});
|
||||
|
||||
it("does not emit until all phases complete", () => {
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
|
||||
// Complete only one phase
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
|
||||
expect(reportHealthy).not.toHaveBeenCalled();
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("scenario isolation", () => {
|
||||
it("expected failure on one scenario does not affect others after completion", () => {
|
||||
// Start both scenarios
|
||||
scenarioMonitor.start(MetricScenario.ApplicationLoad);
|
||||
scenarioMonitor.start(MetricScenario.DatabaseLoad);
|
||||
|
||||
// Complete ApplicationLoad
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
|
||||
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, CommonMetricPhase.Interactive);
|
||||
|
||||
// Now mark expected failure - should only affect DatabaseLoad
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
|
||||
// Let DatabaseLoad timeout
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
// ApplicationLoad emitted healthy on completion
|
||||
// DatabaseLoad emits healthy on timeout (expected failure)
|
||||
expect(reportHealthy).toHaveBeenCalledTimes(2);
|
||||
expect(reportUnhealthy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@ interface InternalScenarioContext {
|
||||
phases: Map<MetricPhase, PhaseContext>; // Track start/end for each phase
|
||||
timeoutId?: number;
|
||||
emitted: boolean;
|
||||
hasExpectedFailure: boolean; // Flag for expected failures (auth, firewall, etc.)
|
||||
}
|
||||
|
||||
class ScenarioMonitor {
|
||||
@@ -75,6 +76,7 @@ class ScenarioMonitor {
|
||||
failed: new Set<MetricPhase>(),
|
||||
phases: new Map<MetricPhase, PhaseContext>(),
|
||||
emitted: false,
|
||||
hasExpectedFailure: false,
|
||||
};
|
||||
|
||||
// Start all required phases at scenario start time
|
||||
@@ -91,7 +93,11 @@ class ScenarioMonitor {
|
||||
timeoutMs: config.timeoutMs,
|
||||
});
|
||||
|
||||
ctx.timeoutId = window.setTimeout(() => this.emit(ctx, false, true), config.timeoutMs);
|
||||
ctx.timeoutId = window.setTimeout(() => {
|
||||
// If an expected failure occurred (auth, firewall, etc.), emit healthy instead of unhealthy
|
||||
const healthy = ctx.hasExpectedFailure;
|
||||
this.emit(ctx, healthy, true);
|
||||
}, config.timeoutMs);
|
||||
this.contexts.set(scenario, ctx);
|
||||
}
|
||||
|
||||
@@ -175,6 +181,24 @@ class ScenarioMonitor {
|
||||
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) {
|
||||
@@ -247,7 +271,8 @@ class ScenarioMonitor {
|
||||
});
|
||||
|
||||
// Call portal backend health metrics endpoint
|
||||
if (healthy && !timedOut) {
|
||||
// If healthy is true (either completed successfully or timeout with expected failure), report healthy
|
||||
if (healthy) {
|
||||
reportHealthy(ctx.scenario, platform, api);
|
||||
} else {
|
||||
reportUnhealthy(ctx.scenario, platform, api);
|
||||
@@ -302,6 +327,19 @@ class ScenarioMonitor {
|
||||
phaseTimings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all scenarios (for testing purposes only).
|
||||
* Clears all active contexts and their timeouts.
|
||||
*/
|
||||
reset() {
|
||||
this.contexts.forEach((ctx) => {
|
||||
if (ctx.timeoutId) {
|
||||
clearTimeout(ctx.timeoutId);
|
||||
}
|
||||
});
|
||||
this.contexts.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export const scenarioMonitor = new ScenarioMonitor();
|
||||
|
||||
@@ -8,6 +8,8 @@ import * as Logger from "../Common/Logger";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { isExpectedError } from "../Metrics/ErrorClassification";
|
||||
import { scenarioMonitor } from "../Metrics/ScenarioMonitor";
|
||||
import { trace, traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { UserContext, userContext } from "../UserContext";
|
||||
|
||||
@@ -127,6 +129,10 @@ export async function acquireMsalTokenForAccount(
|
||||
acquireTokenType: silent ? "silent" : "interactive",
|
||||
errorMessage: JSON.stringify(error),
|
||||
});
|
||||
// Mark expected failure for health metrics so timeout emits healthy
|
||||
if (isExpectedError(error)) {
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
@@ -169,7 +175,10 @@ export async function acquireTokenWithMsal(
|
||||
acquireTokenType: "interactive",
|
||||
errorMessage: JSON.stringify(interactiveError),
|
||||
});
|
||||
|
||||
// Mark expected failure for health metrics so timeout emits healthy
|
||||
if (isExpectedError(interactiveError)) {
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
}
|
||||
throw interactiveError;
|
||||
}
|
||||
} else {
|
||||
@@ -178,7 +187,10 @@ export async function acquireTokenWithMsal(
|
||||
acquireTokenType: "silent",
|
||||
errorMessage: JSON.stringify(silentError),
|
||||
});
|
||||
|
||||
// Mark expected failure for health metrics so timeout emits healthy
|
||||
if (isExpectedError(silentError)) {
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
}
|
||||
throw silentError;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -250,7 +250,7 @@ class TreeNode {
|
||||
// Try three times to wait for the node to expand.
|
||||
for (let i = 0; i < RETRY_COUNT; i++) {
|
||||
try {
|
||||
await tree.waitFor({ state: "visible" });
|
||||
await tree.waitFor({ state: "visible", timeout: 30000 });
|
||||
// The tree has expanded, let's get out of here
|
||||
return true;
|
||||
} catch {
|
||||
|
||||
@@ -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}`);
|
||||
Reference in New Issue
Block a user