mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-03-27 18:43:29 +00:00
Compare commits
11 Commits
user/bchou
...
users/sind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79dbdbbe7f | ||
|
|
3f977df00d | ||
|
|
865e9c906b | ||
|
|
1c34425dd8 | ||
|
|
50a244e6f9 | ||
|
|
9dad75c2f9 | ||
|
|
876b531248 | ||
|
|
28fe5846b3 | ||
|
|
f8533abb64 | ||
|
|
2921294a3d | ||
|
|
a03c289da0 |
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -201,18 +201,18 @@ jobs:
|
||||
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
|
||||
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-cassandra.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
|
||||
echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
|
||||
echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo32.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
|
||||
echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
|
||||
echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
# CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-cassandra.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
# echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
|
||||
# echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
# MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
# echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
|
||||
# echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
# MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo32.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
# echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
|
||||
# echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
# MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
# echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
|
||||
# echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
- name: List test files for shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
||||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --list
|
||||
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
||||
|
||||
4
.github/workflows/cleanup.yml
vendored
4
.github/workflows/cleanup.yml
vendored
@@ -6,8 +6,8 @@ on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# Once every day at 7 AM PST
|
||||
- cron: "0 13 * * *"
|
||||
# Once every two hours
|
||||
- cron: "0 */2 * * *"
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,7 +17,6 @@ Contracts/*
|
||||
failure.png
|
||||
screenshots/*
|
||||
GettingStarted-ignore*.ipynb
|
||||
src/Localization/Keys.generated.ts
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.25 1.5C9.25 1.22386 9.47386 1 9.75 1H10.25C10.5261 1 10.75 1.22386 10.75 1.5V5.5L13 7.5V9H8.75V14L8 15L7.25 14V9H3V7.5L5.25 5.5V1.5C5.25 1.22386 5.47386 1 5.75 1H6.25C6.52614 1 6.75 1.22386 6.75 1.5V5.25H9.25V1.5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 354 B |
14305
package-lock.json
generated
14305
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
69
package.json
69
package.json
@@ -4,7 +4,7 @@
|
||||
"description": "Cosmos Explorer",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "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": {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
587
preview/package-lock.json
generated
587
preview/package-lock.json
generated
@@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
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,7 +1,5 @@
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { SubscriptionType } from "../Contracts/SubscriptionType";
|
||||
import { isExpectedError } from "../Metrics/ErrorClassification";
|
||||
import { scenarioMonitor } from "../Metrics/ScenarioMonitor";
|
||||
import { userContext } from "../UserContext";
|
||||
import { ARMError } from "../Utils/arm/request";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
@@ -9,36 +7,19 @@ import { HttpStatusCodes } from "./Constants";
|
||||
import { logError } from "./Logger";
|
||||
import { sendMessage } from "./MessageHandler";
|
||||
|
||||
export interface HandleErrorOptions {
|
||||
/** Optional redacted error to use for telemetry logging instead of the original error */
|
||||
redactedError?: string | ARMError | Error;
|
||||
}
|
||||
|
||||
export const handleError = (
|
||||
error: string | ARMError | Error,
|
||||
area: string,
|
||||
consoleErrorPrefix?: string,
|
||||
options?: HandleErrorOptions,
|
||||
): void => {
|
||||
export const handleError = (error: string | ARMError | Error, area: string, consoleErrorPrefix?: string): void => {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
const errorCode = error instanceof ARMError ? error.code : undefined;
|
||||
|
||||
// logs error to data explorer console (always shows original, non-redacted message)
|
||||
// logs error to data explorer console
|
||||
const consoleErrorMessage = consoleErrorPrefix ? `${consoleErrorPrefix}:\n ${errorMessage}` : errorMessage;
|
||||
logConsoleError(consoleErrorMessage);
|
||||
|
||||
// logs error to both app insight and kusto (use redacted message if provided)
|
||||
const telemetryErrorMessage = options?.redactedError ? getErrorMessage(options.redactedError) : errorMessage;
|
||||
logError(telemetryErrorMessage, area, errorCode);
|
||||
// logs error to both app insight and kusto
|
||||
logError(errorMessage, area, errorCode);
|
||||
|
||||
// checks for errors caused by firewall and sends them to portal to handle
|
||||
sendNotificationForError(errorMessage, errorCode);
|
||||
|
||||
// Mark expected failures for health metrics (auth, firewall, permissions, etc.)
|
||||
// This ensures timeouts with expected failures emit healthy instead of unhealthy
|
||||
if (isExpectedError(error)) {
|
||||
scenarioMonitor.markExpectedFailure();
|
||||
}
|
||||
};
|
||||
|
||||
export const getErrorMessage = (error: string | Error = ""): string => {
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import { IButtonStyles, IStackStyles, ITextStyles } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
|
||||
export const getDropdownButtonStyles = (disabled: boolean): IButtonStyles => ({
|
||||
root: {
|
||||
width: "100%",
|
||||
height: "32px",
|
||||
padding: "0 28px 0 8px",
|
||||
border: "1px solid #8a8886",
|
||||
background: "#fff",
|
||||
color: "#323130",
|
||||
textAlign: "left",
|
||||
cursor: disabled ? "not-allowed" : "pointer",
|
||||
position: "relative",
|
||||
},
|
||||
flexContainer: {
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
label: {
|
||||
fontWeight: "normal",
|
||||
fontSize: "14px",
|
||||
textAlign: "left",
|
||||
},
|
||||
});
|
||||
|
||||
export const buttonLabelStyles: ITextStyles = {
|
||||
root: {
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
display: "block",
|
||||
textAlign: "left",
|
||||
},
|
||||
};
|
||||
|
||||
export const buttonWrapperStyles: React.CSSProperties = {
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
};
|
||||
|
||||
export const chevronStyles: React.CSSProperties = {
|
||||
position: "absolute",
|
||||
right: "8px",
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
pointerEvents: "none",
|
||||
fontSize: "12px",
|
||||
};
|
||||
|
||||
export const calloutContentStyles: IStackStyles = {
|
||||
root: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
};
|
||||
|
||||
export const listContainerStyles: IStackStyles = {
|
||||
root: {
|
||||
maxHeight: "300px",
|
||||
overflowY: "auto",
|
||||
},
|
||||
};
|
||||
|
||||
export const getItemStyles = (isSelected: boolean): React.CSSProperties => ({
|
||||
padding: "8px 12px",
|
||||
cursor: "pointer",
|
||||
fontSize: "14px",
|
||||
backgroundColor: isSelected ? "#e6e6e6" : "transparent",
|
||||
textAlign: "left",
|
||||
});
|
||||
|
||||
export const emptyMessageStyles: ITextStyles = {
|
||||
root: {
|
||||
padding: "8px 12px",
|
||||
color: "#605e5c",
|
||||
textAlign: "left",
|
||||
},
|
||||
};
|
||||
@@ -1,200 +0,0 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
import React from "react";
|
||||
import { SearchableDropdown } from "./SearchableDropdown";
|
||||
|
||||
interface TestItem {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
describe("SearchableDropdown", () => {
|
||||
const mockItems: TestItem[] = [
|
||||
{ id: "1", name: "Item One" },
|
||||
{ id: "2", name: "Item Two" },
|
||||
{ id: "3", name: "Item Three" },
|
||||
];
|
||||
|
||||
const defaultProps = {
|
||||
label: "Test Label",
|
||||
items: mockItems,
|
||||
selectedItem: null as TestItem | null,
|
||||
onSelect: jest.fn(),
|
||||
getKey: (item: TestItem) => item.id,
|
||||
getDisplayText: (item: TestItem) => item.name,
|
||||
placeholder: "Select an item",
|
||||
filterPlaceholder: "Filter items",
|
||||
className: "test-dropdown",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render with label and placeholder", () => {
|
||||
render(<SearchableDropdown {...defaultProps} />);
|
||||
expect(screen.getByText("Test Label")).toBeInTheDocument();
|
||||
expect(screen.getByText("Select an item")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display selected item", () => {
|
||||
const propsWithSelection = {
|
||||
...defaultProps,
|
||||
selectedItem: mockItems[0],
|
||||
};
|
||||
render(<SearchableDropdown {...propsWithSelection} />);
|
||||
expect(screen.getByText("Item One")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show 'No items found' when items array is empty", () => {
|
||||
const propsWithEmptyItems = {
|
||||
...defaultProps,
|
||||
items: [] as TestItem[],
|
||||
};
|
||||
render(<SearchableDropdown {...propsWithEmptyItems} />);
|
||||
expect(screen.getByText("No Test Labels Found")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should open dropdown when button is clicked", () => {
|
||||
render(<SearchableDropdown {...defaultProps} />);
|
||||
const button = screen.getByText("Select an item");
|
||||
fireEvent.click(button);
|
||||
expect(screen.getByPlaceholderText("Filter items")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should filter items based on search text", () => {
|
||||
render(<SearchableDropdown {...defaultProps} />);
|
||||
const button = screen.getByText("Select an item");
|
||||
fireEvent.click(button);
|
||||
|
||||
const searchBox = screen.getByPlaceholderText("Filter items");
|
||||
fireEvent.change(searchBox, { target: { value: "Two" } });
|
||||
|
||||
expect(screen.getByText("Item Two")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Item One")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Item Three")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onSelect when an item is clicked", () => {
|
||||
const onSelectMock = jest.fn();
|
||||
const propsWithMock = {
|
||||
...defaultProps,
|
||||
onSelect: onSelectMock,
|
||||
};
|
||||
render(<SearchableDropdown {...propsWithMock} />);
|
||||
|
||||
const button = screen.getByText("Select an item");
|
||||
fireEvent.click(button);
|
||||
|
||||
const item = screen.getByText("Item Two");
|
||||
fireEvent.click(item);
|
||||
|
||||
expect(onSelectMock).toHaveBeenCalledWith(mockItems[1]);
|
||||
});
|
||||
|
||||
it("should close dropdown after selecting an item", () => {
|
||||
render(<SearchableDropdown {...defaultProps} />);
|
||||
|
||||
const button = screen.getByText("Select an item");
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(screen.getByPlaceholderText("Filter items")).toBeInTheDocument();
|
||||
|
||||
const item = screen.getByText("Item One");
|
||||
fireEvent.click(item);
|
||||
|
||||
expect(screen.queryByPlaceholderText("Filter items")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should disable button when disabled prop is true", () => {
|
||||
const propsWithDisabled = {
|
||||
...defaultProps,
|
||||
disabled: true,
|
||||
};
|
||||
render(<SearchableDropdown {...propsWithDisabled} />);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should not open dropdown when disabled", () => {
|
||||
const propsWithDisabled = {
|
||||
...defaultProps,
|
||||
disabled: true,
|
||||
};
|
||||
render(<SearchableDropdown {...propsWithDisabled} />);
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(screen.queryByPlaceholderText("Filter items")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show 'No items found' when search yields no results", () => {
|
||||
render(<SearchableDropdown {...defaultProps} />);
|
||||
|
||||
const button = screen.getByText("Select an item");
|
||||
fireEvent.click(button);
|
||||
|
||||
const searchBox = screen.getByPlaceholderText("Filter items");
|
||||
fireEvent.change(searchBox, { target: { value: "Nonexistent" } });
|
||||
|
||||
expect(screen.getByText("No items found")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle case-insensitive filtering", () => {
|
||||
render(<SearchableDropdown {...defaultProps} />);
|
||||
|
||||
const button = screen.getByText("Select an item");
|
||||
fireEvent.click(button);
|
||||
|
||||
const searchBox = screen.getByPlaceholderText("Filter items");
|
||||
fireEvent.change(searchBox, { target: { value: "two" } });
|
||||
|
||||
expect(screen.getByText("Item Two")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Item One")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should clear filter text when dropdown is closed and reopened", () => {
|
||||
render(<SearchableDropdown {...defaultProps} />);
|
||||
|
||||
const button = screen.getByText("Select an item");
|
||||
fireEvent.click(button);
|
||||
|
||||
const searchBox = screen.getByPlaceholderText("Filter items");
|
||||
fireEvent.change(searchBox, { target: { value: "Two" } });
|
||||
|
||||
// Close dropdown by selecting an item
|
||||
const item = screen.getByText("Item Two");
|
||||
fireEvent.click(item);
|
||||
|
||||
// Reopen dropdown
|
||||
fireEvent.click(button);
|
||||
|
||||
// Filter text should be cleared
|
||||
const reopenedSearchBox = screen.getByPlaceholderText("Filter items");
|
||||
expect(reopenedSearchBox).toHaveValue("");
|
||||
});
|
||||
|
||||
it("should use custom placeholder text", () => {
|
||||
const propsWithCustomPlaceholder = {
|
||||
...defaultProps,
|
||||
placeholder: "Choose an option",
|
||||
};
|
||||
render(<SearchableDropdown {...propsWithCustomPlaceholder} />);
|
||||
expect(screen.getByText("Choose an option")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should use custom filter placeholder text", () => {
|
||||
const propsWithCustomFilterPlaceholder = {
|
||||
...defaultProps,
|
||||
filterPlaceholder: "Search here",
|
||||
};
|
||||
render(<SearchableDropdown {...propsWithCustomFilterPlaceholder} />);
|
||||
|
||||
const button = screen.getByText("Select an item");
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(screen.getByPlaceholderText("Search here")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,155 +0,0 @@
|
||||
import {
|
||||
Callout,
|
||||
DefaultButton,
|
||||
DirectionalHint,
|
||||
Icon,
|
||||
ISearchBoxStyles,
|
||||
Label,
|
||||
SearchBox,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
buttonLabelStyles,
|
||||
buttonWrapperStyles,
|
||||
calloutContentStyles,
|
||||
chevronStyles,
|
||||
emptyMessageStyles,
|
||||
getDropdownButtonStyles,
|
||||
getItemStyles,
|
||||
listContainerStyles,
|
||||
} from "./SearchableDropdown.styles";
|
||||
|
||||
interface SearchableDropdownProps<T> {
|
||||
label: string;
|
||||
items: T[];
|
||||
selectedItem: T | null;
|
||||
onSelect: (item: T) => void;
|
||||
getKey: (item: T) => string;
|
||||
getDisplayText: (item: T) => string;
|
||||
placeholder?: string;
|
||||
filterPlaceholder?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
onDismiss?: () => void;
|
||||
searchBoxStyles?: Partial<ISearchBoxStyles>;
|
||||
}
|
||||
|
||||
export const SearchableDropdown = <T,>({
|
||||
label,
|
||||
items,
|
||||
selectedItem,
|
||||
onSelect,
|
||||
getKey,
|
||||
getDisplayText,
|
||||
placeholder = "Select an item",
|
||||
filterPlaceholder = "Filter items",
|
||||
className,
|
||||
disabled = false,
|
||||
onDismiss,
|
||||
searchBoxStyles: customSearchBoxStyles,
|
||||
}: SearchableDropdownProps<T>): React.ReactElement => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [filterText, setFilterText] = useState("");
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const closeDropdown = () => {
|
||||
setIsOpen(false);
|
||||
setFilterText("");
|
||||
};
|
||||
|
||||
const filteredItems = useMemo(
|
||||
() => items?.filter((item) => getDisplayText(item).toLowerCase().includes(filterText.toLowerCase())),
|
||||
[items, filterText, getDisplayText],
|
||||
);
|
||||
|
||||
const handleDismiss = () => {
|
||||
closeDropdown();
|
||||
onDismiss?.();
|
||||
};
|
||||
|
||||
const handleButtonClick = () => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const handleSelect = (item: T) => {
|
||||
onSelect(item);
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const buttonLabel = selectedItem
|
||||
? getDisplayText(selectedItem)
|
||||
: items?.length === 0
|
||||
? `No ${label}s Found`
|
||||
: placeholder;
|
||||
|
||||
const buttonId = `${className}-button`;
|
||||
const buttonStyles = getDropdownButtonStyles(disabled);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Label htmlFor={buttonId}>{label}</Label>
|
||||
<div ref={buttonRef} style={buttonWrapperStyles}>
|
||||
<DefaultButton
|
||||
id={buttonId}
|
||||
className={className}
|
||||
onClick={handleButtonClick}
|
||||
styles={buttonStyles}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text styles={buttonLabelStyles}>{buttonLabel}</Text>
|
||||
</DefaultButton>
|
||||
<Icon iconName="ChevronDown" style={chevronStyles} />
|
||||
</div>
|
||||
{isOpen && (
|
||||
<Callout
|
||||
target={buttonRef.current}
|
||||
onDismiss={handleDismiss}
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
isBeakVisible={false}
|
||||
gapSpace={0}
|
||||
setInitialFocus
|
||||
>
|
||||
<Stack styles={calloutContentStyles} style={{ width: buttonRef.current?.offsetWidth || 300 }}>
|
||||
<SearchBox
|
||||
placeholder={filterPlaceholder}
|
||||
value={filterText}
|
||||
onChange={(_, newValue) => setFilterText(newValue || "")}
|
||||
styles={customSearchBoxStyles}
|
||||
showIcon={true}
|
||||
/>
|
||||
<Stack styles={listContainerStyles}>
|
||||
{filteredItems && filteredItems.length > 0 ? (
|
||||
filteredItems.map((item) => {
|
||||
const key = getKey(item);
|
||||
const isSelected = selectedItem ? getKey(selectedItem) === key : false;
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => handleSelect(item)}
|
||||
style={getItemStyles(isSelected)}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = "#f3f2f1")}
|
||||
onMouseLeave={(e) =>
|
||||
(e.currentTarget.style.backgroundColor = isSelected ? "#e6e6e6" : "transparent")
|
||||
}
|
||||
>
|
||||
<Text>{getDisplayText(item)}</Text>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Text styles={emptyMessageStyles}>No items found</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Callout>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -34,7 +34,6 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
||||
databaseId: params.databaseId,
|
||||
databaseLevelThroughput: params.databaseLevelThroughput,
|
||||
offerThroughput: params.offerThroughput,
|
||||
targetAccountOverride: params.targetAccountOverride,
|
||||
};
|
||||
await createDatabase(createDatabaseParams);
|
||||
}
|
||||
@@ -64,7 +63,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
|
||||
};
|
||||
|
||||
const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise<DataModels.Collection> => {
|
||||
if (!params.createNewDatabase && !params.targetAccountOverride) {
|
||||
if (!params.createNewDatabase) {
|
||||
const isValid = await useDatabases.getState().validateCollectionId(params.databaseId, params.collectionId);
|
||||
if (!isValid) {
|
||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||
@@ -123,9 +122,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
|
||||
};
|
||||
|
||||
const createResponse = await createUpdateSqlContainer(
|
||||
params.targetAccountOverride?.subscriptionId ?? userContext.subscriptionId,
|
||||
params.targetAccountOverride?.resourceGroup ?? userContext.resourceGroup,
|
||||
params.targetAccountOverride?.accountName ?? userContext.databaseAccount.name,
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
params.collectionId,
|
||||
rpPayload,
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
jest.mock("../../Utils/arm/request");
|
||||
jest.mock("../CosmosClient");
|
||||
jest.mock("../../Utils/arm/generatedClients/cosmos/sqlResources");
|
||||
|
||||
import ko from "knockout";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { CreateDatabaseParams, DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { useDatabases } from "../../Explorer/useDatabases";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import { createUpdateSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { SqlDatabaseGetResults } from "../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { createDatabase } from "./createDatabase";
|
||||
|
||||
const mockCreateUpdateSqlDatabase = createUpdateSqlDatabase as jest.MockedFunction<typeof createUpdateSqlDatabase>;
|
||||
|
||||
describe("createDatabase", () => {
|
||||
beforeAll(() => {
|
||||
updateUserContext({
|
||||
databaseAccount: { name: "default-account" } as DatabaseAccount,
|
||||
subscriptionId: "default-subscription",
|
||||
resourceGroup: "default-rg",
|
||||
apiType: "SQL",
|
||||
authType: AuthType.AAD,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockCreateUpdateSqlDatabase.mockResolvedValue({
|
||||
properties: { resource: { id: "db", _rid: "", _self: "", _ts: 0, _etag: "" } },
|
||||
} as SqlDatabaseGetResults);
|
||||
useDatabases.setState({
|
||||
databases: [],
|
||||
validateDatabaseId: () => true,
|
||||
} as unknown as ReturnType<typeof useDatabases.getState>);
|
||||
});
|
||||
|
||||
it("should call ARM createUpdateSqlDatabase when logged in with AAD", async () => {
|
||||
await createDatabase({ databaseId: "testDb" });
|
||||
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("targetAccountOverride behavior", () => {
|
||||
it("should use targetAccountOverride subscriptionId, resourceGroup, and accountName for SQL DB creation", async () => {
|
||||
const params: CreateDatabaseParams = {
|
||||
databaseId: "testDb",
|
||||
targetAccountOverride: {
|
||||
subscriptionId: "override-sub",
|
||||
resourceGroup: "override-rg",
|
||||
accountName: "override-account",
|
||||
},
|
||||
};
|
||||
|
||||
await createDatabase(params);
|
||||
|
||||
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith(
|
||||
"override-sub",
|
||||
"override-rg",
|
||||
"override-account",
|
||||
"testDb",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("should use userContext values when targetAccountOverride is not provided", async () => {
|
||||
await createDatabase({ databaseId: "testDb" });
|
||||
|
||||
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith(
|
||||
"default-subscription",
|
||||
"default-rg",
|
||||
"default-account",
|
||||
"testDb",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("should skip validateDatabaseId check when targetAccountOverride is provided", async () => {
|
||||
// Simulate database already existing — validateDatabaseId returns false
|
||||
useDatabases.setState({
|
||||
databases: [{ id: ko.observable("testDb") } as unknown as ViewModels.Database],
|
||||
validateDatabaseId: () => false,
|
||||
} as unknown as ReturnType<typeof useDatabases.getState>);
|
||||
|
||||
const params: CreateDatabaseParams = {
|
||||
databaseId: "testDb",
|
||||
targetAccountOverride: {
|
||||
subscriptionId: "override-sub",
|
||||
resourceGroup: "override-rg",
|
||||
accountName: "override-account",
|
||||
},
|
||||
};
|
||||
|
||||
// Should NOT throw even though the normal duplicate check would fail
|
||||
await expect(createDatabase(params)).resolves.not.toThrow();
|
||||
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw if validateDatabaseId returns false and no targetAccountOverride is set", async () => {
|
||||
useDatabases.setState({
|
||||
databases: [{ id: ko.observable("existingDb") } as unknown as ViewModels.Database],
|
||||
validateDatabaseId: () => false,
|
||||
} as unknown as ReturnType<typeof useDatabases.getState>);
|
||||
|
||||
await expect(createDatabase({ databaseId: "existingDb" })).rejects.toThrow();
|
||||
expect(mockCreateUpdateSqlDatabase).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should pass databaseId in request payload regardless of targetAccountOverride", async () => {
|
||||
const params: CreateDatabaseParams = {
|
||||
databaseId: "my-database",
|
||||
targetAccountOverride: {
|
||||
subscriptionId: "any-sub",
|
||||
resourceGroup: "any-rg",
|
||||
accountName: "any-account",
|
||||
},
|
||||
};
|
||||
|
||||
await createDatabase(params);
|
||||
|
||||
expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
"my-database",
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({
|
||||
resource: expect.objectContaining({ id: "my-database" }),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,7 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P
|
||||
}
|
||||
|
||||
async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise<DataModels.Database> {
|
||||
if (!params.targetAccountOverride && !useDatabases.getState().validateDatabaseId(params.databaseId)) {
|
||||
if (!useDatabases.getState().validateDatabaseId(params.databaseId)) {
|
||||
const databaseName = getDatabaseName().toLocaleLowerCase();
|
||||
throw new Error(`Create ${databaseName} failed: ${databaseName} with id ${params.databaseId} already exists`);
|
||||
}
|
||||
@@ -72,10 +72,13 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi
|
||||
options,
|
||||
},
|
||||
};
|
||||
const sub = params.targetAccountOverride?.subscriptionId ?? userContext.subscriptionId;
|
||||
const rg = params.targetAccountOverride?.resourceGroup ?? userContext.resourceGroup;
|
||||
const acct = params.targetAccountOverride?.accountName ?? userContext.databaseAccount.name;
|
||||
const createResponse = await createUpdateSqlDatabase(sub, rg, acct, params.databaseId, rpPayload);
|
||||
const createResponse = await createUpdateSqlDatabase(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
params.databaseId,
|
||||
rpPayload,
|
||||
);
|
||||
return createResponse && (createResponse.properties.resource as DataModels.Database);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,8 +44,7 @@ export const deleteDocuments = async (
|
||||
documentIds: DocumentId[],
|
||||
abortSignal: AbortSignal,
|
||||
): Promise<IBulkDeleteResult[]> => {
|
||||
const totalCount = documentIds.length;
|
||||
const clearMessage = logConsoleProgress(`Deleting ${totalCount} ${getEntityName(true)}`);
|
||||
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
|
||||
try {
|
||||
const v2Container = await client().database(collection.databaseId).container(collection.id());
|
||||
|
||||
@@ -84,7 +83,11 @@ export const deleteDocuments = async (
|
||||
const flatAllResult = Array.prototype.concat.apply([], allResult);
|
||||
return flatAllResult;
|
||||
} catch (error) {
|
||||
handleError(error, "DeleteDocuments", `Error while deleting ${totalCount} ${getEntityName(totalCount > 1)}`);
|
||||
handleError(
|
||||
error,
|
||||
"DeleteDocuments",
|
||||
`Error while deleting ${documentIds.length} ${getEntityName(documentIds.length > 1)}`,
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
import { redactSyntaxErrorMessage } from "./queryDocumentsPage";
|
||||
|
||||
/* Typical error to redact looks like this (the message property contains a JSON string with nested structure):
|
||||
{
|
||||
"message": "{\"code\":\"BadRequest\",\"message\":\"{\\\"errors\\\":[{\\\"severity\\\":\\\"Error\\\",\\\"location\\\":{\\\"start\\\":0,\\\"end\\\":5},\\\"code\\\":\\\"SC1001\\\",\\\"message\\\":\\\"Syntax error, incorrect syntax near 'Crazy'.\\\"}]}\\r\\nActivityId: d5424e10-51bd-46f7-9aec-7b40bed36f17, Windows/10.0.20348 cosmos-netstandard-sdk/3.18.0\"}"
|
||||
}
|
||||
*/
|
||||
|
||||
// Helper to create the nested error structure that matches what the SDK returns
|
||||
const createNestedError = (
|
||||
errors: Array<{ severity?: string; location?: { start: number; end: number }; code: string; message: string }>,
|
||||
activityId: string = "test-activity-id",
|
||||
): { message: string } => {
|
||||
const innerErrorsJson = JSON.stringify({ errors });
|
||||
const innerMessage = `${innerErrorsJson}\r\n${activityId}`;
|
||||
const outerJson = JSON.stringify({ code: "BadRequest", message: innerMessage });
|
||||
return { message: outerJson };
|
||||
};
|
||||
|
||||
// Helper to parse the redacted result
|
||||
const parseRedactedResult = (result: { message: string }) => {
|
||||
const outerParsed = JSON.parse(result.message);
|
||||
const [innerErrorsJson, activityIdPart] = outerParsed.message.split("\r\n");
|
||||
const innerErrors = JSON.parse(innerErrorsJson);
|
||||
return { outerParsed, innerErrors, activityIdPart };
|
||||
};
|
||||
|
||||
describe("redactSyntaxErrorMessage", () => {
|
||||
it("should redact SC1001 error message", () => {
|
||||
const error = createNestedError(
|
||||
[
|
||||
{
|
||||
severity: "Error",
|
||||
location: { start: 0, end: 5 },
|
||||
code: "SC1001",
|
||||
message: "Syntax error, incorrect syntax near 'Crazy'.",
|
||||
},
|
||||
],
|
||||
"ActivityId: d5424e10-51bd-46f7-9aec-7b40bed36f17",
|
||||
);
|
||||
|
||||
const result = redactSyntaxErrorMessage(error) as { message: string };
|
||||
const { outerParsed, innerErrors, activityIdPart } = parseRedactedResult(result);
|
||||
|
||||
expect(outerParsed.code).toBe("BadRequest");
|
||||
expect(innerErrors.errors[0].message).toBe("__REDACTED__");
|
||||
expect(activityIdPart).toContain("ActivityId: d5424e10-51bd-46f7-9aec-7b40bed36f17");
|
||||
});
|
||||
|
||||
it("should redact SC2001 error message", () => {
|
||||
const error = createNestedError(
|
||||
[
|
||||
{
|
||||
severity: "Error",
|
||||
location: { start: 0, end: 10 },
|
||||
code: "SC2001",
|
||||
message: "Some sensitive syntax error message.",
|
||||
},
|
||||
],
|
||||
"ActivityId: abc123",
|
||||
);
|
||||
|
||||
const result = redactSyntaxErrorMessage(error) as { message: string };
|
||||
const { outerParsed, innerErrors, activityIdPart } = parseRedactedResult(result);
|
||||
|
||||
expect(outerParsed.code).toBe("BadRequest");
|
||||
expect(innerErrors.errors[0].message).toBe("__REDACTED__");
|
||||
expect(activityIdPart).toContain("ActivityId: abc123");
|
||||
});
|
||||
|
||||
it("should redact multiple errors with SC1001 and SC2001 codes", () => {
|
||||
const error = createNestedError(
|
||||
[
|
||||
{ severity: "Error", code: "SC1001", message: "First error" },
|
||||
{ severity: "Error", code: "SC2001", message: "Second error" },
|
||||
],
|
||||
"ActivityId: xyz",
|
||||
);
|
||||
|
||||
const result = redactSyntaxErrorMessage(error) as { message: string };
|
||||
const { innerErrors } = parseRedactedResult(result);
|
||||
|
||||
expect(innerErrors.errors[0].message).toBe("__REDACTED__");
|
||||
expect(innerErrors.errors[1].message).toBe("__REDACTED__");
|
||||
});
|
||||
|
||||
it("should not redact errors with other codes", () => {
|
||||
const error = createNestedError(
|
||||
[{ severity: "Error", code: "SC9999", message: "This should not be redacted." }],
|
||||
"ActivityId: test123",
|
||||
);
|
||||
|
||||
const result = redactSyntaxErrorMessage(error);
|
||||
|
||||
expect(result).toBe(error); // Should return original error unchanged
|
||||
});
|
||||
|
||||
it("should not modify non-BadRequest errors", () => {
|
||||
const innerMessage = JSON.stringify({ errors: [{ code: "SC1001", message: "Should not be redacted" }] });
|
||||
const error = {
|
||||
message: JSON.stringify({ code: "NotFound", message: innerMessage }),
|
||||
};
|
||||
|
||||
const result = redactSyntaxErrorMessage(error);
|
||||
|
||||
expect(result).toBe(error);
|
||||
});
|
||||
|
||||
it("should handle errors without message property", () => {
|
||||
const error = { code: "BadRequest" };
|
||||
|
||||
const result = redactSyntaxErrorMessage(error);
|
||||
|
||||
expect(result).toBe(error);
|
||||
});
|
||||
|
||||
it("should handle non-object errors", () => {
|
||||
const stringError = "Simple string error";
|
||||
const nullError: null = null;
|
||||
const undefinedError: undefined = undefined;
|
||||
|
||||
expect(redactSyntaxErrorMessage(stringError)).toBe(stringError);
|
||||
expect(redactSyntaxErrorMessage(nullError)).toBe(nullError);
|
||||
expect(redactSyntaxErrorMessage(undefinedError)).toBe(undefinedError);
|
||||
});
|
||||
|
||||
it("should handle malformed JSON in message", () => {
|
||||
const error = {
|
||||
message: "not valid json",
|
||||
};
|
||||
|
||||
const result = redactSyntaxErrorMessage(error);
|
||||
|
||||
expect(result).toBe(error);
|
||||
});
|
||||
|
||||
it("should handle message without ActivityId suffix", () => {
|
||||
const innerErrorsJson = JSON.stringify({
|
||||
errors: [{ severity: "Error", code: "SC1001", message: "Syntax error near something." }],
|
||||
});
|
||||
const error = {
|
||||
message: JSON.stringify({ code: "BadRequest", message: innerErrorsJson + "\r\n" }),
|
||||
};
|
||||
|
||||
const result = redactSyntaxErrorMessage(error) as { message: string };
|
||||
const { innerErrors } = parseRedactedResult(result);
|
||||
|
||||
expect(innerErrors.errors[0].message).toBe("__REDACTED__");
|
||||
});
|
||||
|
||||
it("should preserve other error properties", () => {
|
||||
const baseError = createNestedError([{ code: "SC1001", message: "Error" }], "ActivityId: test");
|
||||
const error = {
|
||||
...baseError,
|
||||
statusCode: 400,
|
||||
additionalInfo: "extra data",
|
||||
};
|
||||
|
||||
const result = redactSyntaxErrorMessage(error) as {
|
||||
message: string;
|
||||
statusCode: number;
|
||||
additionalInfo: string;
|
||||
};
|
||||
|
||||
expect(result.statusCode).toBe(400);
|
||||
expect(result.additionalInfo).toBe("extra data");
|
||||
|
||||
const { innerErrors } = parseRedactedResult(result);
|
||||
expect(innerErrors.errors[0].message).toBe("__REDACTED__");
|
||||
});
|
||||
});
|
||||
@@ -4,51 +4,6 @@ import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { MinimalQueryIterator, nextPage } from "../IteratorUtilities";
|
||||
|
||||
// Redact sensitive information from BadRequest errors with specific codes
|
||||
export const redactSyntaxErrorMessage = (error: unknown): unknown => {
|
||||
const codesToRedact = ["SC1001", "SC2001", "SC1010"];
|
||||
|
||||
try {
|
||||
// Handle error objects with a message property
|
||||
if (error && typeof error === "object" && "message" in error) {
|
||||
const errorObj = error as { code?: string; message?: string };
|
||||
if (typeof errorObj.message === "string") {
|
||||
// Parse the inner JSON from the message
|
||||
const innerJson = JSON.parse(errorObj.message);
|
||||
if (innerJson.code === "BadRequest" && typeof innerJson.message === "string") {
|
||||
const [innerErrorsJson, activityIdPart] = innerJson.message.split("\r\n");
|
||||
const innerErrorsObj = JSON.parse(innerErrorsJson);
|
||||
if (Array.isArray(innerErrorsObj.errors)) {
|
||||
let modified = false;
|
||||
innerErrorsObj.errors = innerErrorsObj.errors.map((err: { code?: string; message?: string }) => {
|
||||
if (err.code && codesToRedact.includes(err.code)) {
|
||||
modified = true;
|
||||
return { ...err, message: "__REDACTED__" };
|
||||
}
|
||||
return err;
|
||||
});
|
||||
|
||||
if (modified) {
|
||||
// Reconstruct the message with the redacted content
|
||||
const redactedMessage = JSON.stringify(innerErrorsObj) + `\r\n${activityIdPart}`;
|
||||
const redactedError = {
|
||||
...error,
|
||||
message: JSON.stringify({ ...innerJson, message: redactedMessage }),
|
||||
body: undefined as unknown, // Clear body to avoid sensitive data
|
||||
};
|
||||
return redactedError;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If parsing fails, return the original error
|
||||
}
|
||||
|
||||
return error;
|
||||
};
|
||||
|
||||
export const queryDocumentsPage = async (
|
||||
resourceName: string,
|
||||
documentsIterator: MinimalQueryIterator,
|
||||
@@ -63,12 +18,7 @@ export const queryDocumentsPage = async (
|
||||
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Redact sensitive information for telemetry while showing original in console
|
||||
const redactedError = redactSyntaxErrorMessage(error);
|
||||
|
||||
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`, {
|
||||
redactedError: redactedError as Error,
|
||||
});
|
||||
handleError(error, "QueryDocumentsPage", `Failed to query ${entityName} for container ${resourceName}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ContainerResponse } from "@azure/cosmos";
|
||||
import { Queries } from "Common/Constants";
|
||||
import * as Logger from "Common/Logger";
|
||||
import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||
import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
@@ -62,14 +61,7 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
||||
return await readCollectionsWithARM(databaseId);
|
||||
}
|
||||
|
||||
Logger.logInfo(`readCollections: calling fetchAll for database ${databaseId}`, "readCollections");
|
||||
const fetchAllStart = Date.now();
|
||||
const sdkResponse = await client().database(databaseId).containers.readAll().fetchAll();
|
||||
Logger.logInfo(
|
||||
`readCollections: fetchAll completed for database ${databaseId}, count=${sdkResponse.resources
|
||||
?.length}, durationMs=${Date.now() - fetchAllStart}`,
|
||||
"readCollections",
|
||||
);
|
||||
return sdkResponse.resources as DataModels.Collection[];
|
||||
} catch (error) {
|
||||
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
jest.mock("../../Utils/arm/request");
|
||||
jest.mock("../CosmosClient");
|
||||
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import { armRequest } from "../../Utils/arm/request";
|
||||
import { client } from "../CosmosClient";
|
||||
import { readDatabases, readDatabasesForAccount } from "./readDatabases";
|
||||
import { readDatabases } from "./readDatabases";
|
||||
|
||||
describe("readDatabases", () => {
|
||||
beforeAll(() => {
|
||||
@@ -43,64 +42,3 @@ describe("readDatabases", () => {
|
||||
expect(client).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("readDatabasesForAccount", () => {
|
||||
const mockDatabase = { id: "testDb", _rid: "", _self: "", _etag: "", _ts: 0 };
|
||||
const mockArmResponse = { value: [{ properties: { resource: mockDatabase } }] };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should call ARM with a path that includes the provided subscriptionId, resourceGroup, and accountName", async () => {
|
||||
(armRequest as jest.Mock).mockResolvedValue(mockArmResponse);
|
||||
|
||||
await readDatabasesForAccount("test-sub", "test-rg", "test-account");
|
||||
|
||||
expect(armRequest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
path: expect.stringContaining("/subscriptions/test-sub/resourceGroups/test-rg/"),
|
||||
}),
|
||||
);
|
||||
expect(armRequest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
path: expect.stringContaining("/databaseAccounts/test-account/sqlDatabases"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return mapped database resources from the response", async () => {
|
||||
const db1 = { id: "db1", _rid: "r1", _self: "/dbs/db1", _etag: "", _ts: 1 };
|
||||
const db2 = { id: "db2", _rid: "r2", _self: "/dbs/db2", _etag: "", _ts: 2 };
|
||||
|
||||
(armRequest as jest.Mock).mockResolvedValue({
|
||||
value: [{ properties: { resource: db1 } }, { properties: { resource: db2 } }],
|
||||
});
|
||||
|
||||
const result = await readDatabasesForAccount("sub", "rg", "account");
|
||||
|
||||
expect(result).toEqual([db1, db2]);
|
||||
});
|
||||
|
||||
it("should return an empty array when the response is null", async () => {
|
||||
(armRequest as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
const result = await readDatabasesForAccount("sub", "rg", "account");
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return an empty array when value is an empty list", async () => {
|
||||
(armRequest as jest.Mock).mockResolvedValue({ value: [] });
|
||||
|
||||
const result = await readDatabasesForAccount("sub", "rg", "account");
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should throw and propagate errors from the ARM call", async () => {
|
||||
(armRequest as jest.Mock).mockRejectedValue(new Error("ARM request failed"));
|
||||
|
||||
await expect(readDatabasesForAccount("sub", "rg", "account")).rejects.toThrow("ARM request failed");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,20 +112,3 @@ async function readDatabasesWithARM(): Promise<DataModels.Database[]> {
|
||||
|
||||
return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database);
|
||||
}
|
||||
|
||||
export async function readDatabasesForAccount(
|
||||
subscriptionId: string,
|
||||
resourceGroup: string,
|
||||
accountName: string,
|
||||
): Promise<DataModels.Database[]> {
|
||||
const clearMessage = logConsoleProgress(`Querying databases for account ${accountName}`);
|
||||
try {
|
||||
const rpResponse = await listSqlDatabases(subscriptionId, resourceGroup, accountName);
|
||||
return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database) ?? [];
|
||||
} catch (error) {
|
||||
handleError(error, "ReadDatabasesForAccount", `Error while querying databases for account ${accountName}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -46,10 +46,6 @@ export type DataExploreMessageV3 =
|
||||
params: {
|
||||
updateType: "created" | "deleted" | "settings";
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: FabricMessageTypes.RestoreContainer;
|
||||
params: [];
|
||||
};
|
||||
export interface GetCosmosTokenMessageOptions {
|
||||
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";
|
||||
|
||||
@@ -275,7 +275,8 @@ export interface DataMaskingPolicy {
|
||||
startPosition: number;
|
||||
length: number;
|
||||
}>;
|
||||
excludedPaths?: string[];
|
||||
excludedPaths: string[];
|
||||
isPolicyEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface MaterializedView {
|
||||
@@ -404,18 +405,11 @@ export interface AutoPilotOfferSettings {
|
||||
targetMaxThroughput?: number;
|
||||
}
|
||||
|
||||
export interface AccountOverride {
|
||||
subscriptionId: string;
|
||||
resourceGroup: string;
|
||||
accountName: string;
|
||||
}
|
||||
|
||||
export interface CreateDatabaseParams {
|
||||
autoPilotMaxThroughput?: number;
|
||||
databaseId: string;
|
||||
databaseLevelThroughput?: boolean;
|
||||
offerThroughput?: number;
|
||||
targetAccountOverride?: AccountOverride;
|
||||
}
|
||||
|
||||
export interface CreateCollectionParamsBase {
|
||||
@@ -435,7 +429,6 @@ export interface CreateCollectionParamsBase {
|
||||
export interface CreateCollectionParams extends CreateCollectionParamsBase {
|
||||
createNewDatabase: boolean;
|
||||
collectionId: string;
|
||||
targetAccountOverride?: AccountOverride;
|
||||
}
|
||||
|
||||
export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase {
|
||||
|
||||
@@ -457,13 +457,13 @@ describe("CopyJobActions", () => {
|
||||
jobName: "test-job",
|
||||
migrationType: "online" as any,
|
||||
source: {
|
||||
subscriptionId: "sub-123",
|
||||
subscription: {} as any,
|
||||
account: { id: "account-1", name: "source-account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscription: {} as any,
|
||||
subscriptionId: "sub-123",
|
||||
account: { id: "account-1", name: "target-account" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
@@ -498,7 +498,7 @@ describe("CopyJobActions", () => {
|
||||
);
|
||||
|
||||
const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4];
|
||||
expect(callArgs.properties.destination.remoteAccountName).toBeUndefined();
|
||||
expect(callArgs.properties.source.remoteAccountName).toBeUndefined();
|
||||
|
||||
expect(mockRefreshJobList).toHaveBeenCalled();
|
||||
expect(mockOnSuccess).toHaveBeenCalled();
|
||||
@@ -509,13 +509,13 @@ describe("CopyJobActions", () => {
|
||||
jobName: "cross-account-job",
|
||||
migrationType: "offline" as any,
|
||||
source: {
|
||||
subscriptionId: "sub-123",
|
||||
subscription: {} as any,
|
||||
account: { id: "account-1", name: "source-account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscription: {} as any,
|
||||
subscriptionId: "sub-456",
|
||||
account: { id: "account-2", name: "target-account" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
@@ -528,7 +528,7 @@ describe("CopyJobActions", () => {
|
||||
await submitCreateCopyJob(mockState, mockOnSuccess);
|
||||
|
||||
const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4];
|
||||
expect(callArgs.properties.destination.remoteAccountName).toBe("target-account");
|
||||
expect(callArgs.properties.source.remoteAccountName).toBe("source-account");
|
||||
expect(mockOnSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -537,13 +537,13 @@ describe("CopyJobActions", () => {
|
||||
jobName: "failing-job",
|
||||
migrationType: "online" as any,
|
||||
source: {
|
||||
subscriptionId: "sub-123",
|
||||
subscription: {} as any,
|
||||
account: { id: "account-1", name: "source-account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscription: {} as any,
|
||||
subscriptionId: "sub-123",
|
||||
account: { id: "account-1", name: "target-account" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
@@ -566,13 +566,13 @@ describe("CopyJobActions", () => {
|
||||
jobName: "test-job",
|
||||
migrationType: "online" as any,
|
||||
source: {
|
||||
subscriptionId: "sub-123",
|
||||
subscription: {} as any,
|
||||
account: { id: "account-1", name: "source-account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscription: {} as any,
|
||||
subscriptionId: "sub-123",
|
||||
account: { id: "account-1", name: "target-account" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
|
||||
@@ -137,12 +137,12 @@ export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess:
|
||||
properties: {
|
||||
source: {
|
||||
component: "CosmosDBSql",
|
||||
...(isSameAccount ? {} : { remoteAccountName: source?.account?.name }),
|
||||
databaseName: source?.databaseId,
|
||||
containerName: source?.containerId,
|
||||
},
|
||||
destination: {
|
||||
component: "CosmosDBSql",
|
||||
...(isSameAccount ? {} : { remoteAccountName: target?.account?.name }),
|
||||
databaseName: target?.databaseId,
|
||||
containerName: target?.containerId,
|
||||
},
|
||||
|
||||
@@ -185,10 +185,9 @@ describe("CommandBar Utils", () => {
|
||||
it("should respect disabled state when provided", () => {
|
||||
const buttons = getCommandBarButtons(mockExplorer, false);
|
||||
|
||||
// Theme toggle (index 2) is disabled in Portal mode, others are not
|
||||
const expectedDisabled = buttons.map((_, index) => index === 2);
|
||||
const actualDisabled = buttons.map((button) => button.disabled);
|
||||
expect(actualDisabled).toEqual(expectedDisabled);
|
||||
buttons.forEach((button) => {
|
||||
expect(button.disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return CommandButtonComponentProps with all required properties", () => {
|
||||
|
||||
@@ -14,7 +14,6 @@ import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
|
||||
|
||||
function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommandBarBtnType[] {
|
||||
const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref);
|
||||
const isPortal = configContext.platform === Platform.Portal;
|
||||
const buttons: CopyJobCommandBarBtnType[] = [
|
||||
{
|
||||
key: "createCopyJob",
|
||||
@@ -34,13 +33,8 @@ function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommand
|
||||
key: "themeToggle",
|
||||
iconSrc: isDarkMode ? SunIcon : MoonIcon,
|
||||
label: isDarkMode ? "Light Theme" : "Dark Theme",
|
||||
ariaLabel: isPortal
|
||||
? "Dark Mode is managed in Azure Portal Settings"
|
||||
: isDarkMode
|
||||
? "Switch to Light Theme"
|
||||
: "Switch to Dark Theme",
|
||||
disabled: isPortal,
|
||||
onClick: isPortal ? () => {} : () => useThemeStore.getState().toggleTheme(),
|
||||
ariaLabel: isDarkMode ? "Switch to Light Theme" : "Switch to Dark Theme",
|
||||
onClick: () => useThemeStore.getState().toggleTheme(),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@ export default {
|
||||
createCopyJobPanelTitle: "Create copy job",
|
||||
|
||||
// Select Account Screen
|
||||
selectAccountDescription: "Please select a destination account to copy to.",
|
||||
selectAccountDescription: "Please select a source account from which to copy.",
|
||||
subscriptionDropdownLabel: "Subscription",
|
||||
subscriptionDropdownPlaceholder: "Select a subscription",
|
||||
destinationAccountDropdownLabel: "Account",
|
||||
destinationAccountDropdownPlaceholder: "Select an account",
|
||||
sourceAccountDropdownLabel: "Account",
|
||||
sourceAccountDropdownPlaceholder: "Select an account",
|
||||
migrationTypeOptions: {
|
||||
offline: {
|
||||
title: "Offline mode",
|
||||
@@ -47,17 +47,14 @@ export default {
|
||||
databaseDropdownPlaceholder: "Select a database",
|
||||
containerDropdownLabel: "Container",
|
||||
containerDropdownPlaceholder: "Select a container",
|
||||
createNewContainerSubHeading: (accountName?: string) =>
|
||||
accountName
|
||||
? `Configure the properties for the new container on destination account "${accountName}".`
|
||||
: "Configure the properties for the new container.",
|
||||
createNewContainerSubHeading: "Select the properties for your container.",
|
||||
createContainerButtonLabel: "Create a new container",
|
||||
createContainerHeading: "Create new container",
|
||||
|
||||
// Preview and Create Screen
|
||||
jobNameLabel: "Job name",
|
||||
destinationSubscriptionLabel: "Destination subscription",
|
||||
destinationAccountLabel: "Destination account",
|
||||
sourceSubscriptionLabel: "Source subscription",
|
||||
sourceAccountLabel: "Source account",
|
||||
sourceDatabaseLabel: "Source database",
|
||||
sourceContainerLabel: "Source container",
|
||||
targetDatabaseLabel: "Destination database",
|
||||
@@ -66,7 +63,7 @@ export default {
|
||||
// Assign Permissions Screen
|
||||
assignPermissions: {
|
||||
crossAccountDescription:
|
||||
"To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write access to the source account by completing the following steps.",
|
||||
"To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.",
|
||||
intraAccountOnlineDescription: (accountName: string) =>
|
||||
`Follow the steps below to enable online copy on your "${accountName}" account.`,
|
||||
crossAccountConfiguration: {
|
||||
@@ -119,18 +116,18 @@ export default {
|
||||
popoverDescription: (accountName: string) =>
|
||||
`Assign the system-assigned managed identity as the default for "${accountName}". To confirm, click the "Yes" button. `,
|
||||
},
|
||||
readWritePermissionAssigned: {
|
||||
title: "Read-write permissions assigned to the default identity.",
|
||||
readPermissionAssigned: {
|
||||
title: "Read permissions assigned to the default identity.",
|
||||
description:
|
||||
"To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.",
|
||||
"To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.",
|
||||
tooltip: {
|
||||
content: "Learn more about",
|
||||
hrefText: "Read-write permissions.",
|
||||
hrefText: "Read permissions.",
|
||||
href: "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
|
||||
},
|
||||
popoverTitle: "Assign read-write permissions to default identity.",
|
||||
popoverTitle: "Read permissions assigned to default identity.",
|
||||
popoverDescription:
|
||||
'Assign read-write permissions on the source account to the default identity of the destination account. To confirm, click the "Yes" button.',
|
||||
"Assign read permissions of the source account to the default identity of the destination account. To confirm click the “Yes” button.",
|
||||
},
|
||||
pointInTimeRestore: {
|
||||
title: "Point In Time Restore enabled",
|
||||
@@ -187,10 +184,5 @@ export default {
|
||||
Skipped: "Cancelled",
|
||||
Cancelled: "Cancelled",
|
||||
},
|
||||
dialog: {
|
||||
heading: "",
|
||||
confirmButtonText: "Confirm",
|
||||
cancelButtonText: "Cancel",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -59,6 +59,12 @@ describe("CopyJobContext", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscription: null,
|
||||
account: null,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: {
|
||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
|
||||
@@ -69,13 +75,7 @@ describe("CopyJobContext", () => {
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscription: null,
|
||||
account: null,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
sourceReadAccessFromTarget: false,
|
||||
});
|
||||
expect(contextValue.flow).toBeNull();
|
||||
expect(contextValue.contextError).toBeNull();
|
||||
@@ -598,8 +598,8 @@ describe("CopyJobContext", () => {
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.copyJobState.source?.subscriptionId).toBe("test-subscription-id");
|
||||
expect(contextValue.copyJobState.source?.account?.name).toBe("test-account");
|
||||
expect(contextValue.copyJobState.source?.subscription?.subscriptionId).toBeUndefined();
|
||||
expect(contextValue.copyJobState.source?.account?.name).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should initialize target with userContext values", () => {
|
||||
@@ -616,11 +616,11 @@ describe("CopyJobContext", () => {
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.copyJobState.target.subscription).toBeNull();
|
||||
expect(contextValue.copyJobState.target.account).toBeNull();
|
||||
expect(contextValue.copyJobState.target.subscriptionId).toBe("test-subscription-id");
|
||||
expect(contextValue.copyJobState.target.account.name).toBe("test-account");
|
||||
});
|
||||
|
||||
it("should initialize sourceReadWriteAccessFromTarget as false", () => {
|
||||
it("should initialize sourceReadAccessFromTarget as false", () => {
|
||||
let contextValue: any;
|
||||
|
||||
render(
|
||||
@@ -634,7 +634,7 @@ describe("CopyJobContext", () => {
|
||||
</CopyJobContextProvider>,
|
||||
);
|
||||
|
||||
expect(contextValue.copyJobState.sourceReadWriteAccessFromTarget).toBe(false);
|
||||
expect(contextValue.copyJobState.sourceReadAccessFromTarget).toBe(false);
|
||||
});
|
||||
|
||||
it("should initialize with empty database and container ids", () => {
|
||||
|
||||
@@ -23,18 +23,18 @@ const getInitialCopyJobState = (): CopyJobContextState => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: userContext.subscriptionId || "",
|
||||
account: userContext.databaseAccount || null,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscription: null,
|
||||
account: null,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
target: {
|
||||
subscriptionId: userContext.subscriptionId || "",
|
||||
account: userContext.databaseAccount || null,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("AddManagedIdentity", () => {
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
sourceReadAccessFromTarget: false,
|
||||
};
|
||||
|
||||
const mockContextValue = {
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from "react";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { CopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { CopyJobContextProviderType } from "../../../Types/CopyJobTypes";
|
||||
import AddReadWritePermissionToDefaultIdentity from "./AddReadWritePermissionToDefaultIdentity";
|
||||
import AddReadPermissionToDefaultIdentity from "./AddReadPermissionToDefaultIdentity";
|
||||
|
||||
jest.mock("../../../../../Common/Logger", () => ({
|
||||
logError: jest.fn(),
|
||||
@@ -73,7 +73,7 @@ import { assignRole, RoleAssignmentType } from "../../../../../Utils/arm/RbacUti
|
||||
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
|
||||
import useToggle from "./hooks/useToggle";
|
||||
|
||||
describe("AddReadWritePermissionToDefaultIdentity Component", () => {
|
||||
describe("AddReadPermissionToDefaultIdentity Component", () => {
|
||||
const mockUseToggle = useToggle as jest.MockedFunction<typeof useToggle>;
|
||||
const mockAssignRole = assignRole as jest.MockedFunction<typeof assignRole>;
|
||||
const mockGetAccountDetailsFromResourceId = getAccountDetailsFromResourceId as jest.MockedFunction<
|
||||
@@ -86,7 +86,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
|
||||
jobName: "test-job",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: "source-sub-id",
|
||||
subscription: { subscriptionId: "source-sub-id" } as Subscription,
|
||||
account: {
|
||||
id: "/subscriptions/source-sub-id/resourceGroups/source-rg/providers/Microsoft.DocumentDB/databaseAccounts/source-account",
|
||||
name: "source-account",
|
||||
@@ -101,7 +101,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscription: { subscriptionId: "target-sub-id" } as Subscription,
|
||||
subscriptionId: "target-sub-id",
|
||||
account: {
|
||||
id: "/subscriptions/target-sub-id/resourceGroups/target-rg/providers/Microsoft.DocumentDB/databaseAccounts/target-account",
|
||||
name: "target-account",
|
||||
@@ -119,7 +119,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
sourceReadAccessFromTarget: false,
|
||||
},
|
||||
setCopyJobState: jest.fn(),
|
||||
setContextError: jest.fn(),
|
||||
@@ -133,7 +133,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
|
||||
const renderComponent = (contextValue = mockContextValue) => {
|
||||
return render(
|
||||
<CopyJobContext.Provider value={contextValue}>
|
||||
<AddReadWritePermissionToDefaultIdentity />
|
||||
<AddReadPermissionToDefaultIdentity />
|
||||
</CopyJobContext.Provider>,
|
||||
);
|
||||
};
|
||||
@@ -164,12 +164,12 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render correctly when sourceReadWriteAccessFromTarget is true", () => {
|
||||
it("should render correctly when sourceReadAccessFromTarget is true", () => {
|
||||
const contextWithAccess = {
|
||||
...mockContextValue,
|
||||
copyJobState: {
|
||||
...mockContextValue.copyJobState,
|
||||
sourceReadWriteAccessFromTarget: true,
|
||||
sourceReadAccessFromTarget: true,
|
||||
},
|
||||
};
|
||||
const { container } = renderComponent(contextWithAccess);
|
||||
@@ -180,7 +180,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
|
||||
describe("Component Structure", () => {
|
||||
it("should display the description text", () => {
|
||||
renderComponent();
|
||||
expect(screen.getByText(ContainerCopyMessages.readWritePermissionAssigned.description)).toBeInTheDocument();
|
||||
expect(screen.getByText(ContainerCopyMessages.readPermissionAssigned.description)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display the info tooltip", () => {
|
||||
@@ -212,10 +212,10 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
|
||||
|
||||
expect(screen.getByTestId("popover-message")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("popover-title")).toHaveTextContent(
|
||||
ContainerCopyMessages.readWritePermissionAssigned.popoverTitle,
|
||||
ContainerCopyMessages.readPermissionAssigned.popoverTitle,
|
||||
);
|
||||
expect(screen.getByTestId("popover-content")).toHaveTextContent(
|
||||
ContainerCopyMessages.readWritePermissionAssigned.popoverDescription,
|
||||
ContainerCopyMessages.readPermissionAssigned.popoverDescription,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -243,7 +243,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
|
||||
expect(mockOnToggle).toHaveBeenCalledWith(null, false);
|
||||
});
|
||||
|
||||
it("should call handleAddReadWritePermission when primary button is clicked", async () => {
|
||||
it("should call handleAddReadPermission when primary button is clicked", async () => {
|
||||
mockGetAccountDetailsFromResourceId.mockReturnValue({
|
||||
subscriptionId: "source-sub-id",
|
||||
resourceGroup: "source-rg",
|
||||
@@ -264,7 +264,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleAddReadWritePermission Function", () => {
|
||||
describe("handleAddReadPermission Function", () => {
|
||||
beforeEach(() => {
|
||||
mockUseToggle.mockReturnValue([true, jest.fn()]);
|
||||
});
|
||||
@@ -312,7 +312,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
|
||||
await waitFor(() => {
|
||||
expect(mockLogError).toHaveBeenCalledWith(
|
||||
"Permission denied",
|
||||
"CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission",
|
||||
"CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -336,14 +336,14 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockLogError).toHaveBeenCalledWith(
|
||||
"Error assigning read-write permission to default identity. Please try again later.",
|
||||
"CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission",
|
||||
"Error assigning read permission to default identity. Please try again later.",
|
||||
"CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission",
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockContextValue.setContextError).toHaveBeenCalledWith(
|
||||
"Error assigning read-write permission to default identity. Please try again later.",
|
||||
"Error assigning read permission to default identity. Please try again later.",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -496,7 +496,7 @@ describe("AddReadWritePermissionToDefaultIdentity Component", () => {
|
||||
|
||||
expect(updatedState).toEqual({
|
||||
...mockContextValue.copyJobState,
|
||||
sourceReadWriteAccessFromTarget: true,
|
||||
sourceReadAccessFromTarget: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,29 +12,27 @@ import useToggle from "./hooks/useToggle";
|
||||
|
||||
const TooltipContent = (
|
||||
<Text>
|
||||
{ContainerCopyMessages.readWritePermissionAssigned.tooltip.content}
|
||||
{ContainerCopyMessages.readPermissionAssigned.tooltip.content}
|
||||
<Link
|
||||
style={{ color: "var(--colorBrandForeground1)" }}
|
||||
href={ContainerCopyMessages.readWritePermissionAssigned.tooltip.href}
|
||||
href={ContainerCopyMessages.readPermissionAssigned.tooltip.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{ContainerCopyMessages.readWritePermissionAssigned.tooltip.hrefText}
|
||||
{ContainerCopyMessages.readPermissionAssigned.tooltip.hrefText}
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
type AddReadPermissionToDefaultIdentityProps = Partial<PermissionSectionConfig>;
|
||||
|
||||
type AddReadWritePermissionToDefaultIdentityProps = Partial<PermissionSectionConfig>;
|
||||
|
||||
const AddReadWritePermissionToDefaultIdentity: React.FC<AddReadWritePermissionToDefaultIdentityProps> = () => {
|
||||
const AddReadPermissionToDefaultIdentity: React.FC<AddReadPermissionToDefaultIdentityProps> = () => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext();
|
||||
const [readWritePermissionAssigned, onToggle] = useToggle(copyJobState.sourceReadWriteAccessFromTarget ?? false);
|
||||
const [readPermissionAssigned, onToggle] = useToggle(false);
|
||||
|
||||
const handleAddReadWritePermission = async () => {
|
||||
const handleAddReadPermission = async () => {
|
||||
const { source, target } = copyJobState;
|
||||
const selectedSourceAccount = source?.account;
|
||||
|
||||
try {
|
||||
const {
|
||||
subscriptionId: sourceSubscriptionId,
|
||||
@@ -49,17 +47,16 @@ const AddReadWritePermissionToDefaultIdentity: React.FC<AddReadWritePermissionTo
|
||||
sourceAccountName,
|
||||
target?.account?.identity?.principalId ?? "",
|
||||
);
|
||||
|
||||
if (assignedRole) {
|
||||
setCopyJobState((prevState) => ({
|
||||
...prevState,
|
||||
sourceReadWriteAccessFromTarget: true,
|
||||
sourceReadAccessFromTarget: true,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error.message || "Error assigning read-write permission to default identity. Please try again later.";
|
||||
logError(errorMessage, "CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission");
|
||||
error.message || "Error assigning read permission to default identity. Please try again later.";
|
||||
logError(errorMessage, "CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission");
|
||||
setContextError(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -69,12 +66,12 @@ const AddReadWritePermissionToDefaultIdentity: React.FC<AddReadWritePermissionTo
|
||||
return (
|
||||
<Stack className="defaultManagedIdentityContainer" tokens={{ childrenGap: 15, padding: "0 0 0 20px" }}>
|
||||
<Text className="toggle-label">
|
||||
{ContainerCopyMessages.readWritePermissionAssigned.description} 
|
||||
{ContainerCopyMessages.readPermissionAssigned.description} 
|
||||
<InfoTooltip content={TooltipContent} />
|
||||
</Text>
|
||||
<Toggle
|
||||
data-test="btn-toggle"
|
||||
checked={readWritePermissionAssigned}
|
||||
checked={readPermissionAssigned}
|
||||
onText={ContainerCopyMessages.toggleBtn.onText}
|
||||
offText={ContainerCopyMessages.toggleBtn.offText}
|
||||
onChange={onToggle}
|
||||
@@ -86,15 +83,15 @@ const AddReadWritePermissionToDefaultIdentity: React.FC<AddReadWritePermissionTo
|
||||
/>
|
||||
<PopoverMessage
|
||||
isLoading={loading}
|
||||
visible={readWritePermissionAssigned}
|
||||
title={ContainerCopyMessages.readWritePermissionAssigned.popoverTitle}
|
||||
visible={readPermissionAssigned}
|
||||
title={ContainerCopyMessages.readPermissionAssigned.popoverTitle}
|
||||
onCancel={() => onToggle(null, false)}
|
||||
onPrimary={handleAddReadWritePermission}
|
||||
onPrimary={handleAddReadPermission}
|
||||
>
|
||||
{ContainerCopyMessages.readWritePermissionAssigned.popoverDescription}
|
||||
{ContainerCopyMessages.readPermissionAssigned.popoverDescription}
|
||||
</PopoverMessage>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddReadWritePermissionToDefaultIdentity;
|
||||
export default AddReadPermissionToDefaultIdentity;
|
||||
@@ -43,12 +43,12 @@ jest.mock("./AddManagedIdentity", () => {
|
||||
return MockAddManagedIdentity;
|
||||
});
|
||||
|
||||
jest.mock("./AddReadWritePermissionToDefaultIdentity", () => {
|
||||
const MockAddReadWritePermissionToDefaultIdentity = () => {
|
||||
return <div data-testid="add-read-write-permission">Add Read-Write Permission Component</div>;
|
||||
jest.mock("./AddReadPermissionToDefaultIdentity", () => {
|
||||
const MockAddReadPermissionToDefaultIdentity = () => {
|
||||
return <div data-testid="add-read-permission">Add Read Permission Component</div>;
|
||||
};
|
||||
MockAddReadWritePermissionToDefaultIdentity.displayName = "MockAddReadWritePermissionToDefaultIdentity";
|
||||
return MockAddReadWritePermissionToDefaultIdentity;
|
||||
MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity";
|
||||
return MockAddReadPermissionToDefaultIdentity;
|
||||
});
|
||||
|
||||
jest.mock("./DefaultManagedIdentity", () => {
|
||||
@@ -85,18 +85,18 @@ describe("AssignPermissions Component", () => {
|
||||
jobName: "test-job",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: "source-sub",
|
||||
subscription: { subscriptionId: "source-sub" } as any,
|
||||
account: { id: "source-account", name: "Source Account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscription: { subscriptionId: "target-sub" } as any,
|
||||
subscriptionId: "target-sub",
|
||||
account: { id: "target-account", name: "Target Account" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
sourceReadAccessFromTarget: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
@@ -164,13 +164,13 @@ describe("AssignPermissions Component", () => {
|
||||
const copyJobState = createMockCopyJobState({
|
||||
migrationType: CopyJobMigrationType.Online,
|
||||
source: {
|
||||
subscriptionId: "same-sub",
|
||||
subscription: { subscriptionId: "same-sub" } as any,
|
||||
account: { id: "same-account", name: "Same Account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscription: { subscriptionId: "same-sub" } as any,
|
||||
subscriptionId: "same-sub",
|
||||
account: { id: "same-account", name: "Same Account" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
@@ -201,7 +201,7 @@ describe("AssignPermissions Component", () => {
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
id: "readWritePermissionAssigned",
|
||||
id: "readPermissionAssigned",
|
||||
title: "Read Permission Assigned",
|
||||
Component: () => <div data-testid="add-read-permission">Add Read Permission Component</div>,
|
||||
disabled: false,
|
||||
@@ -347,7 +347,7 @@ describe("AssignPermissions Component", () => {
|
||||
it("should handle missing account names", () => {
|
||||
const copyJobState = createMockCopyJobState({
|
||||
source: {
|
||||
subscriptionId: "source-sub",
|
||||
subscription: { subscriptionId: "source-sub" } as any,
|
||||
account: { id: "source-account" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
|
||||
@@ -50,18 +50,18 @@ describe("PointInTimeRestore", () => {
|
||||
jobName: "test-job",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: "test-sub",
|
||||
subscription: { subscriptionId: "test-sub", displayName: "Test Subscription" },
|
||||
account: mockSourceAccount,
|
||||
databaseId: "test-db",
|
||||
containerId: "test-container",
|
||||
},
|
||||
target: {
|
||||
subscription: { subscriptionId: "test-sub", displayName: "Test Subscription" },
|
||||
subscriptionId: "test-sub",
|
||||
account: mockSourceAccount,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
sourceReadAccessFromTarget: false,
|
||||
} as CopyJobContextState;
|
||||
|
||||
const mockSetCopyJobState = jest.fn();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should handle missing source account 1`] = `
|
||||
exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle missing source account 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack defaultManagedIdentityContainer css-109"
|
||||
@@ -8,7 +8,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should han
|
||||
<span
|
||||
class="toggle-label css-110"
|
||||
>
|
||||
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
|
||||
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
|
||||
|
||||
<div
|
||||
data-testid="info-tooltip"
|
||||
@@ -24,7 +24,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should han
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Read-write permissions.
|
||||
Read permissions.
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should han
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should handle missing target account identity 1`] = `
|
||||
exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle missing target account identity 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack defaultManagedIdentityContainer css-109"
|
||||
@@ -71,7 +71,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should han
|
||||
<span
|
||||
class="toggle-label css-110"
|
||||
>
|
||||
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
|
||||
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
|
||||
|
||||
<div
|
||||
data-testid="info-tooltip"
|
||||
@@ -87,7 +87,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should han
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Read-write permissions.
|
||||
Read permissions.
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -126,7 +126,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should han
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly when sourceReadWriteAccessFromTarget is true 1`] = `
|
||||
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly when sourceReadAccessFromTarget is true 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack defaultManagedIdentityContainer css-109"
|
||||
@@ -134,7 +134,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
|
||||
<span
|
||||
class="toggle-label css-110"
|
||||
>
|
||||
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
|
||||
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
|
||||
|
||||
<div
|
||||
data-testid="info-tooltip"
|
||||
@@ -150,7 +150,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Read-write permissions.
|
||||
Read permissions.
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -189,7 +189,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly when toggle is on 1`] = `
|
||||
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly when toggle is on 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack defaultManagedIdentityContainer css-109"
|
||||
@@ -197,7 +197,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
|
||||
<span
|
||||
class="toggle-label css-110"
|
||||
>
|
||||
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
|
||||
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
|
||||
|
||||
<div
|
||||
data-testid="info-tooltip"
|
||||
@@ -213,7 +213,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Read-write permissions.
|
||||
Read permissions.
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -255,12 +255,12 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
|
||||
<div
|
||||
data-testid="popover-title"
|
||||
>
|
||||
Assign read-write permissions to default identity.
|
||||
Read permissions assigned to default identity.
|
||||
</div>
|
||||
<div
|
||||
data-testid="popover-content"
|
||||
>
|
||||
Assign read-write permissions on the source account to the default identity of the destination account. To confirm, click the "Yes" button.
|
||||
Assign read permissions of the source account to the default identity of the destination account. To confirm click the “Yes” button.
|
||||
</div>
|
||||
<button
|
||||
data-testid="popover-cancel"
|
||||
@@ -277,7 +277,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly with default state 1`] = `
|
||||
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly with default state 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack defaultManagedIdentityContainer css-109"
|
||||
@@ -285,7 +285,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
|
||||
<span
|
||||
class="toggle-label css-110"
|
||||
>
|
||||
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
|
||||
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
|
||||
|
||||
<div
|
||||
data-testid="info-tooltip"
|
||||
@@ -301,7 +301,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Read-write permissions.
|
||||
Read permissions.
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -340,7 +340,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly with different context states 1`] = `
|
||||
exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly with different context states 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack defaultManagedIdentityContainer css-109"
|
||||
@@ -348,7 +348,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
|
||||
<span
|
||||
class="toggle-label css-110"
|
||||
>
|
||||
To allow data copy from source to the destination container, provide read-write access on the source account to the default identity of the destination account.
|
||||
To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.
|
||||
|
||||
<div
|
||||
data-testid="info-tooltip"
|
||||
@@ -364,7 +364,7 @@ exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should rend
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Read-write permissions.
|
||||
Read permissions.
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ exports[`AssignPermissions Component Accordion Behavior should render accordion
|
||||
<span
|
||||
class="css-110"
|
||||
>
|
||||
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write access to the source account by completing the following steps.
|
||||
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.
|
||||
</span>
|
||||
<div
|
||||
class="ms-Stack css-111"
|
||||
@@ -212,7 +212,7 @@ exports[`AssignPermissions Component Edge Cases should calculate correct indent
|
||||
<span
|
||||
class="css-110"
|
||||
>
|
||||
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write access to the source account by completing the following steps.
|
||||
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.
|
||||
</span>
|
||||
<div
|
||||
class="ms-Stack css-111"
|
||||
@@ -618,7 +618,7 @@ exports[`AssignPermissions Component Edge Cases should handle missing account na
|
||||
<span
|
||||
class="css-110"
|
||||
>
|
||||
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write access to the source account by completing the following steps.
|
||||
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.
|
||||
</span>
|
||||
<div
|
||||
class="ms-Stack css-111"
|
||||
@@ -1153,7 +1153,7 @@ exports[`AssignPermissions Component Permission Groups should render permission
|
||||
<span
|
||||
class="css-110"
|
||||
>
|
||||
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write access to the source account by completing the following steps.
|
||||
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.
|
||||
</span>
|
||||
<div
|
||||
class="ms-Stack css-111"
|
||||
@@ -1307,7 +1307,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
|
||||
<span
|
||||
class="css-110"
|
||||
>
|
||||
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write access to the source account by completing the following steps.
|
||||
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.
|
||||
</span>
|
||||
<div
|
||||
data-testid="shimmer-tree"
|
||||
@@ -1329,7 +1329,7 @@ exports[`AssignPermissions Component Rendering should render without crashing wi
|
||||
<span
|
||||
class="css-110"
|
||||
>
|
||||
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read-write access to the source account by completing the following steps.
|
||||
To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.
|
||||
</span>
|
||||
<div
|
||||
data-testid="shimmer-tree"
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { CopyJobContextState } from "../../../../Types/CopyJobTypes";
|
||||
import * as CopyJobPrerequisitesCacheModule from "../../../Utils/useCopyJobPrerequisitesCache";
|
||||
import usePermissionSections, {
|
||||
checkTargetHasReadWriteRoleOnSource,
|
||||
checkTargetHasReaderRoleOnSource,
|
||||
PermissionGroupConfig,
|
||||
SECTION_IDS,
|
||||
} from "./usePermissionsSection";
|
||||
@@ -40,12 +40,12 @@ jest.mock("../AddManagedIdentity", () => {
|
||||
return MockAddManagedIdentity;
|
||||
});
|
||||
|
||||
jest.mock("../AddReadWritePermissionToDefaultIdentity", () => {
|
||||
const MockAddReadWritePermissionToDefaultIdentity = () => {
|
||||
return <div data-testid="add-read-write-permission">AddReadWritePermissionToDefaultIdentity</div>;
|
||||
jest.mock("../AddReadPermissionToDefaultIdentity", () => {
|
||||
const MockAddReadPermissionToDefaultIdentity = () => {
|
||||
return <div data-testid="add-read-permission">AddReadPermissionToDefaultIdentity</div>;
|
||||
};
|
||||
MockAddReadWritePermissionToDefaultIdentity.displayName = "MockAddReadWritePermissionToDefaultIdentity";
|
||||
return MockAddReadWritePermissionToDefaultIdentity;
|
||||
MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity";
|
||||
return MockAddReadPermissionToDefaultIdentity;
|
||||
});
|
||||
|
||||
jest.mock("../DefaultManagedIdentity", () => {
|
||||
@@ -133,7 +133,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscriptionId: "",
|
||||
subscription: undefined,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -152,7 +152,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscription: undefined,
|
||||
subscriptionId: "",
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -193,7 +193,7 @@ describe("usePermissionsSection", () => {
|
||||
expect(capturedResult[0].sections.map((s) => s.id)).toEqual([
|
||||
SECTION_IDS.addManagedIdentity,
|
||||
SECTION_IDS.defaultManagedIdentity,
|
||||
SECTION_IDS.readWritePermissionAssigned,
|
||||
SECTION_IDS.readPermissionAssigned,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -208,7 +208,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscriptionId: "",
|
||||
subscription: undefined,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -222,7 +222,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscription: undefined,
|
||||
subscriptionId: "",
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -299,7 +299,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscription: undefined,
|
||||
subscriptionId: "",
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -337,7 +337,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscription: undefined,
|
||||
subscriptionId: "",
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -358,17 +358,16 @@ describe("usePermissionsSection", () => {
|
||||
expect(defaultManagedIdentitySection?.completed).toBe(true);
|
||||
});
|
||||
|
||||
it("should validate readWritePermissionAssigned section with contributor role", async () => {
|
||||
it("should validate readPermissionAssigned section with reader role", async () => {
|
||||
const mockRoleDefinitions: RbacUtils.RoleDefinitionType[] = [
|
||||
{
|
||||
id: "role-1",
|
||||
name: "00000000-0000-0000-0000-000000000002",
|
||||
name: "Custom Role",
|
||||
permissions: [
|
||||
{
|
||||
dataActions: [
|
||||
"Microsoft.DocumentDB/databaseAccounts/readMetadata",
|
||||
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
|
||||
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/write",
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -399,7 +398,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscription: undefined,
|
||||
subscriptionId: "",
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -408,9 +407,7 @@ describe("usePermissionsSection", () => {
|
||||
render(<TestWrapper state={state} onResult={noop} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId(`section-${SECTION_IDS.readWritePermissionAssigned}-completed`)).toHaveTextContent(
|
||||
"true",
|
||||
);
|
||||
expect(screen.getByTestId(`section-${SECTION_IDS.readPermissionAssigned}-completed`)).toHaveTextContent("true");
|
||||
});
|
||||
|
||||
expect(mockedRbacUtils.fetchRoleAssignments).toHaveBeenCalledWith(
|
||||
@@ -438,7 +435,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscriptionId: "",
|
||||
subscription: undefined,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -479,7 +476,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscriptionId: "",
|
||||
subscription: undefined,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -549,7 +546,7 @@ describe("usePermissionsSection", () => {
|
||||
type: "",
|
||||
kind: "",
|
||||
},
|
||||
subscription: undefined,
|
||||
subscriptionId: "",
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -571,12 +568,12 @@ describe("usePermissionsSection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkTargetHasReadWriteRoleOnSource", () => {
|
||||
it("should return true for built-in Contributor role", () => {
|
||||
describe("checkTargetHasReaderRoleOnSource", () => {
|
||||
it("should return true for built-in Reader role", () => {
|
||||
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
|
||||
{
|
||||
id: "role-1",
|
||||
name: "00000000-0000-0000-0000-000000000002",
|
||||
name: "00000000-0000-0000-0000-000000000001",
|
||||
permissions: [],
|
||||
assignableScopes: [],
|
||||
resourceGroup: "",
|
||||
@@ -586,21 +583,20 @@ describe("checkTargetHasReadWriteRoleOnSource", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
|
||||
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for custom role with read-write data actions", () => {
|
||||
it("should return true for custom role with required data actions", () => {
|
||||
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
|
||||
{
|
||||
id: "role-1",
|
||||
name: "Custom Contributor Role",
|
||||
name: "Custom Reader Role",
|
||||
permissions: [
|
||||
{
|
||||
dataActions: [
|
||||
"Microsoft.DocumentDB/databaseAccounts/readMetadata",
|
||||
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read",
|
||||
"Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/write",
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -612,7 +608,7 @@ describe("checkTargetHasReadWriteRoleOnSource", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
|
||||
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
@@ -634,12 +630,12 @@ describe("checkTargetHasReadWriteRoleOnSource", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
|
||||
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for empty role definitions", () => {
|
||||
const result = checkTargetHasReadWriteRoleOnSource([]);
|
||||
const result = checkTargetHasReaderRoleOnSource([]);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
@@ -657,11 +653,11 @@ describe("checkTargetHasReadWriteRoleOnSource", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
|
||||
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle multiple roles and return true if any has sufficient read-write permissions", () => {
|
||||
it("should handle multiple roles and return true if any has sufficient permissions", () => {
|
||||
const roleDefinitions: RbacUtils.RoleDefinitionType[] = [
|
||||
{
|
||||
id: "role-1",
|
||||
@@ -679,7 +675,7 @@ describe("checkTargetHasReadWriteRoleOnSource", () => {
|
||||
},
|
||||
{
|
||||
id: "role-2",
|
||||
name: "00000000-0000-0000-0000-000000000002",
|
||||
name: "00000000-0000-0000-0000-000000000001",
|
||||
permissions: [],
|
||||
assignableScopes: [],
|
||||
resourceGroup: "",
|
||||
@@ -689,7 +685,7 @@ describe("checkTargetHasReadWriteRoleOnSource", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const result = checkTargetHasReadWriteRoleOnSource(roleDefinitions);
|
||||
const result = checkTargetHasReaderRoleOnSource(roleDefinitions);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { CopyJobContextState } from "../../../../Types/CopyJobTypes";
|
||||
import { useCopyJobPrerequisitesCache } from "../../../Utils/useCopyJobPrerequisitesCache";
|
||||
import AddManagedIdentity from "../AddManagedIdentity";
|
||||
import AddReadWritePermissionToDefaultIdentity from "../AddReadWritePermissionToDefaultIdentity";
|
||||
import AddReadPermissionToDefaultIdentity from "../AddReadPermissionToDefaultIdentity";
|
||||
import DefaultManagedIdentity from "../DefaultManagedIdentity";
|
||||
import OnlineCopyEnabled from "../OnlineCopyEnabled";
|
||||
import PointInTimeRestore from "../PointInTimeRestore";
|
||||
@@ -36,13 +36,11 @@ export interface PermissionGroupConfig {
|
||||
export const SECTION_IDS = {
|
||||
addManagedIdentity: "addManagedIdentity",
|
||||
defaultManagedIdentity: "defaultManagedIdentity",
|
||||
readWritePermissionAssigned: "readWritePermissionAssigned",
|
||||
readPermissionAssigned: "readPermissionAssigned",
|
||||
pointInTimeRestore: "pointInTimeRestore",
|
||||
onlineCopyEnabled: "onlineCopyEnabled",
|
||||
} as const;
|
||||
|
||||
const COSMOS_DB_BUILT_IN_DATA_CONTRIBUTOR_ROLE_ID = "00000000-0000-0000-0000-000000000002";
|
||||
|
||||
const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
|
||||
{
|
||||
id: SECTION_IDS.addManagedIdentity,
|
||||
@@ -68,9 +66,9 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: SECTION_IDS.readWritePermissionAssigned,
|
||||
title: ContainerCopyMessages.readWritePermissionAssigned.title,
|
||||
Component: AddReadWritePermissionToDefaultIdentity,
|
||||
id: SECTION_IDS.readPermissionAssigned,
|
||||
title: ContainerCopyMessages.readPermissionAssigned.title,
|
||||
Component: AddReadPermissionToDefaultIdentity,
|
||||
disabled: true,
|
||||
validate: async (state: CopyJobContextState) => {
|
||||
const principalId = state?.target?.account?.identity?.principalId;
|
||||
@@ -89,7 +87,7 @@ const PERMISSION_SECTIONS_CONFIG: PermissionSectionConfig[] = [
|
||||
);
|
||||
|
||||
const roleDefinitions = await fetchRoleDefinitions(rolesAssigned ?? []);
|
||||
return checkTargetHasReadWriteRoleOnSource(roleDefinitions ?? []);
|
||||
return checkTargetHasReaderRoleOnSource(roleDefinitions ?? []);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -121,34 +119,18 @@ const PERMISSION_SECTIONS_FOR_ONLINE_JOBS: PermissionSectionConfig[] = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Checks if the user has contributor-style read-write access on the source account.
|
||||
* Checks if the user has the Reader role based on role definitions.
|
||||
*/
|
||||
export function checkTargetHasReadWriteRoleOnSource(roleDefinitions: RoleDefinitionType[]): boolean {
|
||||
return roleDefinitions?.some((role) => {
|
||||
if (role.name === COSMOS_DB_BUILT_IN_DATA_CONTRIBUTOR_ROLE_ID) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const dataActions = role.permissions?.flatMap((permission) => permission.dataActions ?? []) ?? [];
|
||||
|
||||
const hasAccountWildcard = dataActions.includes("Microsoft.DocumentDB/databaseAccounts/*");
|
||||
const hasContainerWildcard =
|
||||
hasAccountWildcard || dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*");
|
||||
const hasItemsWildcard =
|
||||
hasContainerWildcard ||
|
||||
dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*");
|
||||
|
||||
const hasAccountReadMetadata =
|
||||
hasAccountWildcard || dataActions.includes("Microsoft.DocumentDB/databaseAccounts/readMetadata");
|
||||
const hasItemRead =
|
||||
hasItemsWildcard ||
|
||||
dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read");
|
||||
const hasItemWrite =
|
||||
hasItemsWildcard ||
|
||||
dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/write");
|
||||
|
||||
return hasAccountReadMetadata && hasItemRead && hasItemWrite;
|
||||
});
|
||||
export function checkTargetHasReaderRoleOnSource(roleDefinitions: RoleDefinitionType[]): boolean {
|
||||
return roleDefinitions?.some(
|
||||
(role) =>
|
||||
role.name === "00000000-0000-0000-0000-000000000001" ||
|
||||
role.permissions.some(
|
||||
(permission) =>
|
||||
permission.dataActions.includes("Microsoft.DocumentDB/databaseAccounts/readMetadata") &&
|
||||
permission.dataActions.includes("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -81,7 +81,7 @@ describe("AddCollectionPanelWrapper", () => {
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
sourceReadAccessFromTarget: false,
|
||||
},
|
||||
setCopyJobState: mockSetCopyJobState,
|
||||
flow: null,
|
||||
@@ -109,7 +109,7 @@ describe("AddCollectionPanelWrapper", () => {
|
||||
expect(container.querySelector(".addCollectionPanelWrapper")).toBeInTheDocument();
|
||||
expect(container.querySelector(".addCollectionPanelHeader")).toBeInTheDocument();
|
||||
expect(container.querySelector(".addCollectionPanelBody")).toBeInTheDocument();
|
||||
expect(screen.getByText(ContainerCopyMessages.createNewContainerSubHeading())).toBeInTheDocument();
|
||||
expect(screen.getByText(ContainerCopyMessages.createNewContainerSubHeading)).toBeInTheDocument();
|
||||
expect(screen.getByTestId("add-collection-panel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { IDropdownOption, MessageBar, MessageBarType, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
||||
import { readDatabasesForAccount } from "Common/dataAccess/readDatabases";
|
||||
import { AccountOverride } from "Contracts/DataModels";
|
||||
import { Stack, Text } from "@fluentui/react";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { produce } from "immer";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { AddCollectionPanel } from "../../../../Panes/AddCollectionPanel/AddCollectionPanel";
|
||||
import ContainerCopyMessages from "../../../ContainerCopyMessages";
|
||||
import { useCopyJobContext } from "../../../Context/CopyJobContext";
|
||||
import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils";
|
||||
|
||||
type AddCollectionPanelWrapperProps = {
|
||||
explorer?: Explorer;
|
||||
@@ -16,26 +13,7 @@ type AddCollectionPanelWrapperProps = {
|
||||
};
|
||||
|
||||
const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapperProps> = ({ explorer, goBack }) => {
|
||||
const { setCopyJobState, copyJobState } = useCopyJobContext();
|
||||
const [destinationDatabases, setDestinationDatabases] = useState<IDropdownOption[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [permissionError, setPermissionError] = useState<string | null>(null);
|
||||
|
||||
const targetAccountOverride: AccountOverride | undefined = useMemo(() => {
|
||||
const accountId = copyJobState?.target?.account?.id;
|
||||
if (!accountId) {
|
||||
return undefined;
|
||||
}
|
||||
const details = getAccountDetailsFromResourceId(accountId);
|
||||
if (!details?.subscriptionId || !details?.resourceGroup || !details?.accountName) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
subscriptionId: details.subscriptionId,
|
||||
resourceGroup: details.resourceGroup,
|
||||
accountName: details.accountName,
|
||||
};
|
||||
}, [copyJobState?.target?.account?.id]);
|
||||
const { setCopyJobState } = useCopyJobContext();
|
||||
|
||||
useEffect(() => {
|
||||
const sidePanelStore = useSidePanel.getState();
|
||||
@@ -47,52 +25,6 @@ const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapp
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!targetAccountOverride) {
|
||||
setIsLoading(false);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const fetchDatabases = async () => {
|
||||
setIsLoading(true);
|
||||
setPermissionError(null);
|
||||
try {
|
||||
const databases = await readDatabasesForAccount(
|
||||
targetAccountOverride.subscriptionId,
|
||||
targetAccountOverride.resourceGroup,
|
||||
targetAccountOverride.accountName,
|
||||
);
|
||||
if (!cancelled) {
|
||||
setDestinationDatabases(databases.map((db) => ({ key: db.id, text: db.id })));
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
const message = error?.message || String(error);
|
||||
if (message.includes("AuthorizationFailed") || message.includes("403")) {
|
||||
setPermissionError(
|
||||
`You do not have sufficient permissions to access the destination account "${targetAccountOverride.accountName}". ` +
|
||||
"Please ensure you have at least Contributor or Owner access to create databases and containers.",
|
||||
);
|
||||
} else {
|
||||
setPermissionError(
|
||||
`Failed to load databases from the destination account "${targetAccountOverride.accountName}": ${message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchDatabases();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [targetAccountOverride]);
|
||||
|
||||
const handleAddCollectionSuccess = useCallback(
|
||||
(collectionData: { databaseId: string; collectionId: string }) => {
|
||||
setCopyJobState(
|
||||
@@ -106,37 +38,13 @@ const AddCollectionPanelWrapper: React.FunctionComponent<AddCollectionPanelWrapp
|
||||
[goBack],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Stack horizontalAlign="center" verticalAlign="center" styles={{ root: { padding: 20 } }}>
|
||||
<Spinner size={SpinnerSize.large} label="Loading destination account databases..." />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (permissionError) {
|
||||
return (
|
||||
<Stack styles={{ root: { padding: 20 } }}>
|
||||
<MessageBar messageBarType={MessageBarType.error}>{permissionError}</MessageBar>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack className="addCollectionPanelWrapper">
|
||||
<Stack.Item className="addCollectionPanelHeader">
|
||||
<Text className="themeText">
|
||||
{ContainerCopyMessages.createNewContainerSubHeading(targetAccountOverride?.accountName)}
|
||||
</Text>
|
||||
<Text className="themeText">{ContainerCopyMessages.createNewContainerSubHeading}</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item className="addCollectionPanelBody">
|
||||
<AddCollectionPanel
|
||||
explorer={explorer}
|
||||
isCopyJobFlow={true}
|
||||
onSubmitSuccess={handleAddCollectionSuccess}
|
||||
targetAccountOverride={targetAccountOverride}
|
||||
externalDatabaseOptions={destinationDatabases}
|
||||
/>
|
||||
<AddCollectionPanel explorer={explorer} isCopyJobFlow={true} onSubmitSuccess={handleAddCollectionSuccess} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack addCollectionPanelWrapper css-115"
|
||||
class="ms-Stack addCollectionPanelWrapper css-109"
|
||||
>
|
||||
<div
|
||||
class="ms-StackItem addCollectionPanelHeader css-116"
|
||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||
>
|
||||
<span
|
||||
class="themeText css-117"
|
||||
class="themeText css-111"
|
||||
>
|
||||
Configure the properties for the new container.
|
||||
Select the properties for your container.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ms-StackItem addCollectionPanelBody css-116"
|
||||
class="ms-StackItem addCollectionPanelBody css-110"
|
||||
>
|
||||
<div
|
||||
data-testid="add-collection-panel"
|
||||
@@ -44,19 +44,19 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot 1`]
|
||||
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with both props 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack addCollectionPanelWrapper css-115"
|
||||
class="ms-Stack addCollectionPanelWrapper css-109"
|
||||
>
|
||||
<div
|
||||
class="ms-StackItem addCollectionPanelHeader css-116"
|
||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||
>
|
||||
<span
|
||||
class="themeText css-117"
|
||||
class="themeText css-111"
|
||||
>
|
||||
Configure the properties for the new container.
|
||||
Select the properties for your container.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ms-StackItem addCollectionPanelBody css-116"
|
||||
class="ms-StackItem addCollectionPanelBody css-110"
|
||||
>
|
||||
<div
|
||||
data-testid="add-collection-panel"
|
||||
@@ -85,19 +85,19 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
|
||||
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with explorer prop 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack addCollectionPanelWrapper css-115"
|
||||
class="ms-Stack addCollectionPanelWrapper css-109"
|
||||
>
|
||||
<div
|
||||
class="ms-StackItem addCollectionPanelHeader css-116"
|
||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||
>
|
||||
<span
|
||||
class="themeText css-117"
|
||||
class="themeText css-111"
|
||||
>
|
||||
Configure the properties for the new container.
|
||||
Select the properties for your container.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ms-StackItem addCollectionPanelBody css-116"
|
||||
class="ms-StackItem addCollectionPanelBody css-110"
|
||||
>
|
||||
<div
|
||||
data-testid="add-collection-panel"
|
||||
@@ -126,19 +126,19 @@ exports[`AddCollectionPanelWrapper Component Rendering should match snapshot wit
|
||||
exports[`AddCollectionPanelWrapper Component Rendering should match snapshot with goBack prop 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="ms-Stack addCollectionPanelWrapper css-115"
|
||||
class="ms-Stack addCollectionPanelWrapper css-109"
|
||||
>
|
||||
<div
|
||||
class="ms-StackItem addCollectionPanelHeader css-116"
|
||||
class="ms-StackItem addCollectionPanelHeader css-110"
|
||||
>
|
||||
<span
|
||||
class="themeText css-117"
|
||||
class="themeText css-111"
|
||||
>
|
||||
Configure the properties for the new container.
|
||||
Select the properties for your container.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ms-StackItem addCollectionPanelBody css-116"
|
||||
class="ms-StackItem addCollectionPanelBody css-110"
|
||||
>
|
||||
<div
|
||||
data-testid="add-collection-panel"
|
||||
|
||||
@@ -87,18 +87,18 @@ describe("PreviewCopyJob", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
subscription: mockSubscription,
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "source-database",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscription: mockSubscription,
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "target-database",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
sourceReadAccessFromTarget: false,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
@@ -146,7 +146,7 @@ describe("PreviewCopyJob", () => {
|
||||
it("should render with missing source subscription information", () => {
|
||||
const mockContext = createMockContext({
|
||||
source: {
|
||||
subscriptionId: "",
|
||||
subscription: undefined,
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "source-database",
|
||||
containerId: "source-container",
|
||||
@@ -165,7 +165,7 @@ describe("PreviewCopyJob", () => {
|
||||
it("should render with missing source account information", () => {
|
||||
const mockContext = createMockContext({
|
||||
source: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
subscription: mockSubscription,
|
||||
account: null,
|
||||
databaseId: "source-database",
|
||||
containerId: "source-container",
|
||||
@@ -184,13 +184,13 @@ describe("PreviewCopyJob", () => {
|
||||
it("should render with undefined database and container names", () => {
|
||||
const mockContext = createMockContext({
|
||||
source: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
subscription: mockSubscription,
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscription: mockSubscription,
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
@@ -219,7 +219,7 @@ describe("PreviewCopyJob", () => {
|
||||
|
||||
const mockContext = createMockContext({
|
||||
source: {
|
||||
subscriptionId: longNameSubscription.subscriptionId,
|
||||
subscription: longNameSubscription,
|
||||
account: longNameAccount,
|
||||
databaseId: "long-database-name-for-testing-purposes",
|
||||
containerId: "long-container-name-for-testing-purposes",
|
||||
@@ -253,13 +253,13 @@ describe("PreviewCopyJob", () => {
|
||||
it("should handle special characters in database and container names", () => {
|
||||
const mockContext = createMockContext({
|
||||
source: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
subscription: mockSubscription,
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "test-db_with@special#chars",
|
||||
containerId: "test-container_with@special#chars",
|
||||
},
|
||||
target: {
|
||||
subscription: mockSubscription,
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: mockDatabaseAccount,
|
||||
databaseId: "target-db_with@special#chars",
|
||||
containerId: "target-container_with@special#chars",
|
||||
@@ -285,12 +285,12 @@ describe("PreviewCopyJob", () => {
|
||||
|
||||
const mockContext = createMockContext({
|
||||
target: {
|
||||
subscription: mockSubscription,
|
||||
subscriptionId: "target-subscription-id",
|
||||
account: targetAccount,
|
||||
databaseId: "target-database",
|
||||
containerId: "target-container",
|
||||
},
|
||||
sourceReadWriteAccessFromTarget: true,
|
||||
sourceReadAccessFromTarget: true,
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
@@ -360,7 +360,7 @@ describe("PreviewCopyJob", () => {
|
||||
);
|
||||
|
||||
expect(getByText(/Job name/i)).toBeInTheDocument();
|
||||
expect(getByText(/Destination subscription/i)).toBeInTheDocument();
|
||||
expect(getByText(/Destination account/i)).toBeInTheDocument();
|
||||
expect(getByText(/Source subscription/i)).toBeInTheDocument();
|
||||
expect(getByText(/Source account/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,15 +36,15 @@ const PreviewCopyJob: React.FC = () => {
|
||||
<TextField data-test="job-name-textfield" value={jobName} onChange={onJobNameChange} />
|
||||
</FieldRow>
|
||||
<Stack>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.destinationSubscriptionLabel}</Text>
|
||||
<Text data-test="destination-subscription-name" className="themeText">
|
||||
{copyJobState.target?.subscription?.displayName}
|
||||
<Text className="bold themeText">{ContainerCopyMessages.sourceSubscriptionLabel}</Text>
|
||||
<Text data-test="source-subscription-name" className="themeText">
|
||||
{copyJobState.source?.subscription?.displayName}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.destinationAccountLabel}</Text>
|
||||
<Text data-test="destination-account-name" className="themeText">
|
||||
{copyJobState.target?.account?.name}
|
||||
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||
<Text data-test="source-account-name" className="themeText">
|
||||
{copyJobState.source?.account?.name}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack>
|
||||
|
||||
@@ -49,11 +49,11 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination subscription
|
||||
Source subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-subscription-name"
|
||||
data-test="source-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -64,11 +64,11 @@ exports[`PreviewCopyJob should handle special characters in database and contain
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination account
|
||||
Source account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-account-name"
|
||||
data-test="source-account-name"
|
||||
>
|
||||
test-account
|
||||
</span>
|
||||
@@ -371,11 +371,11 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination subscription
|
||||
Source subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-subscription-name"
|
||||
data-test="source-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -386,13 +386,13 @@ exports[`PreviewCopyJob should render component with cross-subscription setup 1`
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination account
|
||||
Source account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-account-name"
|
||||
data-test="source-account-name"
|
||||
>
|
||||
target-account
|
||||
test-account
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -693,11 +693,11 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination subscription
|
||||
Source subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-subscription-name"
|
||||
data-test="source-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -708,11 +708,11 @@ exports[`PreviewCopyJob should render with default state and empty job name 1`]
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination account
|
||||
Source account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-account-name"
|
||||
data-test="source-account-name"
|
||||
>
|
||||
test-account
|
||||
</span>
|
||||
@@ -1015,13 +1015,13 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination subscription
|
||||
Source subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-subscription-name"
|
||||
data-test="source-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
This is a very long subscription name that might cause display issues if not handled properly
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -1030,13 +1030,13 @@ exports[`PreviewCopyJob should render with long subscription and account names 1
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination account
|
||||
Source account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-account-name"
|
||||
data-test="source-account-name"
|
||||
>
|
||||
test-account
|
||||
this-is-a-very-long-database-account-name-that-might-cause-display-issues
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -1337,11 +1337,11 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination subscription
|
||||
Source subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-subscription-name"
|
||||
data-test="source-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -1352,13 +1352,7 @@ exports[`PreviewCopyJob should render with missing source account information 1`
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-account-name"
|
||||
>
|
||||
test-account
|
||||
Source account
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -1659,13 +1653,7 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
Source subscription
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -1674,11 +1662,11 @@ exports[`PreviewCopyJob should render with missing source subscription informati
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination account
|
||||
Source account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-account-name"
|
||||
data-test="source-account-name"
|
||||
>
|
||||
test-account
|
||||
</span>
|
||||
@@ -1981,11 +1969,11 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination subscription
|
||||
Source subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-subscription-name"
|
||||
data-test="source-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -1996,11 +1984,11 @@ exports[`PreviewCopyJob should render with online migration type 1`] = `
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination account
|
||||
Source account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-account-name"
|
||||
data-test="source-account-name"
|
||||
>
|
||||
test-account
|
||||
</span>
|
||||
@@ -2303,11 +2291,11 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination subscription
|
||||
Source subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-subscription-name"
|
||||
data-test="source-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -2318,11 +2306,11 @@ exports[`PreviewCopyJob should render with pre-filled job name 1`] = `
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination account
|
||||
Source account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-account-name"
|
||||
data-test="source-account-name"
|
||||
>
|
||||
test-account
|
||||
</span>
|
||||
@@ -2625,11 +2613,11 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination subscription
|
||||
Source subscription
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-subscription-name"
|
||||
data-test="source-subscription-name"
|
||||
>
|
||||
Test Subscription
|
||||
</span>
|
||||
@@ -2640,11 +2628,11 @@ exports[`PreviewCopyJob should render with undefined database and container name
|
||||
<span
|
||||
class="bold themeText css-125"
|
||||
>
|
||||
Destination account
|
||||
Source account
|
||||
</span>
|
||||
<span
|
||||
class="themeText css-125"
|
||||
data-test="destination-account-name"
|
||||
data-test="source-account-name"
|
||||
>
|
||||
test-account
|
||||
</span>
|
||||
|
||||
@@ -38,12 +38,6 @@ describe("AccountDropdown", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: "",
|
||||
account: null,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscription: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
displayName: "Test Subscription",
|
||||
@@ -52,7 +46,13 @@ describe("AccountDropdown", () => {
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
target: {
|
||||
subscriptionId: "",
|
||||
account: null,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
} as CopyJobContextState;
|
||||
|
||||
const mockCopyJobContextValue = {
|
||||
@@ -129,11 +129,11 @@ describe("AccountDropdown", () => {
|
||||
renderWithContext();
|
||||
|
||||
expect(
|
||||
screen.getByText(`${ContainerCopyMessages.destinationAccountDropdownLabel}:`, { exact: true }),
|
||||
screen.getByText(`${ContainerCopyMessages.sourceAccountDropdownLabel}:`, { exact: true }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("combobox")).toHaveAttribute(
|
||||
"aria-label",
|
||||
ContainerCopyMessages.destinationAccountDropdownLabel,
|
||||
ContainerCopyMessages.sourceAccountDropdownLabel,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -202,7 +202,7 @@ describe("AccountDropdown", () => {
|
||||
|
||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||
const newState = stateUpdateFunction(mockCopyJobState);
|
||||
expect(newState.target.account).toEqual({
|
||||
expect(newState.source.account).toEqual({
|
||||
...mockDatabaseAccount1,
|
||||
id: normalizeAccountId(mockDatabaseAccount1.id),
|
||||
});
|
||||
@@ -226,21 +226,20 @@ describe("AccountDropdown", () => {
|
||||
|
||||
const stateUpdateFunction = mockSetCopyJobState.mock.calls[0][0];
|
||||
const newState = stateUpdateFunction(mockCopyJobState);
|
||||
expect(newState.target.account).toEqual({
|
||||
expect(newState.source.account).toEqual({
|
||||
...mockDatabaseAccount2,
|
||||
id: normalizeAccountId(mockDatabaseAccount2.id),
|
||||
});
|
||||
});
|
||||
|
||||
it("should keep current account if it exists in the filtered list", async () => {
|
||||
const normalizedAccount1 = { ...mockDatabaseAccount1, id: normalizeAccountId(mockDatabaseAccount1.id) };
|
||||
const contextWithSelectedAccount = {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
...mockCopyJobState,
|
||||
target: {
|
||||
...mockCopyJobState.target,
|
||||
account: normalizedAccount1,
|
||||
source: {
|
||||
...mockCopyJobState.source,
|
||||
account: mockDatabaseAccount1,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -257,9 +256,12 @@ describe("AccountDropdown", () => {
|
||||
const newState = stateUpdateFunction(contextWithSelectedAccount.copyJobState);
|
||||
expect(newState).toEqual({
|
||||
...contextWithSelectedAccount.copyJobState,
|
||||
target: {
|
||||
...contextWithSelectedAccount.copyJobState.target,
|
||||
account: normalizedAccount1,
|
||||
source: {
|
||||
...contextWithSelectedAccount.copyJobState.source,
|
||||
account: {
|
||||
...mockDatabaseAccount1,
|
||||
id: normalizeAccountId(mockDatabaseAccount1.id),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -295,8 +297,8 @@ describe("AccountDropdown", () => {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
...mockCopyJobState,
|
||||
target: {
|
||||
...mockCopyJobState.target,
|
||||
source: {
|
||||
...mockCopyJobState.source,
|
||||
account: portalAccount,
|
||||
},
|
||||
},
|
||||
@@ -321,8 +323,8 @@ describe("AccountDropdown", () => {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
...mockCopyJobState,
|
||||
target: {
|
||||
...mockCopyJobState.target,
|
||||
source: {
|
||||
...mockCopyJobState.source,
|
||||
account: hostedAccount,
|
||||
},
|
||||
},
|
||||
@@ -359,8 +361,8 @@ describe("AccountDropdown", () => {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
...mockCopyJobState,
|
||||
target: {
|
||||
...mockCopyJobState.target,
|
||||
source: {
|
||||
...mockCopyJobState.source,
|
||||
subscription: null,
|
||||
},
|
||||
} as CopyJobContextState,
|
||||
@@ -374,13 +376,13 @@ describe("AccountDropdown", () => {
|
||||
});
|
||||
|
||||
it("should not update state if account is already selected and the same", async () => {
|
||||
const selectedAccount = { ...mockDatabaseAccount1, id: normalizeAccountId(mockDatabaseAccount1.id) };
|
||||
const selectedAccount = mockDatabaseAccount1;
|
||||
const contextWithSelectedAccount = {
|
||||
...mockCopyJobContextValue,
|
||||
copyJobState: {
|
||||
...mockCopyJobState,
|
||||
target: {
|
||||
...mockCopyJobState.target,
|
||||
source: {
|
||||
...mockCopyJobState.source,
|
||||
account: selectedAccount,
|
||||
},
|
||||
},
|
||||
@@ -407,7 +409,7 @@ describe("AccountDropdown", () => {
|
||||
renderWithContext();
|
||||
|
||||
const dropdown = screen.getByRole("combobox");
|
||||
expect(dropdown).toHaveAttribute("aria-label", ContainerCopyMessages.destinationAccountDropdownLabel);
|
||||
expect(dropdown).toHaveAttribute("aria-label", ContainerCopyMessages.sourceAccountDropdownLabel);
|
||||
});
|
||||
|
||||
it("should have required attribute", () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ export const normalizeAccountId = (id: string = "") => {
|
||||
export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
||||
const { copyJobState, setCopyJobState } = useCopyJobContext();
|
||||
|
||||
const selectedSubscriptionId = copyJobState?.target?.subscription?.subscriptionId;
|
||||
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
|
||||
const allAccounts: DatabaseAccount[] = useDatabaseAccounts(selectedSubscriptionId);
|
||||
const sqlApiOnlyAccounts = (allAccounts || [])
|
||||
.filter((account) => apiType(account) === "SQL")
|
||||
@@ -36,11 +36,11 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
||||
|
||||
const updateCopyJobState = (newAccount: DatabaseAccount) => {
|
||||
setCopyJobState((prevState) => {
|
||||
if (prevState.target?.account?.id !== newAccount.id) {
|
||||
if (prevState.source?.account?.id !== newAccount.id) {
|
||||
return {
|
||||
...prevState,
|
||||
target: {
|
||||
...prevState.target,
|
||||
source: {
|
||||
...prevState.source,
|
||||
account: newAccount,
|
||||
},
|
||||
};
|
||||
@@ -51,13 +51,13 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (sqlApiOnlyAccounts && sqlApiOnlyAccounts.length > 0 && selectedSubscriptionId) {
|
||||
const currentAccountId = copyJobState?.target?.account?.id;
|
||||
const currentAccountId = copyJobState?.source?.account?.id;
|
||||
const predefinedAccountId = normalizeAccountId(userContext.databaseAccount?.id);
|
||||
const selectedAccountId = currentAccountId || predefinedAccountId;
|
||||
|
||||
const matchedAccount: DatabaseAccount | null =
|
||||
const targetAccount: DatabaseAccount | null =
|
||||
sqlApiOnlyAccounts.find((account) => account.id === selectedAccountId) || null;
|
||||
updateCopyJobState(matchedAccount || sqlApiOnlyAccounts[0]);
|
||||
updateCopyJobState(targetAccount || sqlApiOnlyAccounts[0]);
|
||||
}
|
||||
}, [sqlApiOnlyAccounts?.length, selectedSubscriptionId]);
|
||||
|
||||
@@ -77,13 +77,13 @@ export const AccountDropdown: React.FC<AccountDropdownProps> = () => {
|
||||
};
|
||||
|
||||
const isAccountDropdownDisabled = !selectedSubscriptionId || accountOptions.length === 0;
|
||||
const selectedAccountId = normalizeAccountId(copyJobState?.target?.account?.id ?? "");
|
||||
const selectedAccountId = normalizeAccountId(copyJobState?.source?.account?.id ?? "");
|
||||
|
||||
return (
|
||||
<FieldRow label={ContainerCopyMessages.destinationAccountDropdownLabel}>
|
||||
<FieldRow label={ContainerCopyMessages.sourceAccountDropdownLabel}>
|
||||
<Dropdown
|
||||
placeholder={ContainerCopyMessages.destinationAccountDropdownPlaceholder}
|
||||
ariaLabel={ContainerCopyMessages.destinationAccountDropdownLabel}
|
||||
placeholder={ContainerCopyMessages.sourceAccountDropdownPlaceholder}
|
||||
ariaLabel={ContainerCopyMessages.sourceAccountDropdownLabel}
|
||||
options={accountOptions}
|
||||
disabled={isAccountDropdownDisabled}
|
||||
required
|
||||
|
||||
@@ -29,7 +29,7 @@ describe("MigrationType", () => {
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
sourceReadAccessFromTarget: false,
|
||||
},
|
||||
setCopyJobState: mockSetCopyJobState,
|
||||
flow: { currentScreen: "selectAccount" },
|
||||
|
||||
@@ -17,11 +17,11 @@ export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.m
|
||||
|
||||
const updateCopyJobState = (newSubscription: Subscription) => {
|
||||
setCopyJobState((prevState) => {
|
||||
if (prevState.target?.subscription?.subscriptionId !== newSubscription.subscriptionId) {
|
||||
if (prevState.source?.subscription?.subscriptionId !== newSubscription.subscriptionId) {
|
||||
return {
|
||||
...prevState,
|
||||
target: {
|
||||
...prevState.target,
|
||||
source: {
|
||||
...prevState.source,
|
||||
subscription: newSubscription,
|
||||
account: null,
|
||||
},
|
||||
@@ -33,7 +33,7 @@ export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.m
|
||||
|
||||
useEffect(() => {
|
||||
if (subscriptions && subscriptions.length > 0) {
|
||||
const currentSubscriptionId = copyJobState?.target?.subscription?.subscriptionId;
|
||||
const currentSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
|
||||
const predefinedSubscriptionId = userContext.subscriptionId;
|
||||
const selectedSubscriptionId = currentSubscriptionId || predefinedSubscriptionId;
|
||||
|
||||
@@ -61,7 +61,7 @@ export const SubscriptionDropdown: React.FC<SubscriptionDropdownProps> = React.m
|
||||
}
|
||||
};
|
||||
|
||||
const selectedSubscriptionId = copyJobState?.target?.subscription?.subscriptionId;
|
||||
const selectedSubscriptionId = copyJobState?.source?.subscription?.subscriptionId;
|
||||
|
||||
return (
|
||||
<FieldRow label={ContainerCopyMessages.subscriptionDropdownLabel}>
|
||||
|
||||
@@ -30,18 +30,18 @@ describe("SelectAccount", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Online,
|
||||
source: {
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscription: null as any,
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
target: {
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
sourceReadAccessFromTarget: false,
|
||||
},
|
||||
setCopyJobState: mockSetCopyJobState,
|
||||
flow: { currentScreen: "selectAccount" },
|
||||
@@ -68,7 +68,7 @@ describe("SelectAccount", () => {
|
||||
expect(container.firstChild).toHaveAttribute("data-test", "Panel:SelectAccountContainer");
|
||||
expect(container.firstChild).toHaveClass("selectAccountContainer");
|
||||
|
||||
expect(screen.getByText(/Please select a destination account to copy to/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Please select a source account from which to copy/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId("subscription-dropdown")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("account-dropdown")).toBeInTheDocument();
|
||||
|
||||
@@ -8,7 +8,7 @@ exports[`SelectAccount Component Rendering should render correctly with snapshot
|
||||
<span
|
||||
class="themeText css-110"
|
||||
>
|
||||
Please select a destination account to copy to.
|
||||
Please select a source account from which to copy.
|
||||
</span>
|
||||
<div
|
||||
data-testid="subscription-dropdown"
|
||||
|
||||
@@ -7,9 +7,19 @@ import { dropDownChangeHandler } from "./DropDownChangeHandler";
|
||||
const createMockInitialState = (): CopyJobContextState => ({
|
||||
jobName: "test-job",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
sourceReadAccessFromTarget: false,
|
||||
source: {
|
||||
subscriptionId: "source-sub-id",
|
||||
subscription: {
|
||||
subscriptionId: "source-sub-id",
|
||||
displayName: "Source Subscription",
|
||||
state: "Enabled",
|
||||
subscriptionPolicies: {
|
||||
locationPlacementId: "test",
|
||||
quotaId: "test",
|
||||
spendingLimit: "Off",
|
||||
},
|
||||
authorizationSource: "test",
|
||||
},
|
||||
account: {
|
||||
id: "source-account-id",
|
||||
name: "source-account",
|
||||
@@ -40,17 +50,7 @@ const createMockInitialState = (): CopyJobContextState => ({
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscription: {
|
||||
subscriptionId: "target-sub-id",
|
||||
displayName: "Target Subscription",
|
||||
state: "Enabled",
|
||||
subscriptionPolicies: {
|
||||
locationPlacementId: "test",
|
||||
quotaId: "test",
|
||||
spendingLimit: "Off",
|
||||
},
|
||||
authorizationSource: "test",
|
||||
},
|
||||
subscriptionId: "target-sub-id",
|
||||
account: {
|
||||
id: "target-account-id",
|
||||
name: "target-account",
|
||||
@@ -169,7 +169,7 @@ describe("dropDownChangeHandler", () => {
|
||||
|
||||
expect(capturedState.source.databaseId).toBe("new-source-db");
|
||||
expect(capturedState.source.containerId).toBeUndefined();
|
||||
expect(capturedState.source.subscriptionId).toEqual(initialState.source.subscriptionId);
|
||||
expect(capturedState.source.subscription).toEqual(initialState.source.subscription);
|
||||
expect(capturedState.source.account).toEqual(initialState.source.account);
|
||||
expect(capturedState.target).toEqual(initialState.target);
|
||||
});
|
||||
@@ -181,7 +181,7 @@ describe("dropDownChangeHandler", () => {
|
||||
|
||||
expect(capturedState.jobName).toBe(initialState.jobName);
|
||||
expect(capturedState.migrationType).toBe(initialState.migrationType);
|
||||
expect(capturedState.sourceReadWriteAccessFromTarget).toBe(initialState.sourceReadWriteAccessFromTarget);
|
||||
expect(capturedState.sourceReadAccessFromTarget).toBe(initialState.sourceReadAccessFromTarget);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -193,7 +193,7 @@ describe("dropDownChangeHandler", () => {
|
||||
|
||||
expect(capturedState.source.containerId).toBe("new-source-container");
|
||||
expect(capturedState.source.databaseId).toBe(initialState.source.databaseId);
|
||||
expect(capturedState.source.subscriptionId).toEqual(initialState.source.subscriptionId);
|
||||
expect(capturedState.source.subscription).toEqual(initialState.source.subscription);
|
||||
expect(capturedState.source.account).toEqual(initialState.source.account);
|
||||
expect(capturedState.target).toEqual(initialState.target);
|
||||
});
|
||||
@@ -215,7 +215,7 @@ describe("dropDownChangeHandler", () => {
|
||||
|
||||
expect(capturedState.target.databaseId).toBe("new-target-db");
|
||||
expect(capturedState.target.containerId).toBeUndefined();
|
||||
expect(capturedState.target.subscription).toEqual(initialState.target.subscription);
|
||||
expect(capturedState.target.subscriptionId).toBe(initialState.target.subscriptionId);
|
||||
expect(capturedState.target.account).toEqual(initialState.target.account);
|
||||
expect(capturedState.source).toEqual(initialState.source);
|
||||
});
|
||||
@@ -227,7 +227,7 @@ describe("dropDownChangeHandler", () => {
|
||||
|
||||
expect(capturedState.jobName).toBe(initialState.jobName);
|
||||
expect(capturedState.migrationType).toBe(initialState.migrationType);
|
||||
expect(capturedState.sourceReadWriteAccessFromTarget).toBe(initialState.sourceReadWriteAccessFromTarget);
|
||||
expect(capturedState.sourceReadAccessFromTarget).toBe(initialState.sourceReadAccessFromTarget);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -239,7 +239,7 @@ describe("dropDownChangeHandler", () => {
|
||||
|
||||
expect(capturedState.target.containerId).toBe("new-target-container");
|
||||
expect(capturedState.target.databaseId).toBe(initialState.target.databaseId);
|
||||
expect(capturedState.target.subscription).toEqual(initialState.target.subscription);
|
||||
expect(capturedState.target.subscriptionId).toBe(initialState.target.subscriptionId);
|
||||
expect(capturedState.target.account).toEqual(initialState.target.account);
|
||||
expect(capturedState.source).toEqual(initialState.source);
|
||||
});
|
||||
|
||||
@@ -73,7 +73,7 @@ describe("SelectSourceAndTargetContainers", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: "test-subscription-id",
|
||||
subscription: { subscriptionId: "test-subscription-id" },
|
||||
account: {
|
||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
|
||||
name: "test-account",
|
||||
@@ -82,7 +82,7 @@ describe("SelectSourceAndTargetContainers", () => {
|
||||
containerId: "container1",
|
||||
},
|
||||
target: {
|
||||
subscription: { subscriptionId: "test-subscription-id" },
|
||||
subscriptionId: "test-subscription-id",
|
||||
account: {
|
||||
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
|
||||
name: "test-account",
|
||||
@@ -90,7 +90,7 @@ describe("SelectSourceAndTargetContainers", () => {
|
||||
databaseId: "db2",
|
||||
containerId: "container2",
|
||||
},
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
sourceReadAccessFromTarget: false,
|
||||
};
|
||||
|
||||
const mockMemoizedData = {
|
||||
|
||||
@@ -69,15 +69,15 @@ describe("useSourceAndTargetData", () => {
|
||||
const mockCopyJobState: CopyJobContextState = {
|
||||
jobName: "test-job",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
sourceReadWriteAccessFromTarget: false,
|
||||
sourceReadAccessFromTarget: false,
|
||||
source: {
|
||||
subscriptionId: "source-subscription-id",
|
||||
subscription: mockSubscription,
|
||||
account: mockSourceAccount,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscription: mockSubscription,
|
||||
subscriptionId: "target-subscription-id",
|
||||
account: mockTargetAccount,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
|
||||
@@ -86,13 +86,13 @@ describe("useCopyJobNavigation", () => {
|
||||
jobName: "test-job",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: "source-sub-id",
|
||||
subscription: { subscriptionId: "source-sub-id" } as any,
|
||||
account: { id: "source-account-id", name: "Account-1" } as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscription: { subscriptionId: "target-sub-id" } as any,
|
||||
subscriptionId: "target-sub-id",
|
||||
account: { id: "target-account-id", name: "Account-2" } as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
|
||||
@@ -142,14 +142,14 @@ describe("useCreateCopyJobScreensList", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: "test-sub",
|
||||
subscription: { subscriptionId: "test-sub" } as any,
|
||||
account: { name: "test-account" } as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscription: { subscriptionId: "test-sub" } as any,
|
||||
account: { name: "test-account" } as any,
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -171,14 +171,14 @@ describe("useCreateCopyJobScreensList", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
subscription: null as any,
|
||||
account: { name: "test-account" } as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscription: null as any,
|
||||
account: { name: "test-account" } as any,
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
@@ -210,13 +210,13 @@ describe("useCreateCopyJobScreensList", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: "",
|
||||
subscription: null as any,
|
||||
account: null as any,
|
||||
databaseId: "source-db",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscription: null as any,
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
@@ -240,13 +240,13 @@ describe("useCreateCopyJobScreensList", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: "",
|
||||
subscription: null as any,
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "source-container",
|
||||
},
|
||||
target: {
|
||||
subscription: null as any,
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "target-db",
|
||||
containerId: "target-container",
|
||||
@@ -288,13 +288,13 @@ describe("useCreateCopyJobScreensList", () => {
|
||||
jobName: "valid-job-name_123",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: "",
|
||||
subscription: null as any,
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscription: null as any,
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
@@ -318,13 +318,13 @@ describe("useCreateCopyJobScreensList", () => {
|
||||
jobName: "invalid job name with spaces!",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: "",
|
||||
subscription: null as any,
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscription: null as any,
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
@@ -348,13 +348,13 @@ describe("useCreateCopyJobScreensList", () => {
|
||||
jobName: "",
|
||||
migrationType: CopyJobMigrationType.Offline,
|
||||
source: {
|
||||
subscriptionId: "",
|
||||
subscription: null as any,
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
},
|
||||
target: {
|
||||
subscription: null as any,
|
||||
subscriptionId: "",
|
||||
account: null as any,
|
||||
databaseId: "",
|
||||
containerId: "",
|
||||
|
||||
@@ -36,7 +36,7 @@ function useCreateCopyJobScreensList(goBack: () => void): Screen[] {
|
||||
component: <SelectAccount />,
|
||||
validations: [
|
||||
{
|
||||
validate: (state: CopyJobContextState) => !!state?.target?.subscription && !!state?.target?.account,
|
||||
validate: (state: CopyJobContextState) => !!state?.source?.subscription && !!state?.source?.account,
|
||||
message: "Please select a subscription and account to proceed",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
import "@testing-library/jest-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
@@ -6,20 +5,6 @@ import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../E
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
import CopyJobActionMenu from "./CopyJobActionMenu";
|
||||
|
||||
const mockShowOkCancelModalDialog = jest.fn();
|
||||
const mockCloseDialog = jest.fn();
|
||||
const mockOpenDialog = jest.fn();
|
||||
|
||||
jest.mock("../../../Controls/Dialog", () => ({
|
||||
useDialog: {
|
||||
getState: () => ({
|
||||
showOkCancelModalDialog: mockShowOkCancelModalDialog,
|
||||
closeDialog: mockCloseDialog,
|
||||
openDialog: mockOpenDialog,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../ContainerCopyMessages", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
@@ -33,11 +18,6 @@ jest.mock("../../ContainerCopyMessages", () => ({
|
||||
cancel: "Cancel",
|
||||
complete: "Complete",
|
||||
},
|
||||
dialog: {
|
||||
heading: "Confirm Action",
|
||||
confirmButtonText: "Confirm",
|
||||
cancelButtonText: "Cancel",
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -70,9 +50,6 @@ describe("CopyJobActionMenu", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockShowOkCancelModalDialog.mockClear();
|
||||
mockCloseDialog.mockClear();
|
||||
mockOpenDialog.mockClear();
|
||||
});
|
||||
|
||||
describe("Component Rendering", () => {
|
||||
@@ -289,29 +266,7 @@ describe("CopyJobActionMenu", () => {
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.pause, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should show confirmation dialog when cancel action is clicked", () => {
|
||||
const job = createMockJob({ Name: "Test Job", Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action",
|
||||
null,
|
||||
"Confirm",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
null,
|
||||
expect.any(Object), // dialogBody content
|
||||
);
|
||||
});
|
||||
|
||||
it("should call handleClick when dialog is confirmed for cancel action", () => {
|
||||
it("should call handleClick when cancel action is clicked", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
@@ -322,9 +277,6 @@ describe("CopyJobActionMenu", () => {
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
|
||||
onOkCallback();
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function));
|
||||
});
|
||||
|
||||
@@ -342,33 +294,7 @@ describe("CopyJobActionMenu", () => {
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.resume, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should show confirmation dialog when complete action is clicked", () => {
|
||||
const job = createMockJob({
|
||||
Name: "Test Online Job",
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const completeButton = screen.getByText("Complete");
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action",
|
||||
null,
|
||||
"Confirm",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
null,
|
||||
expect.any(Object), // dialogBody content
|
||||
);
|
||||
});
|
||||
|
||||
it("should call handleClick when dialog is confirmed for complete action", () => {
|
||||
it("should call handleClick when complete action is clicked", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
@@ -382,87 +308,10 @@ describe("CopyJobActionMenu", () => {
|
||||
const completeButton = screen.getByText("Complete");
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
|
||||
onOkCallback();
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.complete, expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Dialog Body Content", () => {
|
||||
it("should pass correct dialog body content for cancel action", () => {
|
||||
const job = createMockJob({ Name: "MyTestJob", Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action",
|
||||
null,
|
||||
"Confirm",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
null,
|
||||
expect.objectContaining({
|
||||
props: expect.objectContaining({
|
||||
tokens: expect.any(Object),
|
||||
children: expect.any(Array),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should pass correct dialog body content for complete action", () => {
|
||||
const job = createMockJob({
|
||||
Name: "OnlineTestJob",
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const completeButton = screen.getByText("Complete");
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action",
|
||||
null,
|
||||
"Confirm",
|
||||
expect.any(Function),
|
||||
"Cancel",
|
||||
null,
|
||||
expect.objectContaining({
|
||||
props: expect.objectContaining({
|
||||
tokens: expect.any(Object),
|
||||
children: expect.any(Array),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not show dialog body for actions without confirmation", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Disabled States During Updates", () => {
|
||||
const TestComponentWrapper: React.FC<{
|
||||
job: CopyJobType;
|
||||
@@ -490,13 +339,8 @@ describe("CopyJobActionMenu", () => {
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const pauseButtonAfterClick = screen.getByText("Pause").closest("button");
|
||||
const pauseButtonAfterClick = screen.getByText("Pause");
|
||||
expect(pauseButtonAfterClick).toBeInTheDocument();
|
||||
expect(pauseButtonAfterClick).toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
const cancelButtonAfterClick = screen.getByText("Cancel").closest("button");
|
||||
expect(cancelButtonAfterClick).toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
it("should not disable actions for different jobs when one is updating", () => {
|
||||
@@ -516,7 +360,23 @@ describe("CopyJobActionMenu", () => {
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should disable complete action when job is being updated", () => {
|
||||
it("should properly handle multiple action types being disabled for the same job", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
render(<TestComponentWrapper job={job} />);
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
fireEvent.click(screen.getByText("Pause"));
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
fireEvent.click(screen.getByText("Cancel"));
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
expect(screen.getByText("Pause")).toBeInTheDocument();
|
||||
expect(screen.getByText("Cancel")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle complete action disabled state for online jobs", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
@@ -530,34 +390,8 @@ describe("CopyJobActionMenu", () => {
|
||||
const completeButton = screen.getByText("Complete");
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
// Simulate dialog confirmation to trigger state update
|
||||
const [, , , onOkCallback] = mockShowOkCancelModalDialog.mock.calls[0];
|
||||
onOkCallback();
|
||||
|
||||
fireEvent.click(actionButton);
|
||||
const completeButtonAfterClick = screen.getByText("Complete").closest("button");
|
||||
expect(completeButtonAfterClick).toBeInTheDocument();
|
||||
expect(completeButtonAfterClick).toHaveAttribute("aria-disabled", "true");
|
||||
});
|
||||
|
||||
it("should disable complete action when any other action is being performed", () => {
|
||||
const job = createMockJob({
|
||||
Status: CopyJobStatusType.InProgress,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<TestComponentWrapper job={job} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const completeButtonAfterClick = screen.getByText("Complete").closest("button");
|
||||
expect(completeButtonAfterClick).toBeInTheDocument();
|
||||
expect(completeButtonAfterClick).toHaveAttribute("aria-disabled", "true");
|
||||
expect(screen.getByText("Complete")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -628,7 +462,6 @@ describe("CopyJobActionMenu", () => {
|
||||
|
||||
expect(actionButton).toHaveAttribute("aria-label", "Actions");
|
||||
expect(actionButton).toHaveAttribute("title", "Actions");
|
||||
expect(actionButton).toHaveAttribute("role", "button");
|
||||
|
||||
const moreIcon = actionButton.querySelector('[data-icon-name="More"]');
|
||||
expect(moreIcon || actionButton).toBeInTheDocument();
|
||||
@@ -775,129 +608,4 @@ describe("CopyJobActionMenu", () => {
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Complete Coverage Tests", () => {
|
||||
it("should handle all possible dialog scenarios", () => {
|
||||
const dialogTests = [
|
||||
{ action: CopyJobActions.cancel, status: CopyJobStatusType.InProgress, shouldShowDialog: true },
|
||||
{
|
||||
action: CopyJobActions.complete,
|
||||
status: CopyJobStatusType.InProgress,
|
||||
mode: CopyJobMigrationType.Online,
|
||||
shouldShowDialog: true,
|
||||
},
|
||||
{ action: CopyJobActions.pause, status: CopyJobStatusType.InProgress, shouldShowDialog: false },
|
||||
{ action: CopyJobActions.resume, status: CopyJobStatusType.Paused, shouldShowDialog: false },
|
||||
];
|
||||
|
||||
dialogTests.forEach(({ action, status, mode = CopyJobMigrationType.Offline, shouldShowDialog }, index) => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const job = createMockJob({ Status: status, Mode: mode, Name: `DialogTestJob${index}` });
|
||||
const { unmount } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const actionText = action.charAt(0).toUpperCase() + action.slice(1);
|
||||
if (screen.queryByText(actionText)) {
|
||||
fireEvent.click(screen.getByText(actionText));
|
||||
|
||||
if (shouldShowDialog) {
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalled();
|
||||
} else {
|
||||
expect(mockShowOkCancelModalDialog).not.toHaveBeenCalled();
|
||||
expect(mockHandleClick).toHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("should verify component handles state updates correctly", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
const stateUpdater = jest.fn();
|
||||
|
||||
const testHandleClick: HandleJobActionClickType = (job, action, setUpdatingJobAction) => {
|
||||
setUpdatingJobAction({ jobName: job.Name, action });
|
||||
stateUpdater(job.Name, action);
|
||||
};
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={testHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const pauseButton = screen.getByText("Pause");
|
||||
fireEvent.click(pauseButton);
|
||||
|
||||
expect(stateUpdater).toHaveBeenCalledWith(job.Name, CopyJobActions.pause);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Full Integration Coverage", () => {
|
||||
it("should test complete workflow for cancel action with dialog", () => {
|
||||
const job = createMockJob({ Name: "Integration Test Job", Status: CopyJobStatusType.InProgress });
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
expect(actionButton).toHaveAttribute("data-test", "CopyJobActionMenu/Button:Integration Test Job");
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalledWith(
|
||||
"Confirm Action", // title
|
||||
null, // subText
|
||||
"Confirm", // confirmLabel
|
||||
expect.any(Function), // onOk
|
||||
"Cancel", // cancelLabel
|
||||
null, // onCancel
|
||||
expect.any(Object), // contentHtml (dialogBody)
|
||||
);
|
||||
|
||||
const onOkCallback = mockShowOkCancelModalDialog.mock.calls[0][3];
|
||||
onOkCallback();
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.cancel, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should test complete workflow for complete action with dialog", () => {
|
||||
const job = createMockJob({
|
||||
Name: "Online Integration Job",
|
||||
Status: CopyJobStatusType.Running,
|
||||
Mode: CopyJobMigrationType.Online,
|
||||
});
|
||||
|
||||
render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
const actionButton = screen.getByRole("button", { name: "Actions" });
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const completeButton = screen.getByText("Complete");
|
||||
fireEvent.click(completeButton);
|
||||
|
||||
expect(mockShowOkCancelModalDialog).toHaveBeenCalled();
|
||||
|
||||
const dialogContent = mockShowOkCancelModalDialog.mock.calls[0][6];
|
||||
expect(dialogContent).toBeTruthy();
|
||||
|
||||
const onOkCallback = mockShowOkCancelModalDialog.mock.calls[0][3];
|
||||
onOkCallback();
|
||||
|
||||
expect(mockHandleClick).toHaveBeenCalledWith(job, CopyJobActions.complete, expect.any(Function));
|
||||
});
|
||||
|
||||
it("should maintain proper component lifecycle", () => {
|
||||
const job = createMockJob({ Status: CopyJobStatusType.InProgress });
|
||||
const { rerender, unmount } = render(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
|
||||
rerender(<CopyJobActionMenu job={job} handleClick={mockHandleClick} />);
|
||||
expect(screen.getByRole("button", { name: "Actions" })).toBeInTheDocument();
|
||||
|
||||
expect(() => unmount()).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DirectionalHint, IconButton, IContextualMenuProps, Stack } from "@fluentui/react";
|
||||
import { IconButton, IContextualMenuProps } from "@fluentui/react";
|
||||
import React from "react";
|
||||
import { useDialog } from "../../../Controls/Dialog";
|
||||
import ContainerCopyMessages from "../../ContainerCopyMessages";
|
||||
import { CopyJobActions, CopyJobMigrationType, CopyJobStatusType } from "../../Enums/CopyJobEnums";
|
||||
import { CopyJobType, HandleJobActionClickType } from "../../Types/CopyJobTypes";
|
||||
@@ -10,28 +9,6 @@ interface CopyJobActionMenuProps {
|
||||
handleClick: HandleJobActionClickType;
|
||||
}
|
||||
|
||||
const dialogBody = {
|
||||
[CopyJobActions.cancel]: (jobName: string) => (
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack.Item>
|
||||
You are about to cancel <b>{jobName}</b> copy job.
|
||||
</Stack.Item>
|
||||
<Stack.Item>Cancelling will stop the job immediately.</Stack.Item>
|
||||
</Stack>
|
||||
),
|
||||
[CopyJobActions.complete]: (jobName: string) => (
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack.Item>
|
||||
You are about to complete <b>{jobName}</b> copy job.
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
Once completed, continuous data copy will stop after any pending documents are processed. To maintain data
|
||||
integrity, we recommend stopping updates to the source container before completing the job.
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
),
|
||||
};
|
||||
|
||||
const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick }) => {
|
||||
const [updatingJobAction, setUpdatingJobAction] = React.useState<{ jobName: string; action: string } | null>(null);
|
||||
if (
|
||||
@@ -45,22 +22,9 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
return null;
|
||||
}
|
||||
|
||||
const showActionConfirmationDialog = (job: CopyJobType, action: CopyJobActions): void => {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkCancelModalDialog(
|
||||
ContainerCopyMessages.MonitorJobs.dialog.heading,
|
||||
null,
|
||||
ContainerCopyMessages.MonitorJobs.dialog.confirmButtonText,
|
||||
() => handleClick(job, action, setUpdatingJobAction),
|
||||
ContainerCopyMessages.MonitorJobs.dialog.cancelButtonText,
|
||||
null,
|
||||
action in dialogBody ? dialogBody[action as keyof typeof dialogBody](job.Name) : null,
|
||||
);
|
||||
};
|
||||
|
||||
const getMenuItems = (): IContextualMenuProps["items"] => {
|
||||
const isThisJobUpdating = updatingJobAction?.jobName === job.Name;
|
||||
const updatingAction = updatingJobAction?.action;
|
||||
|
||||
const baseItems = [
|
||||
{
|
||||
@@ -68,21 +32,21 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.pause,
|
||||
iconProps: { iconName: "Pause" },
|
||||
onClick: () => handleClick(job, CopyJobActions.pause, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating,
|
||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.pause,
|
||||
},
|
||||
{
|
||||
key: CopyJobActions.cancel,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.cancel,
|
||||
iconProps: { iconName: "Cancel" },
|
||||
onClick: () => showActionConfirmationDialog(job, CopyJobActions.cancel),
|
||||
disabled: isThisJobUpdating,
|
||||
onClick: () => handleClick(job, CopyJobActions.cancel, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.cancel,
|
||||
},
|
||||
{
|
||||
key: CopyJobActions.resume,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.resume,
|
||||
iconProps: { iconName: "Play" },
|
||||
onClick: () => handleClick(job, CopyJobActions.resume, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating,
|
||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.resume,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -103,8 +67,8 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
key: CopyJobActions.complete,
|
||||
text: ContainerCopyMessages.MonitorJobs.Actions.complete,
|
||||
iconProps: { iconName: "CheckMark" },
|
||||
onClick: () => showActionConfirmationDialog(job, CopyJobActions.complete),
|
||||
disabled: isThisJobUpdating,
|
||||
onClick: () => handleClick(job, CopyJobActions.complete, setUpdatingJobAction),
|
||||
disabled: isThisJobUpdating && updatingAction === CopyJobActions.complete,
|
||||
});
|
||||
}
|
||||
return filteredItems;
|
||||
@@ -122,8 +86,8 @@ const CopyJobActionMenu: React.FC<CopyJobActionMenuProps> = ({ job, handleClick
|
||||
data-test={`CopyJobActionMenu/Button:${job.Name}`}
|
||||
role="button"
|
||||
iconProps={{ iconName: "More", styles: { root: { fontSize: "20px", fontWeight: "bold" } } }}
|
||||
menuProps={{ items: getMenuItems(), directionalHint: DirectionalHint.leftTopEdge, directionalHintFixed: false }}
|
||||
menuIconProps={{ iconName: "", className: "hidden" }}
|
||||
menuProps={{ items: getMenuItems() }}
|
||||
menuIconProps={{ iconName: "" }}
|
||||
ariaLabel={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
||||
title={ContainerCopyMessages.MonitorJobs.Columns.actions}
|
||||
/>
|
||||
|
||||
@@ -19,7 +19,7 @@ jest.mock("../../ContainerCopyMessages", () => ({
|
||||
sourceContainerLabel: "Source Container",
|
||||
targetDatabaseLabel: "Destination Database",
|
||||
targetContainerLabel: "Destination Container",
|
||||
destinationAccountLabel: "Destination account",
|
||||
sourceAccountLabel: "Source Account",
|
||||
MonitorJobs: {
|
||||
Columns: {
|
||||
lastUpdatedTime: "Date & time",
|
||||
@@ -102,8 +102,8 @@ describe("CopyJobDetails", () => {
|
||||
expect(screen.getByText("Date & time")).toBeInTheDocument();
|
||||
expect(screen.getByText("2024-01-01T10:00:00Z")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Destination account")).toBeInTheDocument();
|
||||
expect(screen.getByText("targetAccount")).toBeInTheDocument();
|
||||
expect(screen.getByText("Source Account")).toBeInTheDocument();
|
||||
expect(screen.getByText("sourceAccount")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Mode")).toBeInTheDocument();
|
||||
expect(screen.getByText("Offline")).toBeInTheDocument();
|
||||
@@ -263,7 +263,7 @@ describe("CopyJobDetails", () => {
|
||||
expect(screen.getByText("complex_source_container_with_underscores")).toBeInTheDocument();
|
||||
expect(screen.getByText("complex-target-db-with-hyphens")).toBeInTheDocument();
|
||||
expect(screen.getByText("complex_target_container_with_underscores")).toBeInTheDocument();
|
||||
expect(screen.getByText("complex.target.account")).toBeInTheDocument();
|
||||
expect(screen.getByText("complex.source.account")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -322,11 +322,11 @@ describe("CopyJobDetails", () => {
|
||||
render(<CopyJobDetails job={mockBasicJob} />);
|
||||
|
||||
const dateTimeHeading = screen.getByText("Date & time");
|
||||
const destinationAccountHeading = screen.getByText("Destination account");
|
||||
const sourceAccountHeading = screen.getByText("Source Account");
|
||||
const modeHeading = screen.getByText("Mode");
|
||||
|
||||
expect(dateTimeHeading).toHaveClass("bold");
|
||||
expect(destinationAccountHeading).toHaveClass("bold");
|
||||
expect(sourceAccountHeading).toHaveClass("bold");
|
||||
expect(modeHeading).toHaveClass("bold");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,8 +106,8 @@ const CopyJobDetails: React.FC<CopyJobDetailsProps> = ({ job }) => {
|
||||
<Text className="themeText">{job.LastUpdatedTime}</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={sectionCss.verticalAlign}>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.destinationAccountLabel}</Text>
|
||||
<Text className="themeText">{job.Destination?.remoteAccountName}</Text>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.sourceAccountLabel}</Text>
|
||||
<Text className="themeText">{job.Source?.remoteAccountName}</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={sectionCss.verticalAlign}>
|
||||
<Text className="bold themeText">{ContainerCopyMessages.MonitorJobs.Columns.mode}</Text>
|
||||
|
||||
@@ -11,17 +11,9 @@ jest.mock("../../Actions/CopyJobActions", () => ({
|
||||
|
||||
jest.mock("./CopyJobColumns", () => ({
|
||||
getColumns: jest.fn(() => [
|
||||
{
|
||||
key: "LastUpdatedTime",
|
||||
name: "Date & time",
|
||||
fieldName: "LastUpdatedTime",
|
||||
minWidth: 140,
|
||||
maxWidth: 300,
|
||||
isResizable: true,
|
||||
},
|
||||
{
|
||||
key: "Name",
|
||||
name: "Job name",
|
||||
name: "Name",
|
||||
fieldName: "Name",
|
||||
minWidth: 140,
|
||||
maxWidth: 300,
|
||||
@@ -173,165 +165,6 @@ describe("CopyJobsList", () => {
|
||||
expect(screen.getByTestId("action-menu-job-2")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("action-menu-job-3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders filter TextField with data-test attribute", () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const filterTextField = document.querySelector('[data-test="CopyJobsList/FilterTextField"]');
|
||||
expect(filterTextField).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders search TextField with correct placeholder", () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("Search jobs...");
|
||||
expect(searchInput).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Filtering", () => {
|
||||
it("filters jobs by Name when text is entered", async () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
||||
fireEvent.change(filterInput, { target: { value: "Job 1" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Test Job 3")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("filters jobs case-insensitively", async () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
||||
fireEvent.change(filterInput, { target: { value: "test job 1" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows all jobs when filter text is empty", async () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
||||
fireEvent.change(filterInput, { target: { value: "Job 1" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.change(filterInput, { target: { value: "" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 3")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("filters jobs by Status across all columns", async () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
||||
fireEvent.change(filterInput, { target: { value: CopyJobStatusType.Running } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Test Job 3")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("filters jobs by Mode across all columns", async () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
||||
fireEvent.change(filterInput, { target: { value: "Offline" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Test Job 1")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 2")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Test Job 3")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows no results when filter matches no jobs", async () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
||||
fireEvent.change(filterInput, { target: { value: "NonExistentJob" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Test Job 1")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Test Job 2")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Test Job 3")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("filters by partial text match", async () => {
|
||||
render(<CopyJobsList jobs={mockJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
||||
fireEvent.change(filterInput, { target: { value: "Test" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Job 3")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("resets pagination when filter changes", async () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 25 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
||||
|
||||
// Navigate to page 2
|
||||
fireEvent.click(screen.getByLabelText("Go to next page"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Showing 11 - 20 of 25 items")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Apply filter - should reset to page 1
|
||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
||||
fireEvent.change(filterInput, { target: { value: "Job 1" } });
|
||||
|
||||
await waitFor(() => {
|
||||
// Filtered results show from the beginning
|
||||
expect(screen.getByText("Test Job 1")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("updates filtered count in pager", async () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 25 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: i < 5 ? `Alpha Job ${i + 1}` : `Beta Job ${i + 1}`,
|
||||
}));
|
||||
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} pageSize={10} />);
|
||||
|
||||
expect(screen.getByText("Showing 1 - 10 of 25 items")).toBeInTheDocument();
|
||||
|
||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
||||
fireEvent.change(filterInput, { target: { value: "Alpha" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Showing 1 - 10 of 25 items")).not.toBeInTheDocument();
|
||||
// Pager should not be visible since filtered results (5) are less than page size (10)
|
||||
expect(screen.queryByLabelText("Go to next page")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Pagination", () => {
|
||||
@@ -509,7 +342,7 @@ describe("CopyJobsList", () => {
|
||||
|
||||
describe("Component Props", () => {
|
||||
it("uses default page size when not provided", () => {
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 20 }, (_, i) => ({
|
||||
const manyJobs: CopyJobType[] = Array.from({ length: 12 }, (_, i) => ({
|
||||
...mockJobs[0],
|
||||
ID: `job-${i + 1}`,
|
||||
Name: `Test Job ${i + 1}`,
|
||||
@@ -518,7 +351,7 @@ describe("CopyJobsList", () => {
|
||||
render(<CopyJobsList jobs={manyJobs} handleActionClick={mockHandleActionClick} />);
|
||||
|
||||
expect(screen.getByLabelText("Go to next page")).toBeInTheDocument();
|
||||
expect(screen.getByText("Showing 1 - 15 of 20 items")).toBeInTheDocument();
|
||||
expect(screen.getByText("Showing 1 - 10 of 12 items")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("passes correct props to getColumns function", async () => {
|
||||
@@ -607,33 +440,7 @@ describe("CopyJobsList", () => {
|
||||
render(<CopyJobsList jobs={largeJobsList} handleActionClick={mockHandleActionClick} />);
|
||||
}).not.toThrow();
|
||||
|
||||
expect(screen.getByText("Showing 1 - 15 of 1000 items")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles filtering with null or undefined values gracefully", async () => {
|
||||
const jobsWithNullValues: CopyJobType[] = [
|
||||
{
|
||||
...mockJobs[0],
|
||||
ID: "job-with-values",
|
||||
Name: "Valid Job",
|
||||
},
|
||||
{
|
||||
...mockJobs[1],
|
||||
ID: "job-null-name",
|
||||
Name: undefined as unknown as string,
|
||||
},
|
||||
];
|
||||
|
||||
expect(() => {
|
||||
render(<CopyJobsList jobs={jobsWithNullValues} handleActionClick={mockHandleActionClick} />);
|
||||
}).not.toThrow();
|
||||
|
||||
const filterInput = screen.getByPlaceholderText("Search jobs...");
|
||||
fireEvent.change(filterInput, { target: { value: "Valid" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Valid Job")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("Showing 1 - 10 of 1000 items")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,9 +12,8 @@ import {
|
||||
Stack,
|
||||
Sticky,
|
||||
StickyPositionType,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import Pager from "../../../../Common/Pager";
|
||||
import { useThemeStore } from "../../../../hooks/useTheme";
|
||||
import { getThemeTokens } from "../../../Theme/ThemeUtil";
|
||||
@@ -31,15 +30,9 @@ interface CopyJobsListProps {
|
||||
const styles = {
|
||||
container: { height: "100%" } as React.CSSProperties,
|
||||
stackItem: { position: "relative", marginBottom: "20px" } as React.CSSProperties,
|
||||
filterContainer: {
|
||||
margin: "15px 5px",
|
||||
},
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 15;
|
||||
|
||||
// Columns to search across
|
||||
const searchableFields = ["Name", "Status", "LastUpdatedTime", "Mode"];
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pageSize = PAGE_SIZE }) => {
|
||||
const isDarkMode = useThemeStore((state) => state.isDarkMode);
|
||||
@@ -48,23 +41,6 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
|
||||
const [sortedJobs, setSortedJobs] = React.useState<CopyJobType[]>(jobs);
|
||||
const [sortedColumnKey, setSortedColumnKey] = React.useState<string | undefined>(undefined);
|
||||
const [isSortedDescending, setIsSortedDescending] = React.useState<boolean>(false);
|
||||
const [filterText, setFilterText] = React.useState<string>("");
|
||||
|
||||
const filteredJobs = useMemo(() => {
|
||||
if (!filterText) {
|
||||
return sortedJobs;
|
||||
}
|
||||
const lowerFilterText = filterText.toLowerCase();
|
||||
return sortedJobs.filter((job: any) => {
|
||||
return searchableFields.some((field) => {
|
||||
const value = job[field];
|
||||
if (value === undefined || value === null) {
|
||||
return false;
|
||||
}
|
||||
return String(value).toLowerCase().includes(lowerFilterText);
|
||||
});
|
||||
});
|
||||
}, [sortedJobs, filterText]);
|
||||
|
||||
useEffect(() => {
|
||||
setSortedJobs(jobs);
|
||||
@@ -88,15 +64,7 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
|
||||
setStartIndex(0);
|
||||
};
|
||||
|
||||
const sortableColumns: IColumn[] = getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending);
|
||||
|
||||
const handleFilterTextChange = (
|
||||
_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string,
|
||||
) => {
|
||||
setFilterText(newValue || "");
|
||||
setStartIndex(0);
|
||||
};
|
||||
const columns: IColumn[] = getColumns(handleSort, handleActionClick, sortedColumnKey, isSortedDescending);
|
||||
|
||||
const _handleRowClick = (job: CopyJobType) => {
|
||||
openCopyJobDetailsPanel(job);
|
||||
@@ -113,25 +81,14 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<Stack verticalFill={true}>
|
||||
<Stack.Item>
|
||||
<div style={styles.filterContainer}>
|
||||
<TextField
|
||||
data-test="CopyJobsList/FilterTextField"
|
||||
placeholder="Search jobs..."
|
||||
ariaLabel="Search jobs"
|
||||
value={filterText}
|
||||
onChange={handleFilterTextChange}
|
||||
/>
|
||||
</div>
|
||||
</Stack.Item>
|
||||
<Stack.Item verticalFill={true} grow={1} shrink={1} style={styles.stackItem}>
|
||||
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
|
||||
<ShimmeredDetailsList
|
||||
className="CopyJobListContainer"
|
||||
onRenderRow={_onRenderRow}
|
||||
checkboxVisibility={2}
|
||||
columns={sortableColumns}
|
||||
items={filteredJobs.slice(startIndex, startIndex + pageSize)}
|
||||
columns={columns}
|
||||
items={sortedJobs.slice(startIndex, startIndex + pageSize)}
|
||||
enableShimmer={false}
|
||||
constrainMode={ConstrainMode.unconstrained}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
@@ -160,12 +117,12 @@ const CopyJobsList: React.FC<CopyJobsListProps> = ({ jobs, handleActionClick, pa
|
||||
/>
|
||||
</ScrollablePane>
|
||||
</Stack.Item>
|
||||
{filteredJobs.length > pageSize && (
|
||||
{sortedJobs.length > pageSize && (
|
||||
<Stack.Item>
|
||||
<Pager
|
||||
disabled={false}
|
||||
startIndex={startIndex}
|
||||
totalCount={filteredJobs.length}
|
||||
totalCount={sortedJobs.length}
|
||||
pageSize={pageSize}
|
||||
onLoadPage={(startIdx /* pageSize */) => {
|
||||
setStartIndex(startIdx);
|
||||
|
||||
@@ -55,15 +55,15 @@ export interface DatabaseContainerSectionProps {
|
||||
export interface CopyJobContextState {
|
||||
jobName: string;
|
||||
migrationType: CopyJobMigrationType;
|
||||
sourceReadWriteAccessFromTarget?: boolean;
|
||||
sourceReadAccessFromTarget?: boolean;
|
||||
source: {
|
||||
subscriptionId: string;
|
||||
subscription: Subscription | null;
|
||||
account: DatabaseAccount | null;
|
||||
databaseId: string;
|
||||
containerId: string;
|
||||
};
|
||||
target: {
|
||||
subscription: Subscription | null;
|
||||
subscriptionId: string;
|
||||
account: DatabaseAccount | null;
|
||||
databaseId: string;
|
||||
containerId: string;
|
||||
|
||||
@@ -1,27 +1,5 @@
|
||||
@import "../../../less/Common/Constants.less";
|
||||
|
||||
.themedTextFieldStyles() {
|
||||
.ms-TextField {
|
||||
.ms-TextField-fieldGroup {
|
||||
background-color: var(--colorNeutralBackground1);
|
||||
border-color: var(--colorNeutralStroke1);
|
||||
}
|
||||
|
||||
.ms-TextField-field {
|
||||
color: var(--colorNeutralForeground1);
|
||||
background-color: var(--colorNeutralBackground1);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--colorNeutralForeground4);
|
||||
}
|
||||
}
|
||||
|
||||
.ms-Label {
|
||||
color: var(--colorNeutralForeground1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Common theme-aware classes
|
||||
.themeText {
|
||||
color: var(--colorNeutralForeground1);
|
||||
@@ -141,8 +119,25 @@
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.themedTextFieldStyles();
|
||||
.ms-TextField {
|
||||
.ms-TextField-fieldGroup {
|
||||
background-color: var(--colorNeutralBackground1);
|
||||
border-color: var(--colorNeutralStroke1);
|
||||
}
|
||||
|
||||
.ms-TextField-field {
|
||||
color: var(--colorNeutralForeground1);
|
||||
background-color: var(--colorNeutralBackground1);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--colorNeutralForeground4);
|
||||
}
|
||||
}
|
||||
|
||||
.ms-Label {
|
||||
color: var(--colorNeutralForeground1);
|
||||
}
|
||||
}
|
||||
.migrationTypeDescription {
|
||||
p {
|
||||
color: var(--colorNeutralForeground1);
|
||||
@@ -178,11 +173,6 @@
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
body.isDarkMode & {
|
||||
.themedTextFieldStyles();
|
||||
}
|
||||
|
||||
.ms-DetailsList {
|
||||
width: 100%;
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@ import {
|
||||
AddGlobalSecondaryIndexPanelProps,
|
||||
} from "Explorer/Panes/AddGlobalSecondaryIndexPanel/AddGlobalSecondaryIndexPanel";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { Keys, t } from "Localization";
|
||||
import { isFabric, isFabricNative, openRestoreContainerDialog } from "Platform/Fabric/FabricUtil";
|
||||
import { isFabric, isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import { traceOpen } from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
@@ -24,9 +23,7 @@ import DeleteSprocIcon from "../../images/DeleteSproc.svg";
|
||||
import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
|
||||
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
|
||||
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
|
||||
import PinIcon from "../../images/Pin.svg";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
@@ -54,45 +51,28 @@ export const createDatabaseContextMenu = (container: Explorer, databaseId: strin
|
||||
if (isFabric() && userContext.fabricContext?.isReadOnly) {
|
||||
return undefined;
|
||||
}
|
||||
const isPinned = useDatabases.getState().isPinned(databaseId);
|
||||
|
||||
const items: TreeNodeMenuItem[] = [
|
||||
{
|
||||
iconSrc: PinIcon,
|
||||
onClick: () => useDatabases.getState().togglePinDatabase(databaseId),
|
||||
label: isPinned ? "Unpin from top" : "Pin to top",
|
||||
},
|
||||
{
|
||||
iconSrc: AddCollectionIcon,
|
||||
onClick: () => container.onNewCollectionClicked({ databaseId }),
|
||||
label: t(Keys.contextMenu.newContainer, { containerName: getCollectionName() }),
|
||||
label: `New ${getCollectionName()}`,
|
||||
},
|
||||
];
|
||||
|
||||
if (isFabricNative() && !userContext.fabricContext?.isReadOnly) {
|
||||
const features = extractFeatures();
|
||||
if (features?.enableRestoreContainer) {
|
||||
items.push({
|
||||
iconSrc: AddCollectionIcon,
|
||||
onClick: () => openRestoreContainerDialog(),
|
||||
label: t(Keys.contextMenu.restoreContainer, { containerName: getCollectionName() }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!isFabricNative() && (userContext.apiType !== "Tables" || userContext.features.enableSDKoperations)) {
|
||||
items.push({
|
||||
iconSrc: DeleteDatabaseIcon,
|
||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||
useSidePanel.getState().getRef = lastFocusedElement;
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getDatabaseName(),
|
||||
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
(useSidePanel.getState().getRef = lastFocusedElement),
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getDatabaseName(),
|
||||
<DeleteDatabaseConfirmationPanel refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
},
|
||||
label: t(Keys.contextMenu.deleteDatabase, { databaseName: getDatabaseName() }),
|
||||
label: `Delete ${getDatabaseName()}`,
|
||||
styleClass: "deleteDatabaseMenuItem",
|
||||
});
|
||||
}
|
||||
@@ -108,7 +88,7 @@ export const createCollectionContextMenuButton = (
|
||||
items.push({
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
onClick: () => selectedCollection && selectedCollection.onNewQueryClick(selectedCollection, undefined),
|
||||
label: t(Keys.contextMenu.newSqlQuery),
|
||||
label: "New SQL Query",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,7 +96,7 @@ export const createCollectionContextMenuButton = (
|
||||
items.push({
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, undefined),
|
||||
label: t(Keys.contextMenu.newQuery),
|
||||
label: "New Query",
|
||||
});
|
||||
|
||||
items.push({
|
||||
@@ -131,8 +111,8 @@ export const createCollectionContextMenuButton = (
|
||||
},
|
||||
label:
|
||||
useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell
|
||||
? t(Keys.contextMenu.openMongoShell)
|
||||
: t(Keys.contextMenu.newShell),
|
||||
? "Open Mongo Shell"
|
||||
: "New Shell",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -145,7 +125,7 @@ export const createCollectionContextMenuButton = (
|
||||
onClick: () => {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Cassandra);
|
||||
},
|
||||
label: t(Keys.contextMenu.openCassandraShell),
|
||||
label: "Open Cassandra Shell",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -158,7 +138,7 @@ export const createCollectionContextMenuButton = (
|
||||
onClick: () => {
|
||||
selectedCollection && selectedCollection.onNewStoredProcedureClick(selectedCollection, undefined);
|
||||
},
|
||||
label: t(Keys.contextMenu.newStoredProcedure),
|
||||
label: "New Stored Procedure",
|
||||
});
|
||||
|
||||
items.push({
|
||||
@@ -166,7 +146,7 @@ export const createCollectionContextMenuButton = (
|
||||
onClick: () => {
|
||||
selectedCollection && selectedCollection.onNewUserDefinedFunctionClick(selectedCollection);
|
||||
},
|
||||
label: t(Keys.contextMenu.newUdf),
|
||||
label: "New UDF",
|
||||
});
|
||||
|
||||
items.push({
|
||||
@@ -174,7 +154,7 @@ export const createCollectionContextMenuButton = (
|
||||
onClick: () => {
|
||||
selectedCollection && selectedCollection.onNewTriggerClick(selectedCollection, undefined);
|
||||
},
|
||||
label: t(Keys.contextMenu.newTrigger),
|
||||
label: "New Trigger",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -183,15 +163,15 @@ export const createCollectionContextMenuButton = (
|
||||
iconSrc: DeleteCollectionIcon,
|
||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
||||
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
||||
useSidePanel.getState().getRef = lastFocusedElement;
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getCollectionName(),
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
(useSidePanel.getState().getRef = lastFocusedElement),
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Delete " + getCollectionName(),
|
||||
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||
);
|
||||
},
|
||||
label: t(Keys.contextMenu.deleteContainer, { containerName: getCollectionName() }),
|
||||
label: `Delete ${getCollectionName()}`,
|
||||
styleClass: "deleteCollectionMenuItem",
|
||||
});
|
||||
}
|
||||
@@ -228,14 +208,14 @@ export const createSampleCollectionContextMenuButton = (): TreeNodeMenuItem[] =>
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||
traceOpen(Action.OpenQueryCopilotFromNewQuery, { apiType: userContext.apiType });
|
||||
},
|
||||
label: t(Keys.contextMenu.newSqlQuery),
|
||||
label: "New SQL Query",
|
||||
});
|
||||
} else if (copilotVersion === "v2.0") {
|
||||
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
|
||||
items.push({
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
onClick: () => sampleCollection && sampleCollection.onNewQueryClick(sampleCollection, undefined),
|
||||
label: t(Keys.contextMenu.newSqlQuery),
|
||||
label: "New SQL Query",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -255,7 +235,7 @@ export const createStoreProcedureContextMenuItems = (
|
||||
{
|
||||
iconSrc: DeleteSprocIcon,
|
||||
onClick: () => storedProcedure.delete(),
|
||||
label: t(Keys.contextMenu.deleteStoredProcedure),
|
||||
label: "Delete Stored Procedure",
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -269,7 +249,7 @@ export const createTriggerContextMenuItems = (container: Explorer, trigger: Trig
|
||||
{
|
||||
iconSrc: DeleteTriggerIcon,
|
||||
onClick: () => trigger.delete(),
|
||||
label: t(Keys.contextMenu.deleteTrigger),
|
||||
label: "Delete Trigger",
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -286,7 +266,7 @@ export const createUserDefinedFunctionContextMenuItems = (
|
||||
{
|
||||
iconSrc: DeleteUDFIcon,
|
||||
onClick: () => userDefinedFunction.delete(),
|
||||
label: t(Keys.contextMenu.deleteUdf),
|
||||
label: "Delete User Defined Function",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -58,11 +58,6 @@ export interface CommandButtonComponentProps {
|
||||
*/
|
||||
tooltipText?: string;
|
||||
|
||||
/**
|
||||
* Rich JSX content for tooltip (used instead of tooltipText when provided)
|
||||
*/
|
||||
tooltipContent?: React.ReactNode;
|
||||
|
||||
/**
|
||||
* Custom styles to apply to the button using Fluent UI theme tokens
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import React, { FC, useEffect } from "react";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { Keys, t } from "Localization";
|
||||
|
||||
export interface DialogState {
|
||||
visible: boolean;
|
||||
@@ -89,7 +88,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
||||
isModal: true,
|
||||
title,
|
||||
subText,
|
||||
primaryButtonText: t(Keys.common.close),
|
||||
primaryButtonText: "Close",
|
||||
secondaryButtonText: undefined,
|
||||
onPrimaryButtonClick: () => {
|
||||
get().closeDialog();
|
||||
|
||||
@@ -30,6 +30,7 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
|
||||
dataMaskingPolicy: {
|
||||
includedPaths: [],
|
||||
excludedPaths: ["/excludedPath"],
|
||||
isPolicyEnabled: true,
|
||||
},
|
||||
indexes: [],
|
||||
}),
|
||||
@@ -306,10 +307,12 @@ describe("SettingsComponent", () => {
|
||||
dataMaskingContent: {
|
||||
includedPaths: [],
|
||||
excludedPaths: ["/excludedPath"],
|
||||
isPolicyEnabled: true,
|
||||
},
|
||||
dataMaskingContentBaseline: {
|
||||
includedPaths: [],
|
||||
excludedPaths: [],
|
||||
isPolicyEnabled: false,
|
||||
},
|
||||
isDataMaskingDirty: true,
|
||||
});
|
||||
@@ -323,6 +326,7 @@ describe("SettingsComponent", () => {
|
||||
expect(wrapper.state("dataMaskingContentBaseline")).toEqual({
|
||||
includedPaths: [],
|
||||
excludedPaths: ["/excludedPath"],
|
||||
isPolicyEnabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -336,6 +340,7 @@ describe("SettingsComponent", () => {
|
||||
const invalidPolicy: InvalidPolicy = {
|
||||
includedPaths: "invalid",
|
||||
excludedPaths: [],
|
||||
isPolicyEnabled: false,
|
||||
};
|
||||
// Use type assertion since we're deliberately testing with invalid data
|
||||
settingsComponentInstance["onDataMaskingContentChange"](invalidPolicy as unknown as DataModels.DataMaskingPolicy);
|
||||
@@ -344,6 +349,7 @@ describe("SettingsComponent", () => {
|
||||
expect(wrapper.state("dataMaskingContent")).toEqual({
|
||||
includedPaths: "invalid",
|
||||
excludedPaths: [],
|
||||
isPolicyEnabled: false,
|
||||
});
|
||||
expect(wrapper.state("dataMaskingValidationErrors")).toEqual(["includedPaths must be an array"]);
|
||||
|
||||
@@ -358,6 +364,7 @@ describe("SettingsComponent", () => {
|
||||
},
|
||||
],
|
||||
excludedPaths: ["/excludedPath"],
|
||||
isPolicyEnabled: true,
|
||||
};
|
||||
|
||||
settingsComponentInstance["onDataMaskingContentChange"](validPolicy);
|
||||
@@ -381,6 +388,7 @@ describe("SettingsComponent", () => {
|
||||
},
|
||||
],
|
||||
excludedPaths: ["/excludedPath1"],
|
||||
isPolicyEnabled: false,
|
||||
};
|
||||
|
||||
const modifiedPolicy = {
|
||||
@@ -393,6 +401,7 @@ describe("SettingsComponent", () => {
|
||||
},
|
||||
],
|
||||
excludedPaths: ["/excludedPath2"],
|
||||
isPolicyEnabled: true,
|
||||
};
|
||||
|
||||
// Set initial state
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isCapabilityEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||
import * as React from "react";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
@@ -44,7 +44,6 @@ import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter
|
||||
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||
import "./SettingsComponent.less";
|
||||
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||
import { Keys, t } from "Localization";
|
||||
import {
|
||||
ConflictResolutionComponent,
|
||||
ConflictResolutionComponentProps,
|
||||
@@ -71,7 +70,6 @@ import {
|
||||
getMongoNotification,
|
||||
getTabTitle,
|
||||
hasDatabaseSharedThroughput,
|
||||
isDataMaskingEnabled,
|
||||
isDirty,
|
||||
parseConflictResolutionMode,
|
||||
parseConflictResolutionProcedure,
|
||||
@@ -688,14 +686,22 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.setState({ isComputedPropertiesDirty: isComputedPropertiesDirty });
|
||||
|
||||
private onDataMaskingContentChange = (newDataMasking: DataModels.DataMaskingPolicy): void => {
|
||||
const validationErrors = [];
|
||||
if (newDataMasking.includedPaths === undefined || newDataMasking.includedPaths === null) {
|
||||
validationErrors.push(t(Keys.controls.settings.dataMasking.includedPathsRequired));
|
||||
} else if (!Array.isArray(newDataMasking.includedPaths)) {
|
||||
validationErrors.push(t(Keys.controls.settings.dataMasking.includedPathsMustBeArray));
|
||||
if (!newDataMasking.excludedPaths) {
|
||||
newDataMasking.excludedPaths = [];
|
||||
}
|
||||
if (newDataMasking.excludedPaths !== undefined && !Array.isArray(newDataMasking.excludedPaths)) {
|
||||
validationErrors.push(t(Keys.controls.settings.dataMasking.excludedPathsMustBeArray));
|
||||
if (!newDataMasking.includedPaths) {
|
||||
newDataMasking.includedPaths = [];
|
||||
}
|
||||
|
||||
const validationErrors = [];
|
||||
if (!Array.isArray(newDataMasking.includedPaths)) {
|
||||
validationErrors.push("includedPaths must be an array");
|
||||
}
|
||||
if (!Array.isArray(newDataMasking.excludedPaths)) {
|
||||
validationErrors.push("excludedPaths must be an array");
|
||||
}
|
||||
if (typeof newDataMasking.isPolicyEnabled !== "boolean") {
|
||||
validationErrors.push("isPolicyEnabled must be a boolean");
|
||||
}
|
||||
|
||||
this.setState({
|
||||
@@ -836,6 +842,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const dataMaskingContent: DataModels.DataMaskingPolicy = {
|
||||
includedPaths: this.collection.dataMaskingPolicy?.()?.includedPaths || [],
|
||||
excludedPaths: this.collection.dataMaskingPolicy?.()?.excludedPaths || [],
|
||||
isPolicyEnabled: this.collection.dataMaskingPolicy?.()?.isPolicyEnabled ?? true,
|
||||
};
|
||||
const conflictResolutionPolicy: DataModels.ConflictResolutionPolicy =
|
||||
this.collection.conflictResolutionPolicy && this.collection.conflictResolutionPolicy();
|
||||
@@ -897,7 +904,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
const isExecuting = this.props.settingsTab.isExecuting();
|
||||
if (this.saveSettingsButton.isVisible()) {
|
||||
const label = t(Keys.common.save);
|
||||
const label = "Save";
|
||||
buttons.push({
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: label,
|
||||
@@ -910,7 +917,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
|
||||
if (this.discardSettingsChangesButton.isVisible()) {
|
||||
const label = t(Keys.common.discard);
|
||||
const label = "Discard";
|
||||
buttons.push({
|
||||
iconSrc: DiscardIcon,
|
||||
iconAlt: label,
|
||||
@@ -935,10 +942,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
|
||||
const throughputDelta = (newThroughput - this.offer.autoscaleMaxThroughput) * numberOfRegions;
|
||||
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
|
||||
throughputError = t(Keys.controls.settings.throughput.throughputCapError, {
|
||||
throughputCap: String(throughputCap),
|
||||
newTotalThroughput: String(this.totalThroughputUsed + throughputDelta),
|
||||
});
|
||||
throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
|
||||
this.totalThroughputUsed + throughputDelta
|
||||
} RU/s. Change total throughput limit in cost management.`;
|
||||
}
|
||||
this.setState({ autoPilotThroughput: newThroughput, throughputError });
|
||||
};
|
||||
@@ -949,10 +955,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const numberOfRegions = userContext.databaseAccount?.properties.locations?.length || 1;
|
||||
const throughputDelta = (newThroughput - this.offer.manualThroughput) * numberOfRegions;
|
||||
if (throughputCap && throughputCap !== -1 && throughputCap - this.totalThroughputUsed < throughputDelta) {
|
||||
throughputError = t(Keys.controls.settings.throughput.throughputCapError, {
|
||||
throughputCap: String(throughputCap),
|
||||
newTotalThroughput: String(this.totalThroughputUsed + throughputDelta),
|
||||
});
|
||||
throughputError = `Your account is currently configured with a total throughput limit of ${throughputCap} RU/s. This update isn't possible because it would increase the total throughput to ${
|
||||
this.totalThroughputUsed + throughputDelta
|
||||
} RU/s. Change total throughput limit in cost management.`;
|
||||
}
|
||||
this.setState({ throughput: newThroughput, throughputError });
|
||||
};
|
||||
@@ -1068,8 +1073,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
newCollection.fullTextPolicy = this.state.fullTextPolicy;
|
||||
|
||||
// Only send data masking policy if it was modified (dirty) and data masking is enabled
|
||||
if (this.state.isDataMaskingDirty && isDataMaskingEnabled(this.collection.dataMaskingPolicy?.())) {
|
||||
// Only send data masking policy if it was modified (dirty)
|
||||
if (this.state.isDataMaskingDirty && isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking)) {
|
||||
newCollection.dataMaskingPolicy = this.state.dataMaskingContent;
|
||||
}
|
||||
|
||||
@@ -1458,7 +1463,15 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
});
|
||||
}
|
||||
|
||||
if (isDataMaskingEnabled(this.collection.dataMaskingPolicy?.())) {
|
||||
// Check if DDM should be enabled
|
||||
const shouldEnableDDM = (): boolean => {
|
||||
const hasDataMaskingCapability = isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking);
|
||||
const isSqlAccount = userContext.apiType === "SQL";
|
||||
|
||||
return isSqlAccount && hasDataMaskingCapability; // Only show for SQL accounts with DDM capability
|
||||
};
|
||||
|
||||
if (shouldEnableDDM()) {
|
||||
const dataMaskingComponentProps: DataMaskingComponentProps = {
|
||||
shouldDiscardDataMasking: this.state.shouldDiscardDataMasking,
|
||||
resetShouldDiscardDataMasking: this.resetShouldDiscardDataMasking,
|
||||
@@ -1563,7 +1576,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
>
|
||||
{this.shouldShowKeyspaceSharedThroughputMessage() && (
|
||||
<div>{t(Keys.controls.settings.scale.keyspaceSharedThroughput)}</div>
|
||||
<div>This table shared throughput is configured at the keyspace</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import * as React from "react";
|
||||
import { Urls } from "../../../Common/Constants";
|
||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||
@@ -339,12 +338,10 @@ export const getEstimatedSpendingElement = (
|
||||
const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : "";
|
||||
return (
|
||||
<Stack>
|
||||
<Text style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.controls.settings.costEstimate.title)}
|
||||
</Text>
|
||||
<Text style={{ fontWeight: 600, color: "var(--colorNeutralForeground1)" }}>Cost estimate*</Text>
|
||||
{costElement}
|
||||
<Text style={{ fontWeight: 600, marginTop: 15, color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.controls.settings.costEstimate.howWeCalculate)}
|
||||
How we calculate this
|
||||
</Text>
|
||||
<Stack id="throughputSpendElement" style={{ marginTop: 5 }}>
|
||||
<span>
|
||||
@@ -356,8 +353,7 @@ export const getEstimatedSpendingElement = (
|
||||
</span>
|
||||
<span>
|
||||
{priceBreakdown.currencySign}
|
||||
{priceBreakdown.pricePerRu}
|
||||
{t(Keys.controls.settings.costEstimate.perRu)}
|
||||
{priceBreakdown.pricePerRu}/RU
|
||||
</span>
|
||||
</Stack>
|
||||
<Text style={{ marginTop: 15, color: "var(--colorNeutralForeground1)" }}>
|
||||
@@ -369,16 +365,18 @@ export const getEstimatedSpendingElement = (
|
||||
|
||||
export const manualToAutoscaleDisclaimerElement: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
|
||||
{t(Keys.controls.settings.throughput.manualToAutoscaleDisclaimer)}{" "}
|
||||
<Link href={Urls.autoscaleMigration}>{t(Keys.common.learnMore)}</Link>
|
||||
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings
|
||||
and storage of your resource. After autoscale has been enabled, you can change the max RU/s.{" "}
|
||||
<Link href={Urls.autoscaleMigration}>Learn more</Link>
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const ttlWarning: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
{t(Keys.controls.settings.throughput.ttlWarningText)}{" "}
|
||||
The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete
|
||||
operation explicitly issued by a client application. For more information see,{" "}
|
||||
<Link target="_blank" href="https://aka.ms/cosmos-db-ttl">
|
||||
{t(Keys.controls.settings.throughput.ttlWarningLinkText)}
|
||||
Time to Live (TTL) in Azure Cosmos DB
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
@@ -386,28 +384,29 @@ export const ttlWarning: JSX.Element = (
|
||||
|
||||
export const unsavedEditorWarningMessage = (editor: editorType): JSX.Element => (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
{t(Keys.controls.settings.throughput.unsavedEditorWarningPrefix)}{" "}
|
||||
You have not saved the latest changes made to your{" "}
|
||||
{editor === "indexPolicy"
|
||||
? t(Keys.controls.settings.throughput.unsavedIndexingPolicy)
|
||||
? "indexing policy"
|
||||
: editor === "dataMasking"
|
||||
? t(Keys.controls.settings.throughput.unsavedDataMaskingPolicy)
|
||||
: t(Keys.controls.settings.throughput.unsavedComputedProperties)}
|
||||
{t(Keys.controls.settings.throughput.unsavedEditorWarningSuffix)}
|
||||
? "data masking policy"
|
||||
: "computed properties"}
|
||||
. Please click save to confirm the changes.
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
|
||||
{t(Keys.controls.settings.throughput.updateDelayedApplyWarning)}
|
||||
You are about to request an increase in throughput beyond the pre-allocated capacity. This operation will take some
|
||||
time to complete.
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const getUpdateThroughputBeyondInstantLimitMessage = (instantMaximumThroughput: number): JSX.Element => {
|
||||
return (
|
||||
<Text id="updateThroughputDelayedApplyWarningMessage">
|
||||
{t(Keys.controls.settings.throughput.scalingUpDelayMessage, {
|
||||
instantMaximumThroughput: String(instantMaximumThroughput),
|
||||
})}
|
||||
Scaling up will take 4-6 hours as it exceeds what Azure Cosmos DB can instantly support currently based on your
|
||||
number of physical partitions. You can increase your throughput to {instantMaximumThroughput} instantly or proceed
|
||||
with this value and wait until the scale-up is completed.
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
@@ -419,26 +418,22 @@ export const getUpdateThroughputBeyondSupportLimitMessage = (
|
||||
return (
|
||||
<>
|
||||
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
|
||||
{t(Keys.controls.settings.throughput.exceedPreAllocatedMessage)}
|
||||
Your request to increase throughput exceeds the pre-allocated capacity which may take longer than expected.
|
||||
There are three options you can choose from to proceed:
|
||||
</Text>
|
||||
<ol style={{ fontSize: 14, color: "var(--colorNeutralForeground1)", marginTop: "5px" }}>
|
||||
<li>
|
||||
{t(Keys.controls.settings.throughput.instantScaleOption, {
|
||||
instantMaximumThroughput: String(instantMaximumThroughput),
|
||||
})}
|
||||
</li>
|
||||
<li>You can instantly scale up to {instantMaximumThroughput} RU/s.</li>
|
||||
{instantMaximumThroughput < maximumThroughput && (
|
||||
<li>
|
||||
{t(Keys.controls.settings.throughput.asyncScaleOption, { maximumThroughput: String(maximumThroughput) })}
|
||||
</li>
|
||||
<li>You can asynchronously scale up to any value under {maximumThroughput} RU/s in 4-6 hours.</li>
|
||||
)}
|
||||
<li>
|
||||
{t(Keys.controls.settings.throughput.quotaMaxOption, { maximumThroughput: String(maximumThroughput) })}
|
||||
Your current quota max is {maximumThroughput} RU/s. To go over this limit, you must request a quota increase
|
||||
and the Azure Cosmos DB team will review.
|
||||
<Link
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase"
|
||||
target="_blank"
|
||||
>
|
||||
{t(Keys.common.learnMore)}
|
||||
Learn more
|
||||
</Link>
|
||||
</li>
|
||||
</ol>
|
||||
@@ -449,19 +444,23 @@ export const getUpdateThroughputBeyondSupportLimitMessage = (
|
||||
export const getUpdateThroughputBelowMinimumMessage = (minimum: number): JSX.Element => {
|
||||
return (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
{t(Keys.controls.settings.throughput.belowMinimumMessage, { minimum: String(minimum) })}
|
||||
You are not able to lower throughput below your current minimum of {minimum} RU/s. For more information on this
|
||||
limit, please refer to our service quote documentation.
|
||||
<Link
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
|
||||
target="_blank"
|
||||
>
|
||||
{t(Keys.common.learnMore)}
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export const saveThroughputWarningMessage: JSX.Element = (
|
||||
<Text>{t(Keys.controls.settings.throughput.saveThroughputWarning)}</Text>
|
||||
<Text>
|
||||
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below
|
||||
before saving your changes
|
||||
</Text>
|
||||
);
|
||||
|
||||
const getCurrentThroughput = (
|
||||
@@ -473,29 +472,23 @@ const getCurrentThroughput = (
|
||||
if (targetThroughput) {
|
||||
if (throughput) {
|
||||
return isAutoscale
|
||||
? `, ${t(Keys.controls.settings.throughput.currentAutoscaleThroughput)} ${Math.round(
|
||||
? `, Current autoscale throughput: ${Math.round(
|
||||
throughput / 10,
|
||||
)} - ${throughput} ${throughputUnit}, ${t(
|
||||
Keys.controls.settings.throughput.targetAutoscaleThroughput,
|
||||
)} ${Math.round(targetThroughput / 10)} - ${targetThroughput} ${throughputUnit}`
|
||||
: `, ${t(Keys.controls.settings.throughput.currentManualThroughput)} ${throughput} ${throughputUnit}, ${t(
|
||||
Keys.controls.settings.throughput.targetManualThroughput,
|
||||
)} ${targetThroughput}`;
|
||||
} else {
|
||||
return isAutoscale
|
||||
? `, ${t(Keys.controls.settings.throughput.targetAutoscaleThroughput)} ${Math.round(
|
||||
)} - ${throughput} ${throughputUnit}, Target autoscale throughput: ${Math.round(
|
||||
targetThroughput / 10,
|
||||
)} - ${targetThroughput} ${throughputUnit}`
|
||||
: `, ${t(Keys.controls.settings.throughput.targetManualThroughput)} ${targetThroughput} ${throughputUnit}`;
|
||||
: `, Current manual throughput: ${throughput} ${throughputUnit}, Target manual throughput: ${targetThroughput}`;
|
||||
} else {
|
||||
return isAutoscale
|
||||
? `, Target autoscale throughput: ${Math.round(targetThroughput / 10)} - ${targetThroughput} ${throughputUnit}`
|
||||
: `, Target manual throughput: ${targetThroughput} ${throughputUnit}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetThroughput && throughput) {
|
||||
return isAutoscale
|
||||
? `, ${t(Keys.controls.settings.throughput.currentAutoscaleThroughput)} ${Math.round(
|
||||
throughput / 10,
|
||||
)} - ${throughput} ${throughputUnit}`
|
||||
: `, ${t(Keys.controls.settings.throughput.currentManualThroughput)} ${throughput} ${throughputUnit}`;
|
||||
? `, Current autoscale throughput: ${Math.round(throughput / 10)} - ${throughput} ${throughputUnit}`
|
||||
: `, Current manual throughput: ${throughput} ${throughputUnit}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
@@ -510,10 +503,10 @@ export const getThroughputApplyDelayedMessage = (
|
||||
requestedThroughput: number,
|
||||
): JSX.Element => (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
{t(Keys.controls.settings.throughput.applyDelayedMessage)}
|
||||
The request to increase the throughput has successfully been submitted. This operation will take 1-3 business days
|
||||
to complete. View the latest status in Notifications.
|
||||
<br />
|
||||
{t(Keys.controls.settings.throughput.databaseLabel)} {databaseName},{" "}
|
||||
{t(Keys.controls.settings.throughput.containerLabel)} {collectionName}{" "}
|
||||
Database: {databaseName}, Container: {collectionName}{" "}
|
||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
|
||||
</Text>
|
||||
);
|
||||
@@ -526,13 +519,9 @@ export const getThroughputApplyShortDelayMessage = (
|
||||
collectionName: string,
|
||||
): JSX.Element => (
|
||||
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
|
||||
{t(Keys.controls.settings.throughput.applyShortDelayMessage)}
|
||||
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
||||
<br />
|
||||
{collectionName
|
||||
? `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName}, ${t(
|
||||
Keys.controls.settings.throughput.containerLabel,
|
||||
)} ${collectionName} `
|
||||
: `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName} `}
|
||||
{collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `}
|
||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
|
||||
</Text>
|
||||
);
|
||||
@@ -546,13 +535,10 @@ export const getThroughputApplyLongDelayMessage = (
|
||||
requestedThroughput: number,
|
||||
): JSX.Element => (
|
||||
<Text styles={infoAndToolTipTextStyle} id="throughputApplyLongDelayMessage">
|
||||
{t(Keys.controls.settings.throughput.applyLongDelayMessage)}
|
||||
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to
|
||||
complete. View the latest status in Notifications.
|
||||
<br />
|
||||
{collectionName
|
||||
? `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName}, ${t(
|
||||
Keys.controls.settings.throughput.containerLabel,
|
||||
)} ${collectionName} `
|
||||
: `${t(Keys.controls.settings.throughput.databaseLabel)} ${databaseName} `}
|
||||
{collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `}
|
||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
|
||||
</Text>
|
||||
);
|
||||
@@ -561,49 +547,63 @@ export const getToolTipContainer = (content: string | JSX.Element): JSX.Element
|
||||
content ? <Text styles={infoAndToolTipTextStyle}>{content}</Text> : undefined;
|
||||
|
||||
export const conflictResolutionLwwTooltip: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.conflictResolution.lwwTooltip)}</Text>
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
Gets or sets the name of a integer property in your documents which is used for the Last Write Wins (LWW) based
|
||||
conflict resolution scheme. By default, the system uses the system defined timestamp property, _ts to decide the
|
||||
winner for the conflicting versions of the document. Specify your own integer property if you want to override the
|
||||
default timestamp based conflict resolution.
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const conflictResolutionCustomToolTip: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
{t(Keys.controls.settings.conflictResolution.customTooltip)}
|
||||
Gets or sets the name of a stored procedure (aka merge procedure) for resolving the conflicts. You can write
|
||||
application defined logic to determine the winner of the conflicting versions of a document. The stored procedure
|
||||
will get executed transactionally, exactly once, on the server side. If you do not provide a stored procedure, the
|
||||
conflicts will be populated in the
|
||||
<Link className="linkDarkBackground" href="https://aka.ms/dataexplorerconflics" target="_blank">
|
||||
{t(Keys.controls.settings.conflictResolution.customTooltipConflictsFeed)}
|
||||
{` conflicts feed`}
|
||||
</Link>
|
||||
{t(Keys.controls.settings.conflictResolution.customTooltipSuffix)}
|
||||
. You can update/re-register the stored procedure at any time.
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const changeFeedPolicyToolTip: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.changeFeed.tooltip)}</Text>
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
Enable change feed log retention policy to retain last 10 minutes of history for items in the container by default.
|
||||
To support this, the request unit (RU) charge for this container will be multiplied by a factor of two for writes.
|
||||
Reads are unaffected.
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const mongoIndexingPolicyDisclaimer: JSX.Element = (
|
||||
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.controls.settings.mongoIndexing.disclaimer)}
|
||||
For queries that filter on multiple properties, create multiple single field indexes instead of a compound index.
|
||||
<Link
|
||||
href="https://docs.microsoft.com/azure/cosmos-db/mongodb-indexing#index-types"
|
||||
target="_blank"
|
||||
style={{ color: "var(--colorBrandForeground1)" }}
|
||||
>
|
||||
{t(Keys.controls.settings.mongoIndexing.disclaimerCompoundIndexesLink)}
|
||||
{` Compound indexes `}
|
||||
</Link>
|
||||
{t(Keys.controls.settings.mongoIndexing.disclaimerSuffix)}
|
||||
are only used for sorting query results. If you need to add a compound index, you can create one using the Mongo
|
||||
shell.
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const mongoCompoundIndexNotSupportedMessage: JSX.Element = (
|
||||
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.controls.settings.mongoIndexing.compoundNotSupported)}
|
||||
Collections with compound indexes are not yet supported in the indexing editor. To modify indexing policy for this
|
||||
collection, use the Mongo Shell.
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const mongoIndexingPolicyAADError: JSX.Element = (
|
||||
<MessageBar messageBarType={MessageBarType.error}>
|
||||
<Text>
|
||||
{t(Keys.controls.settings.mongoIndexing.aadError)}
|
||||
To use the indexing policy editor, please login to the
|
||||
<Link target="_blank" href="https://portal.azure.com">
|
||||
{t(Keys.controls.settings.mongoIndexing.aadErrorLink)}
|
||||
{"azure portal."}
|
||||
</Link>
|
||||
</Text>
|
||||
</MessageBar>
|
||||
@@ -611,7 +611,7 @@ export const mongoIndexingPolicyAADError: JSX.Element = (
|
||||
|
||||
export const mongoIndexTransformationRefreshingMessage: JSX.Element = (
|
||||
<Stack horizontal {...mongoWarningStackProps}>
|
||||
<Text styles={infoAndToolTipTextStyle}>{t(Keys.controls.settings.mongoIndexing.refreshingProgress)}</Text>
|
||||
<Text styles={infoAndToolTipTextStyle}>Refreshing index transformation progress</Text>
|
||||
<Spinner size={SpinnerSize.small} />
|
||||
</Stack>
|
||||
);
|
||||
@@ -623,18 +623,15 @@ export const renderMongoIndexTransformationRefreshMessage = (
|
||||
if (progress === 0) {
|
||||
return (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
{t(Keys.controls.settings.mongoIndexing.canMakeMoreChangesZero)}
|
||||
<Link onClick={performRefresh}>{t(Keys.controls.settings.mongoIndexing.refreshToCheck)}</Link>
|
||||
{"You can make more indexing changes once the current index transformation is complete. "}
|
||||
<Link onClick={performRefresh}>{"Refresh to check if it has completed."}</Link>
|
||||
</Text>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
{`${t(Keys.controls.settings.mongoIndexing.canMakeMoreChangesProgress).replace(
|
||||
"{{progress}}",
|
||||
String(progress),
|
||||
)} `}
|
||||
<Link onClick={performRefresh}>{t(Keys.controls.settings.mongoIndexing.refreshToCheckProgress)}</Link>
|
||||
{`You can make more indexing changes once the current index transformation has completed. It is ${progress}% complete. `}
|
||||
<Link onClick={performRefresh}>{"Refresh to check the progress."}</Link>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { titleAndInputStackProps, unsavedEditorWarningMessage } from "Explorer/C
|
||||
import { isDirty } from "Explorer/Controls/Settings/SettingsUtils";
|
||||
import { loadMonaco } from "Explorer/LazyMonaco";
|
||||
import { monacoTheme, useThemeStore } from "hooks/useTheme";
|
||||
import { Keys, t } from "Localization";
|
||||
import * as monaco from "monaco-editor";
|
||||
import * as React from "react";
|
||||
export interface ComputedPropertiesComponentProps {
|
||||
@@ -108,7 +107,7 @@ export class ComputedPropertiesComponent extends React.Component<
|
||||
this.computedPropertiesEditor = monaco.editor.create(this.computedPropertiesDiv.current, {
|
||||
value: value,
|
||||
language: "json",
|
||||
ariaLabel: t(Keys.controls.settings.computedProperties.ariaLabel),
|
||||
ariaLabel: "Computed properties",
|
||||
theme: monacoTheme(),
|
||||
});
|
||||
if (this.computedPropertiesEditor) {
|
||||
@@ -152,9 +151,9 @@ export class ComputedPropertiesComponent extends React.Component<
|
||||
)}
|
||||
<Text style={{ marginLeft: "30px", marginBottom: "10px", color: "var(--colorNeutralForeground1)" }}>
|
||||
<Link target="_blank" href="https://aka.ms/computed-properties-preview/">
|
||||
{t(Keys.common.learnMore)} <FontIcon iconName="NavigateExternalInline" />
|
||||
{"Learn more"} <FontIcon iconName="NavigateExternalInline" />
|
||||
</Link>
|
||||
  {t(Keys.controls.settings.computedProperties.learnMorePrefix)}
|
||||
  about how to define computed properties and how to use them.
|
||||
</Text>
|
||||
<div
|
||||
className="settingsV2Editor"
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ChoiceGroup, IChoiceGroupOption, ITextFieldProps, Stack, TextField } fr
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import { Keys, t } from "Localization";
|
||||
import {
|
||||
conflictResolutionCustomToolTip,
|
||||
conflictResolutionLwwTooltip,
|
||||
@@ -33,12 +32,9 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
|
||||
private conflictResolutionChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{
|
||||
key: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||
text: t(Keys.controls.settings.conflictResolution.lwwDefault),
|
||||
},
|
||||
{
|
||||
key: DataModels.ConflictResolutionMode.Custom,
|
||||
text: t(Keys.controls.settings.conflictResolution.customMergeProcedure),
|
||||
text: "Last Write Wins (default)",
|
||||
},
|
||||
{ key: DataModels.ConflictResolutionMode.Custom, text: "Merge Procedure (custom)" },
|
||||
];
|
||||
|
||||
componentDidMount(): void {
|
||||
@@ -89,7 +85,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
|
||||
|
||||
private getConflictResolutionModeComponent = (): JSX.Element => (
|
||||
<ChoiceGroup
|
||||
label={t(Keys.controls.settings.conflictResolution.mode)}
|
||||
label="Mode"
|
||||
selectedKey={this.props.conflictResolutionPolicyMode}
|
||||
options={this.conflictResolutionChoiceGroupOptions}
|
||||
onChange={this.onConflictResolutionPolicyModeChange}
|
||||
@@ -107,7 +103,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
|
||||
private getConflictResolutionLWWComponent = (): JSX.Element => (
|
||||
<TextField
|
||||
id="conflictResolutionLwwTextField"
|
||||
label={t(Keys.controls.settings.conflictResolution.conflictResolverProperty)}
|
||||
label={"Conflict Resolver Property"}
|
||||
onRenderLabel={this.onRenderLwwComponentTextField}
|
||||
styles={{
|
||||
fieldGroup: {
|
||||
@@ -162,7 +158,7 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
|
||||
return (
|
||||
<TextField
|
||||
id="conflictResolutionCustomTextField"
|
||||
label={t(Keys.controls.settings.conflictResolution.storedProcedure)}
|
||||
label="Stored procedure"
|
||||
onRenderLabel={this.onRenderCustomComponentTextField}
|
||||
styles={{
|
||||
fieldGroup: {
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils";
|
||||
import { ContainerPolicyTabTypes, isDirty } from "Explorer/Controls/Settings/SettingsUtils";
|
||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
|
||||
export interface ContainerPolicyComponentProps {
|
||||
@@ -154,7 +153,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
|
||||
<PivotItem
|
||||
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.VectorPolicyTab]}
|
||||
style={{ marginTop: 20, color: "var(--colorNeutralForeground1)" }}
|
||||
headerText={t(Keys.controls.settings.containerPolicy.vectorPolicy)}
|
||||
headerText="Vector Policy"
|
||||
>
|
||||
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
|
||||
{vectorEmbeddings && (
|
||||
@@ -176,7 +175,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
|
||||
<PivotItem
|
||||
itemKey={ContainerPolicyTabTypes[ContainerPolicyTabTypes.FullTextPolicyTab]}
|
||||
style={{ marginTop: 20, color: "var(--colorNeutralForeground1)" }}
|
||||
headerText={t(Keys.controls.settings.containerPolicy.fullTextPolicy)}
|
||||
headerText="Full Text Policy"
|
||||
>
|
||||
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative", maxWidth: "400px" } }}>
|
||||
{fullTextSearchPolicy ? (
|
||||
@@ -219,7 +218,7 @@ export const ContainerPolicyComponent: React.FC<ContainerPolicyComponentProps> =
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t(Keys.controls.settings.containerPolicy.createFullTextPolicy)}
|
||||
Create new full text search policy
|
||||
</DefaultButton>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -53,6 +53,7 @@ describe("DataMaskingComponent", () => {
|
||||
},
|
||||
],
|
||||
excludedPaths: [],
|
||||
isPolicyEnabled: false,
|
||||
};
|
||||
|
||||
let changeContentCallback: () => void;
|
||||
@@ -77,7 +78,7 @@ describe("DataMaskingComponent", () => {
|
||||
<DataMaskingComponent
|
||||
{...mockProps}
|
||||
dataMaskingContent={samplePolicy}
|
||||
dataMaskingContentBaseline={{ ...samplePolicy, excludedPaths: ["/excluded"] }}
|
||||
dataMaskingContentBaseline={{ ...samplePolicy, isPolicyEnabled: true }}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -122,7 +123,7 @@ describe("DataMaskingComponent", () => {
|
||||
});
|
||||
|
||||
it("resets content when shouldDiscardDataMasking is true", async () => {
|
||||
const baselinePolicy = { ...samplePolicy, excludedPaths: ["/excluded"] };
|
||||
const baselinePolicy = { ...samplePolicy, isPolicyEnabled: true };
|
||||
|
||||
const wrapper = mount(
|
||||
<DataMaskingComponent
|
||||
@@ -158,7 +159,7 @@ describe("DataMaskingComponent", () => {
|
||||
wrapper.update();
|
||||
|
||||
// Update baseline to trigger componentDidUpdate
|
||||
const newBaseline = { ...samplePolicy, excludedPaths: ["/excluded"] };
|
||||
const newBaseline = { ...samplePolicy, isPolicyEnabled: true };
|
||||
wrapper.setProps({ dataMaskingContentBaseline: newBaseline });
|
||||
|
||||
expect(mockProps.onDataMaskingDirtyChange).toHaveBeenCalledWith(true);
|
||||
@@ -173,6 +174,7 @@ describe("DataMaskingComponent", () => {
|
||||
const invalidPolicy: Record<string, unknown> = {
|
||||
includedPaths: "not an array",
|
||||
excludedPaths: [] as string[],
|
||||
isPolicyEnabled: "not a boolean",
|
||||
};
|
||||
|
||||
mockGetValue.mockReturnValue(JSON.stringify(invalidPolicy));
|
||||
@@ -195,7 +197,7 @@ describe("DataMaskingComponent", () => {
|
||||
wrapper.update();
|
||||
|
||||
// First change
|
||||
const modifiedPolicy1 = { ...samplePolicy, excludedPaths: ["/path1"] };
|
||||
const modifiedPolicy1 = { ...samplePolicy, isPolicyEnabled: true };
|
||||
mockGetValue.mockReturnValue(JSON.stringify(modifiedPolicy1));
|
||||
changeContentCallback();
|
||||
expect(mockProps.onDataMaskingDirtyChange).toHaveBeenCalledWith(true);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { MessageBar, MessageBarType, Stack } from "@fluentui/react";
|
||||
import * as monaco from "monaco-editor";
|
||||
import * as React from "react";
|
||||
import * as Constants from "../../../../Common/Constants";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import { Keys, t } from "Localization";
|
||||
import { isCapabilityEnabled } from "../../../../Utils/CapabilityUtils";
|
||||
import { loadMonaco } from "../../../LazyMonaco";
|
||||
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
|
||||
import { isDirty as isContentDirty, isDataMaskingEnabled } from "../SettingsUtils";
|
||||
import { isDirty as isContentDirty } from "../SettingsUtils";
|
||||
|
||||
export interface DataMaskingComponentProps {
|
||||
shouldDiscardDataMasking: boolean;
|
||||
@@ -23,8 +24,16 @@ interface DataMaskingComponentState {
|
||||
}
|
||||
|
||||
const emptyDataMaskingPolicy: DataModels.DataMaskingPolicy = {
|
||||
includedPaths: [],
|
||||
includedPaths: [
|
||||
{
|
||||
path: "/",
|
||||
strategy: "Default",
|
||||
startPosition: 0,
|
||||
length: -1,
|
||||
},
|
||||
],
|
||||
excludedPaths: [],
|
||||
isPolicyEnabled: true,
|
||||
};
|
||||
|
||||
export class DataMaskingComponent extends React.Component<DataMaskingComponentProps, DataMaskingComponentState> {
|
||||
@@ -90,7 +99,7 @@ export class DataMaskingComponent extends React.Component<DataMaskingComponentPr
|
||||
value: value,
|
||||
language: "json",
|
||||
automaticLayout: true,
|
||||
ariaLabel: t(Keys.controls.settings.dataMasking.ariaLabel),
|
||||
ariaLabel: "Data Masking Policy",
|
||||
fontSize: 13,
|
||||
minimap: { enabled: false },
|
||||
wordWrap: "off",
|
||||
@@ -131,7 +140,7 @@ export class DataMaskingComponent extends React.Component<DataMaskingComponentPr
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (!isDataMaskingEnabled(this.props.dataMaskingContent)) {
|
||||
if (!isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -143,7 +152,7 @@ export class DataMaskingComponent extends React.Component<DataMaskingComponentPr
|
||||
)}
|
||||
{this.props.validationErrors.length > 0 && (
|
||||
<MessageBar messageBarType={MessageBarType.error}>
|
||||
{t(Keys.controls.settings.dataMasking.validationFailed)} {this.props.validationErrors.join(", ")}
|
||||
Validation failed: {this.props.validationErrors.join(", ")}
|
||||
</MessageBar>
|
||||
)}
|
||||
<div className="settingsV2Editor" tabIndex={0} ref={this.dataMaskingDiv}></div>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { FontIcon, Link, Stack, Text } from "@fluentui/react";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import React from "react";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import { Keys, t } from "Localization";
|
||||
import { GlobalSecondaryIndexSourceComponent } from "./GlobalSecondaryIndexSourceComponent";
|
||||
import { GlobalSecondaryIndexTargetComponent } from "./GlobalSecondaryIndexTargetComponent";
|
||||
|
||||
@@ -22,9 +21,7 @@ export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexCompone
|
||||
<Stack tokens={{ childrenGap: 8 }} styles={{ root: { maxWidth: 600 } }}>
|
||||
<Stack horizontal verticalAlign="center" wrap tokens={{ childrenGap: 8 }}>
|
||||
{isSourceContainer && (
|
||||
<Text styles={{ root: { fontWeight: 600 } }}>
|
||||
{t(Keys.controls.settings.globalSecondaryIndex.indexesDefined)}
|
||||
</Text>
|
||||
<Text styles={{ root: { fontWeight: 600 } }}>This container has the following indexes defined for it.</Text>
|
||||
)}
|
||||
<Text>
|
||||
<Link
|
||||
@@ -34,7 +31,7 @@ export const GlobalSecondaryIndexComponent: React.FC<GlobalSecondaryIndexCompone
|
||||
Learn more
|
||||
<FontIcon iconName="NavigateExternalInline" style={{ marginLeft: "4px" }} />
|
||||
</Link>{" "}
|
||||
{t(Keys.controls.settings.globalSecondaryIndex.learnMoreSuffix)}
|
||||
about how to define global secondary indexes and how to use them.
|
||||
</Text>
|
||||
</Stack>
|
||||
{isSourceContainer && <GlobalSecondaryIndexSourceComponent collection={collection} explorer={explorer} />}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useSidePanel } from "hooks/useSidePanel";
|
||||
import * as monaco from "monaco-editor";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import { Keys, t } from "Localization";
|
||||
|
||||
export interface GlobalSecondaryIndexSourceComponentProps {
|
||||
collection: ViewModels.Collection;
|
||||
@@ -68,7 +67,7 @@ export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexS
|
||||
editorRef.current = monacoInstance.editor.create(editorContainerRef.current, {
|
||||
value: jsonValue,
|
||||
language: "json",
|
||||
ariaLabel: t(Keys.controls.settings.globalSecondaryIndex.jsonAriaLabel),
|
||||
ariaLabel: "Global Secondary Index JSON",
|
||||
readOnly: true,
|
||||
});
|
||||
};
|
||||
@@ -99,7 +98,7 @@ export const GlobalSecondaryIndexSourceComponent: React.FC<GlobalSecondaryIndexS
|
||||
}}
|
||||
/>
|
||||
<PrimaryButton
|
||||
text={t(Keys.controls.settings.globalSecondaryIndex.addIndex)}
|
||||
text="Add index"
|
||||
styles={{ root: { width: "fit-content", marginTop: 12 } }}
|
||||
onClick={() =>
|
||||
useSidePanel
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Stack, Text } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import { Keys, t } from "Localization";
|
||||
|
||||
export interface GlobalSecondaryIndexTargetComponentProps {
|
||||
collection: ViewModels.Collection;
|
||||
@@ -26,21 +25,17 @@ export const GlobalSecondaryIndexTargetComponent: React.FC<GlobalSecondaryIndexT
|
||||
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 15 }} styles={{ root: { maxWidth: 600 } }}>
|
||||
<Text styles={textHeadingStyle}>{t(Keys.controls.settings.globalSecondaryIndex.settingsTitle)}</Text>
|
||||
<Text styles={textHeadingStyle}>Global Secondary Index Settings</Text>
|
||||
|
||||
<Stack tokens={{ childrenGap: 5 }}>
|
||||
<Text styles={{ root: { fontWeight: "600" } }}>
|
||||
{t(Keys.controls.settings.globalSecondaryIndex.sourceContainer)}
|
||||
</Text>
|
||||
<Text styles={{ root: { fontWeight: "600" } }}>Source container</Text>
|
||||
<Stack styles={valueBoxStyle}>
|
||||
<Text>{globalSecondaryIndexDefinition?.sourceCollectionId}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack tokens={{ childrenGap: 5 }}>
|
||||
<Text styles={{ root: { fontWeight: "600" } }}>
|
||||
{t(Keys.controls.settings.globalSecondaryIndex.indexDefinition)}
|
||||
</Text>
|
||||
<Text styles={{ root: { fontWeight: "600" } }}>Global secondary index definition</Text>
|
||||
<Stack styles={valueBoxStyle}>
|
||||
<Text>{globalSecondaryIndexDefinition?.definition}</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { monacoTheme, useThemeStore } from "hooks/useTheme";
|
||||
import * as monaco from "monaco-editor";
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import { Keys, t } from "Localization";
|
||||
import { loadMonaco } from "../../../LazyMonaco";
|
||||
import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils";
|
||||
import { isDirty, isIndexTransforming } from "../SettingsUtils";
|
||||
@@ -120,7 +119,7 @@ export class IndexingPolicyComponent extends React.Component<
|
||||
value: value,
|
||||
language: "json",
|
||||
readOnly: isIndexTransforming(this.props.indexTransformationProgress),
|
||||
ariaLabel: t(Keys.controls.settings.indexingPolicy.ariaLabel),
|
||||
ariaLabel: "Indexing Policy",
|
||||
theme: monacoTheme(),
|
||||
});
|
||||
if (this.indexingPolicyEditor) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { MessageBar, MessageBarType } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
|
||||
import { Keys, t } from "Localization";
|
||||
import {
|
||||
mongoIndexTransformationRefreshingMessage,
|
||||
renderMongoIndexTransformationRefreshMessage,
|
||||
@@ -47,11 +46,7 @@ export class IndexingPolicyRefreshComponent extends React.Component<
|
||||
try {
|
||||
await this.props.refreshIndexTransformationProgress();
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
"RefreshIndexTransformationProgress",
|
||||
t(Keys.controls.settings.indexingPolicyRefresh.refreshFailed),
|
||||
);
|
||||
handleError(error, "RefreshIndexTransformationProgress", "Refreshing index transformation progress failed");
|
||||
} finally {
|
||||
this.setState({ isRefreshing: false });
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
|
||||
You can make more indexing changes once the current index transformation has completed. It is 90% complete.
|
||||
<StyledLinkBase
|
||||
onClick={[Function]}
|
||||
>
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
IDropdownOption,
|
||||
ITextField,
|
||||
} from "@fluentui/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import {
|
||||
addMongoIndexSubElementsTokens,
|
||||
mongoErrorMessageStyles,
|
||||
@@ -67,7 +66,7 @@ export class AddMongoIndexComponent extends React.Component<AddMongoIndexCompone
|
||||
<Stack {...mongoWarningStackProps}>
|
||||
<Stack horizontal tokens={addMongoIndexSubElementsTokens}>
|
||||
<TextField
|
||||
ariaLabel={t(Keys.controls.settings.mongoIndexing.indexFieldName) + " " + this.props.position}
|
||||
ariaLabel={"Index Field Name " + this.props.position}
|
||||
disabled={this.props.disabled}
|
||||
styles={shortWidthTextFieldStyles}
|
||||
componentRef={this.setRef}
|
||||
@@ -77,17 +76,17 @@ export class AddMongoIndexComponent extends React.Component<AddMongoIndexCompone
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
ariaLabel={t(Keys.controls.settings.mongoIndexing.indexType) + " " + this.props.position}
|
||||
ariaLabel={"Index Type " + this.props.position}
|
||||
disabled={this.props.disabled}
|
||||
styles={shortWidthDropDownStyles}
|
||||
placeholder={t(Keys.controls.settings.mongoIndexing.selectIndexType)}
|
||||
placeholder="Select an index type"
|
||||
selectedKey={this.props.type}
|
||||
options={this.indexTypes}
|
||||
onChange={this.onTypeChange}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
ariaLabel={t(Keys.controls.settings.mongoIndexing.undoButton) + " " + this.props.position}
|
||||
ariaLabel={"Undo Button " + this.props.position}
|
||||
iconProps={{ iconName: "Undo" }}
|
||||
disabled={!this.props.description && !this.props.type}
|
||||
onClick={() => this.props.onDiscard()}
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { MongoIndex } from "../../../../../Utils/arm/generatedClients/cosmos/types";
|
||||
import { Keys, t } from "Localization";
|
||||
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import {
|
||||
addMongoIndexStackProps,
|
||||
@@ -84,25 +83,11 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
};
|
||||
|
||||
private initialIndexesColumns: IColumn[] = [
|
||||
{
|
||||
key: "definition",
|
||||
name: t(Keys.controls.settings.mongoIndexing.definitionColumn),
|
||||
fieldName: "definition",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
},
|
||||
{
|
||||
key: "type",
|
||||
name: t(Keys.controls.settings.mongoIndexing.typeColumn),
|
||||
fieldName: "type",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
},
|
||||
{ key: "definition", name: "Definition", fieldName: "definition", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "type", name: "Type", fieldName: "type", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{
|
||||
key: "actionButton",
|
||||
name: t(Keys.controls.settings.mongoIndexing.dropIndexColumn),
|
||||
name: "Drop Index",
|
||||
fieldName: "actionButton",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
@@ -111,25 +96,11 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
];
|
||||
|
||||
private indexesToBeDroppedColumns: IColumn[] = [
|
||||
{
|
||||
key: "definition",
|
||||
name: t(Keys.controls.settings.mongoIndexing.definitionColumn),
|
||||
fieldName: "definition",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
},
|
||||
{
|
||||
key: "type",
|
||||
name: t(Keys.controls.settings.mongoIndexing.typeColumn),
|
||||
fieldName: "type",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
},
|
||||
{ key: "definition", name: "Definition", fieldName: "definition", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "type", name: "Type", fieldName: "type", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{
|
||||
key: "actionButton",
|
||||
name: t(Keys.controls.settings.mongoIndexing.addIndexBackColumn),
|
||||
name: "Add index back",
|
||||
fieldName: "actionButton",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
@@ -190,7 +161,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
private getActionButton = (arrayPosition: number, isCurrentIndex: boolean): JSX.Element => {
|
||||
return isCurrentIndex ? (
|
||||
<IconButton
|
||||
ariaLabel={t(Keys.controls.settings.mongoIndexing.deleteIndexButton)}
|
||||
ariaLabel="Delete index Button"
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
disabled={isIndexTransforming(this.props.indexTransformationProgress)}
|
||||
onClick={() => {
|
||||
@@ -199,7 +170,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
ariaLabel={t(Keys.controls.settings.mongoIndexing.addBackIndexButton)}
|
||||
ariaLabel="Add back Index Button"
|
||||
iconProps={{ iconName: "Add" }}
|
||||
onClick={() => {
|
||||
this.props.onRevertIndexDrop(arrayPosition);
|
||||
@@ -287,10 +258,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
|
||||
return (
|
||||
<Stack {...createAndAddMongoIndexStackProps} styles={mediumWidthStackStyles}>
|
||||
<CollapsibleSectionComponent
|
||||
title={t(Keys.controls.settings.mongoIndexing.currentIndexes)}
|
||||
isExpandedByDefault={true}
|
||||
>
|
||||
<CollapsibleSectionComponent title="Current index(es)" isExpandedByDefault={true}>
|
||||
{
|
||||
<>
|
||||
<DetailsList
|
||||
@@ -317,10 +285,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
|
||||
return (
|
||||
<Stack styles={mediumWidthStackStyles}>
|
||||
<CollapsibleSectionComponent
|
||||
title={t(Keys.controls.settings.mongoIndexing.indexesToBeDropped)}
|
||||
isExpandedByDefault={true}
|
||||
>
|
||||
<CollapsibleSectionComponent title="Index(es) to be dropped" isExpandedByDefault={true}>
|
||||
{indexesToBeDropped.length > 0 && (
|
||||
<DetailsList
|
||||
styles={customDetailsListStyles}
|
||||
|
||||
@@ -18,7 +18,6 @@ import { cancelDataTransferJob, pollDataTransferJob } from "Common/dataAccess/da
|
||||
import { Platform, configContext } from "ConfigContext";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { ChangePartitionKeyPane } from "Explorer/Panes/ChangePartitionKeyPane/ChangePartitionKeyPane";
|
||||
import { Keys, t } from "Localization";
|
||||
import {
|
||||
CosmosSqlDataTransferDataSourceSink,
|
||||
DataTransferJobGetResults,
|
||||
@@ -81,7 +80,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
||||
return (collection.partitionKeyProperties || []).map((property) => "/" + property).join(", ");
|
||||
};
|
||||
|
||||
const partitionKeyName = t(Keys.controls.settings.partitionKey.partitionKey);
|
||||
const partitionKeyName = "Partition key";
|
||||
const partitionKeyValue = getPartitionKeyValue();
|
||||
|
||||
const textHeadingStyle = {
|
||||
@@ -149,28 +148,22 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
||||
const getProgressDescription = (): string => {
|
||||
const processedCount = portalDataTransferJob?.properties?.processedCount;
|
||||
const totalCount = portalDataTransferJob?.properties?.totalCount;
|
||||
const processedCountString =
|
||||
totalCount > 0
|
||||
? t(Keys.controls.settings.partitionKeyEditor.documentsProcessed, {
|
||||
processedCount: String(processedCount),
|
||||
totalCount: String(totalCount),
|
||||
})
|
||||
: "";
|
||||
const processedCountString = totalCount > 0 ? `(${processedCount} of ${totalCount} documents processed)` : "";
|
||||
return `${portalDataTransferJob?.properties?.status} ${processedCountString}`;
|
||||
};
|
||||
|
||||
const startPartitionkeyChangeWorkflow = () => {
|
||||
useSidePanel.getState().openSidePanel(
|
||||
t(Keys.controls.settings.partitionKeyEditor.changePartitionKey, {
|
||||
partitionKeyName: t(Keys.controls.settings.partitionKey.partitionKey).toLowerCase(),
|
||||
}),
|
||||
<ChangePartitionKeyPane
|
||||
sourceDatabase={database}
|
||||
sourceCollection={collection}
|
||||
explorer={explorer}
|
||||
onClose={refreshDataTransferOperations}
|
||||
/>,
|
||||
);
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Change partition key",
|
||||
<ChangePartitionKeyPane
|
||||
sourceDatabase={database}
|
||||
sourceCollection={collection}
|
||||
explorer={explorer}
|
||||
onClose={refreshDataTransferOperations}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
const getPercentageComplete = () => {
|
||||
@@ -188,28 +181,16 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 20 }} styles={{ root: { maxWidth: 600 } }}>
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
{!isReadOnly && (
|
||||
<Text styles={textHeadingStyle}>
|
||||
{t(Keys.controls.settings.partitionKeyEditor.changePartitionKey, {
|
||||
partitionKeyName: partitionKeyName.toLowerCase(),
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
{!isReadOnly && <Text styles={textHeadingStyle}>Change {partitionKeyName.toLowerCase()}</Text>}
|
||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
||||
<Stack tokens={{ childrenGap: 5 }}>
|
||||
<Text styles={textSubHeadingStyle}>
|
||||
{t(Keys.controls.settings.partitionKeyEditor.currentPartitionKey, {
|
||||
partitionKeyName: partitionKeyName.toLowerCase(),
|
||||
})}
|
||||
</Text>
|
||||
<Text styles={textSubHeadingStyle}>{t(Keys.controls.settings.partitionKeyEditor.partitioning)}</Text>
|
||||
<Text styles={textSubHeadingStyle}>Current {partitionKeyName.toLowerCase()}</Text>
|
||||
<Text styles={textSubHeadingStyle}>Partitioning</Text>
|
||||
</Stack>
|
||||
<Stack tokens={{ childrenGap: 5 }} data-test="partition-key-values">
|
||||
<Text styles={textSubHeadingStyle1}>{partitionKeyValue}</Text>
|
||||
<Text styles={textSubHeadingStyle1}>
|
||||
{isHierarchicalPartitionedContainer()
|
||||
? t(Keys.controls.settings.partitionKeyEditor.hierarchical)
|
||||
: t(Keys.controls.settings.partitionKeyEditor.nonHierarchical)}
|
||||
{isHierarchicalPartitionedContainer() ? "Hierarchical" : "Non-hierarchical"}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -223,33 +204,33 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
||||
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||
styles={darkThemeMessageBarStyles}
|
||||
>
|
||||
{t(Keys.controls.settings.partitionKeyEditor.safeguardWarning)}
|
||||
To safeguard the integrity of the data being copied to the new container, ensure that no updates are made to
|
||||
the source container for the entire duration of the partition key change process.
|
||||
<Link
|
||||
href="https://learn.microsoft.com/azure/cosmos-db/container-copy#how-does-container-copy-work"
|
||||
target="_blank"
|
||||
underline
|
||||
style={{ color: "var(--colorBrandForeground1)" }}
|
||||
>
|
||||
{t(Keys.common.learnMore)}
|
||||
Learn more
|
||||
</Link>
|
||||
</MessageBar>
|
||||
<Text styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>
|
||||
{t(Keys.controls.settings.partitionKeyEditor.changeDescription)}
|
||||
To change the partition key, a new destination container must be created or an existing destination
|
||||
container selected. Data will then be copied to the destination container.
|
||||
</Text>
|
||||
{configContext.platform !== Platform.Emulator && (
|
||||
<PrimaryButton
|
||||
data-test="change-partition-key-button"
|
||||
styles={{ root: { width: "fit-content" } }}
|
||||
text={t(Keys.controls.settings.partitionKeyEditor.changeButton)}
|
||||
text="Change"
|
||||
onClick={startPartitionkeyChangeWorkflow}
|
||||
disabled={isCurrentJobInProgress(portalDataTransferJob)}
|
||||
/>
|
||||
)}
|
||||
{portalDataTransferJob && (
|
||||
<Stack>
|
||||
<Text styles={textHeadingStyle}>
|
||||
{t(Keys.controls.settings.partitionKeyEditor.changeJob, { partitionKeyName })}
|
||||
</Text>
|
||||
<Text styles={textHeadingStyle}>{partitionKeyName} change job</Text>
|
||||
<Stack
|
||||
horizontal
|
||||
tokens={{ childrenGap: 20 }}
|
||||
@@ -270,10 +251,7 @@ export const PartitionKeyComponent: React.FC<PartitionKeyComponentProps> = ({
|
||||
}}
|
||||
></ProgressIndicator>
|
||||
{isCurrentJobInProgress(portalDataTransferJob) && (
|
||||
<DefaultButton
|
||||
text={t(Keys.controls.settings.partitionKeyEditor.cancelButton)}
|
||||
onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)}
|
||||
/>
|
||||
<DefaultButton text="Cancel" onClick={() => cancelRunningDataTransferJob(portalDataTransferJob)} />
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Link, MessageBar, MessageBarType, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import * as React from "react";
|
||||
import * as Constants from "../../../../Common/Constants";
|
||||
import { Platform, configContext } from "../../../../ConfigContext";
|
||||
@@ -93,10 +92,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
}
|
||||
|
||||
const minThroughput: string = this.getMinRUs().toLocaleString();
|
||||
const maxThroughput: string = !this.props.isFixedContainer
|
||||
? t(Keys.controls.settings.scale.unlimited)
|
||||
: this.getMaxRUs().toLocaleString();
|
||||
return t(Keys.controls.settings.scale.throughputRangeLabel, { min: minThroughput, max: maxThroughput });
|
||||
const maxThroughput: string = !this.props.isFixedContainer ? "unlimited" : this.getMaxRUs().toLocaleString();
|
||||
return `Throughput (${minThroughput} - ${maxThroughput} RU/s)`;
|
||||
};
|
||||
|
||||
public canThroughputExceedMaximumValue = (): boolean => {
|
||||
@@ -159,12 +156,14 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
const freeTierLimits = SharedConstants.FreeTierLimits;
|
||||
return (
|
||||
<Text>
|
||||
{t(Keys.controls.settings.scale.freeTierInfo, { ru: freeTierLimits.RU, storage: freeTierLimits.Storage })}
|
||||
With free tier, you will get the first {freeTierLimits.RU} RU/s and {freeTierLimits.Storage} GB of storage in
|
||||
this account for free. To keep your account free, keep the total RU/s across all resources in the account to{" "}
|
||||
{freeTierLimits.RU} RU/s.
|
||||
<Link
|
||||
href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts"
|
||||
target="_blank"
|
||||
>
|
||||
{t(Keys.controls.settings.scale.freeTierLearnMore)}
|
||||
Learn more.
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
@@ -189,9 +188,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
{/* TODO: Replace link with call to the Azure Support blade */}
|
||||
{this.isAutoScaleEnabled() && (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<Text>{t(Keys.controls.settings.scale.throughputRuS)}</Text>
|
||||
<Text>Throughput (RU/s)</Text>
|
||||
<TextField disabled styles={getTextFieldStyles(undefined, undefined)} />
|
||||
<Text>{t(Keys.controls.settings.scale.autoScaleCustomSettings)}</Text>
|
||||
<Text>
|
||||
Your account has custom settings that prevents setting throughput at the container level. Please work with
|
||||
your Cosmos DB engineering team point of contact to make changes.
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import { Keys, t } from "Localization";
|
||||
import { userContext } from "../../../../UserContext";
|
||||
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
import {
|
||||
@@ -86,12 +85,9 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
constructor(props: SubSettingsComponentProps) {
|
||||
super(props);
|
||||
this.geospatialVisible = userContext.apiType === "SQL";
|
||||
this.partitionKeyName =
|
||||
userContext.apiType === "Mongo"
|
||||
? t(Keys.controls.settings.partitionKey.shardKey)
|
||||
: t(Keys.controls.settings.partitionKey.partitionKey);
|
||||
this.partitionKeyName = userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||
this.partitionKeyValue = this.getPartitionKeyValue();
|
||||
this.uniqueKeyName = t(Keys.controls.settings.subSettings.uniqueKeys);
|
||||
this.uniqueKeyName = "Unique keys";
|
||||
this.uniqueKeyValue = this.getUniqueKeyValue();
|
||||
}
|
||||
|
||||
@@ -147,13 +143,9 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
};
|
||||
|
||||
private ttlChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: TtlType.Off, text: t(Keys.controls.settings.subSettings.ttlOff), ariaLabel: "ttl-off-option" },
|
||||
{
|
||||
key: TtlType.OnNoDefault,
|
||||
text: t(Keys.controls.settings.subSettings.ttlOnNoDefault),
|
||||
ariaLabel: "ttl-on-no-default-option",
|
||||
},
|
||||
{ key: TtlType.On, text: t(Keys.controls.settings.subSettings.ttlOn), ariaLabel: "ttl-on-option" },
|
||||
{ key: TtlType.Off, text: "Off", ariaLabel: "ttl-off-option" },
|
||||
{ key: TtlType.OnNoDefault, text: "On (no default)", ariaLabel: "ttl-on-no-default-option" },
|
||||
{ key: TtlType.On, text: "On", ariaLabel: "ttl-on-option" },
|
||||
];
|
||||
|
||||
public getTtlValue = (value: string): TtlType => {
|
||||
@@ -224,13 +216,13 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.controls.settings.subSettings.mongoTtlMessage)}{" "}
|
||||
To enable time-to-live (TTL) for your collection/documents,{" "}
|
||||
<Link
|
||||
href="https://docs.microsoft.com/en-us/azure/cosmos-db/mongodb-time-to-live"
|
||||
target="_blank"
|
||||
style={{ color: "var(--colorBrandForeground1)" }}
|
||||
>
|
||||
{t(Keys.controls.settings.subSettings.mongoTtlLinkText)}
|
||||
create a TTL index
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
@@ -239,7 +231,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<ChoiceGroup
|
||||
id="timeToLive"
|
||||
label={t(Keys.controls.settings.subSettings.timeToLive)}
|
||||
label="Time to Live"
|
||||
selectedKey={this.props.timeToLive}
|
||||
options={this.ttlChoiceGroupOptions}
|
||||
onChange={this.onTtlChange}
|
||||
@@ -263,8 +255,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
max={Int32.Max}
|
||||
value={this.props.displayedTtlSeconds}
|
||||
onChange={this.onTimeToLiveSecondsChange}
|
||||
suffix={t(Keys.controls.settings.subSettings.seconds)}
|
||||
ariaLabel={t(Keys.controls.settings.subSettings.timeToLiveInSeconds)}
|
||||
suffix="second(s)"
|
||||
ariaLabel={`Time to live in seconds`}
|
||||
data-test="ttl-input"
|
||||
/>
|
||||
)}
|
||||
@@ -272,16 +264,16 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
);
|
||||
|
||||
private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: TtlType.Off, text: t(Keys.controls.settings.subSettings.ttlOff), disabled: true },
|
||||
{ key: TtlType.OnNoDefault, text: t(Keys.controls.settings.subSettings.ttlOnNoDefault) },
|
||||
{ key: TtlType.On, text: t(Keys.controls.settings.subSettings.ttlOn) },
|
||||
{ key: TtlType.Off, text: "Off", disabled: true },
|
||||
{ key: TtlType.OnNoDefault, text: "On (no default)" },
|
||||
{ key: TtlType.On, text: "On" },
|
||||
];
|
||||
|
||||
private getAnalyticalStorageTtlComponent = (): JSX.Element => (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<ChoiceGroup
|
||||
id="analyticalStorageTimeToLive"
|
||||
label={t(Keys.controls.settings.subSettings.analyticalStorageTtl)}
|
||||
label="Analytical Storage Time to Live"
|
||||
selectedKey={this.props.analyticalStorageTtlSelection}
|
||||
options={this.analyticalTtlChoiceGroupOptions}
|
||||
onChange={this.onAnalyticalStorageTtlSelectionChange}
|
||||
@@ -302,7 +294,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
min={1}
|
||||
max={Int32.Max}
|
||||
value={this.props.analyticalStorageTtlSeconds?.toString()}
|
||||
suffix={t(Keys.controls.settings.subSettings.seconds)}
|
||||
suffix="second(s)"
|
||||
onChange={this.onAnalyticalStorageTtlSecondsChange}
|
||||
/>
|
||||
)}
|
||||
@@ -310,22 +302,14 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
);
|
||||
|
||||
private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{
|
||||
key: GeospatialConfigType.Geography,
|
||||
text: t(Keys.controls.settings.subSettings.geography),
|
||||
ariaLabel: "geography-option",
|
||||
},
|
||||
{
|
||||
key: GeospatialConfigType.Geometry,
|
||||
text: t(Keys.controls.settings.subSettings.geometry),
|
||||
ariaLabel: "geometry-option",
|
||||
},
|
||||
{ key: GeospatialConfigType.Geography, text: "Geography", ariaLabel: "geography-option" },
|
||||
{ key: GeospatialConfigType.Geometry, text: "Geometry", ariaLabel: "geometry-option" },
|
||||
];
|
||||
|
||||
private getGeoSpatialComponent = (): JSX.Element => (
|
||||
<ChoiceGroup
|
||||
id="geoSpatialConfig"
|
||||
label={t(Keys.controls.settings.subSettings.geospatialConfiguration)}
|
||||
label="Geospatial Configuration"
|
||||
selectedKey={this.props.geospatialConfigType}
|
||||
options={this.geoSpatialConfigTypeChoiceGroupOptions}
|
||||
onChange={this.onGeoSpatialConfigTypeChange}
|
||||
@@ -334,8 +318,8 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
);
|
||||
|
||||
private changeFeedChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: ChangeFeedPolicyState.Off, text: t(Keys.controls.settings.subSettings.ttlOff) },
|
||||
{ key: ChangeFeedPolicyState.On, text: t(Keys.controls.settings.subSettings.ttlOn) },
|
||||
{ key: ChangeFeedPolicyState.Off, text: "Off" },
|
||||
{ key: ChangeFeedPolicyState.On, text: "On" },
|
||||
];
|
||||
|
||||
private getChangeFeedComponent = (): JSX.Element => {
|
||||
@@ -344,10 +328,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
return (
|
||||
<Stack>
|
||||
<Label id={labelId}>
|
||||
<ToolTipLabelComponent
|
||||
label={t(Keys.controls.settings.changeFeed.label)}
|
||||
toolTipElement={changeFeedPolicyToolTip}
|
||||
/>
|
||||
<ToolTipLabelComponent label="Change feed log retention policy" toolTipElement={changeFeedPolicyToolTip} />
|
||||
</Label>
|
||||
<ChoiceGroup
|
||||
id="changeFeedPolicy"
|
||||
@@ -373,10 +354,9 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
{this.getPartitionKeyVisible() && (
|
||||
<TooltipHost
|
||||
content={t(Keys.controls.settings.subSettings.partitionKeyTooltipTemplate, {
|
||||
partitionKeyName: this.partitionKeyName.toLowerCase(),
|
||||
partitionKeyValue: this.partitionKeyValue,
|
||||
})}
|
||||
content={`This ${this.partitionKeyName.toLowerCase()} is used to distribute data across multiple partitions for scalability. The value "${
|
||||
this.partitionKeyValue
|
||||
}" determines how documents are partitioned.`}
|
||||
styles={{
|
||||
root: {
|
||||
display: "block",
|
||||
@@ -393,20 +373,14 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
)}
|
||||
|
||||
{userContext.apiType === "SQL" && this.isLargePartitionKeyEnabled() && (
|
||||
<Text className={classNames.hintText}>
|
||||
{t(Keys.controls.settings.subSettings.largePartitionKeyEnabled, {
|
||||
partitionKeyName: this.partitionKeyName.toLowerCase(),
|
||||
})}
|
||||
</Text>
|
||||
<Text className={classNames.hintText}>Large {this.partitionKeyName.toLowerCase()} has been enabled.</Text>
|
||||
)}
|
||||
|
||||
{userContext.apiType === "SQL" &&
|
||||
(this.isHierarchicalPartitionedContainer() ? (
|
||||
<Text className={classNames.hintText}>{t(Keys.controls.settings.subSettings.hierarchicalPartitioned)}</Text>
|
||||
<Text className={classNames.hintText}>Hierarchically partitioned container.</Text>
|
||||
) : (
|
||||
<Text className={classNames.hintText}>
|
||||
{t(Keys.controls.settings.subSettings.nonHierarchicalPartitioned)}
|
||||
</Text>
|
||||
<Text className={classNames.hintText}>Non-hierarchically partitioned container.</Text>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Label, Slider, Stack, TextField, Toggle } from "@fluentui/react";
|
||||
import { ThroughputBucket } from "Contracts/DataModels";
|
||||
import { Keys, t } from "Localization";
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { isDirty } from "../../SettingsUtils";
|
||||
|
||||
@@ -66,9 +65,7 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
||||
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: "m" }} styles={{ root: { width: "70%", maxWidth: 700 } }}>
|
||||
<Label styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>
|
||||
{t(Keys.controls.settings.throughputBuckets.label)}
|
||||
</Label>
|
||||
<Label styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>Throughput Buckets</Label>
|
||||
<Stack>
|
||||
{throughputBuckets?.map((bucket) => (
|
||||
<Stack key={bucket.id} horizontal tokens={{ childrenGap: 8 }} verticalAlign="center">
|
||||
@@ -79,9 +76,7 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
||||
value={bucket.maxThroughputPercentage}
|
||||
onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
|
||||
showValue={false}
|
||||
label={`${t(Keys.controls.settings.throughputBuckets.bucketLabel, { id: String(bucket.id) })}${
|
||||
bucket.id === 1 ? t(Keys.controls.settings.throughputBuckets.dataExplorerQueryBucket) : ""
|
||||
}`}
|
||||
label={`Bucket ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
|
||||
styles={{
|
||||
root: { flex: 2, maxWidth: 400 },
|
||||
titleLabel: {
|
||||
@@ -104,8 +99,8 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
||||
disabled={bucket.maxThroughputPercentage === 100}
|
||||
/>
|
||||
<Toggle
|
||||
onText={t(Keys.controls.settings.throughputBuckets.active)}
|
||||
offText={t(Keys.controls.settings.throughputBuckets.inactive)}
|
||||
onText="Active"
|
||||
offText="Inactive"
|
||||
checked={bucket.maxThroughputPercentage !== 100}
|
||||
onChange={(event, checked) => onToggle(bucket.id, checked)}
|
||||
styles={{
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
Text,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import { Keys, t } from "Localization";
|
||||
import React from "react";
|
||||
import * as DataModels from "../../../../../Contracts/DataModels";
|
||||
import * as SharedConstants from "../../../../../Shared/Constants";
|
||||
@@ -98,8 +97,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
private throughputInputMaxValue: number;
|
||||
private autoPilotInputMaxValue: number;
|
||||
private options: IChoiceGroupOption[] = [
|
||||
{ key: "true", text: t(Keys.controls.settings.throughputInput.autoscale) },
|
||||
{ key: "false", text: t(Keys.controls.settings.throughputInput.manual) },
|
||||
{ key: "true", text: "Autoscale" },
|
||||
{ key: "false", text: "Manual" },
|
||||
];
|
||||
|
||||
// Style constants for theme-aware colors and layout
|
||||
@@ -245,7 +244,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
return (
|
||||
<div>
|
||||
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
|
||||
{t(Keys.controls.settings.costEstimate.updatedCostPerMonth)}
|
||||
Updated cost per month
|
||||
</Text>
|
||||
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
|
||||
<Text
|
||||
@@ -254,8 +253,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
|
||||
}}
|
||||
>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)}{" "}
|
||||
{t(Keys.controls.settings.throughputInput.min)}
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)} min
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
@@ -263,8 +261,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
|
||||
}}
|
||||
>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}{" "}
|
||||
{t(Keys.controls.settings.throughputInput.max)}
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} max
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
@@ -277,7 +274,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
||||
{newThroughput && newThroughputCostElement()}
|
||||
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
|
||||
{t(Keys.controls.settings.costEstimate.currentCostPerMonth)}
|
||||
Current cost per month
|
||||
</Text>
|
||||
<Stack horizontal style={{ marginTop: 5, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
|
||||
<Text
|
||||
@@ -286,8 +283,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
|
||||
}}
|
||||
>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)}{" "}
|
||||
{t(Keys.controls.settings.throughputInput.min)}
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)} min
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
@@ -295,8 +291,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY,
|
||||
}}
|
||||
>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}{" "}
|
||||
{t(Keys.controls.settings.throughputInput.max)}
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} max
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -331,20 +326,17 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
return (
|
||||
<div>
|
||||
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
|
||||
{t(Keys.controls.settings.costEstimate.updatedCostPerMonth)}
|
||||
Updated cost per month
|
||||
</Text>
|
||||
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
|
||||
<Text style={this.settingsAndScaleStyle.root}>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}
|
||||
{t(Keys.controls.settings.costEstimate.perHour)}
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}/hr
|
||||
</Text>
|
||||
<Text style={this.settingsAndScaleStyle.root}>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}
|
||||
{t(Keys.controls.settings.costEstimate.perDay)}
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}/day
|
||||
</Text>
|
||||
<Text style={this.settingsAndScaleStyle.root}>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
|
||||
{t(Keys.controls.settings.costEstimate.perMonth)}
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}/mo
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
@@ -357,20 +349,17 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
||||
{newThroughput && newThroughputCostElement()}
|
||||
<Text style={{ fontWeight: 600, color: ThroughputInputAutoPilotV3Component.TEXT_COLOR_PRIMARY }}>
|
||||
{t(Keys.controls.settings.costEstimate.currentCostPerMonth)}
|
||||
Current cost per month
|
||||
</Text>
|
||||
<Stack horizontal style={{ marginTop: 5 }}>
|
||||
<Text style={this.settingsAndScaleStyle.root}>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}
|
||||
{t(Keys.controls.settings.costEstimate.perHour)}
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}/hr
|
||||
</Text>
|
||||
<Text style={this.settingsAndScaleStyle.root}>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}
|
||||
{t(Keys.controls.settings.costEstimate.perDay)}
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}/day
|
||||
</Text>
|
||||
<Text style={this.settingsAndScaleStyle.root}>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
|
||||
{t(Keys.controls.settings.costEstimate.perMonth)}
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}/mo
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -455,14 +444,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
this.setState({ spendAckChecked: checked });
|
||||
|
||||
private getStorageCapacityTitle = (): JSX.Element => {
|
||||
const capacity: string = this.props.isFixed
|
||||
? t(Keys.controls.settings.throughputInput.fixed)
|
||||
: t(Keys.controls.settings.throughputInput.unlimited);
|
||||
const capacity: string = this.props.isFixed ? "Fixed" : "Unlimited";
|
||||
return (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<Label style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.controls.settings.throughputInput.storageCapacity)}
|
||||
</Label>
|
||||
<Label style={{ color: "var(--colorNeutralForeground1)" }}>Storage capacity</Label>
|
||||
<Text style={{ color: "var(--colorNeutralForeground1)" }}>{capacity}</Text>
|
||||
</Stack>
|
||||
);
|
||||
@@ -558,6 +543,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
<span style={{ float: "left", transform: "translateX(-50%)" }}>
|
||||
{this.props.instantMaximumThroughput.toLocaleString(ThroughputInputAutoPilotV3Component.LOCALE_EN_US)}
|
||||
</span>
|
||||
<span style={{ float: "right" }}>
|
||||
{this.props.softAllowedMaximumThroughput.toLocaleString(ThroughputInputAutoPilotV3Component.LOCALE_EN_US)}
|
||||
</span>
|
||||
<span style={{ float: "right" }} data-test="soft-allowed-maximum-throughput">
|
||||
{this.props.softAllowedMaximumThroughput.toLocaleString(ThroughputInputAutoPilotV3Component.LOCALE_EN_US)}
|
||||
</span>
|
||||
@@ -570,14 +558,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
/>
|
||||
<Stack horizontal>
|
||||
<Stack.Item style={{ width: "34%", paddingRight: "5px" }}>
|
||||
<Separator styles={this.thoughputRangeSeparatorStyles}>
|
||||
{t(Keys.controls.settings.throughputInput.instant)}
|
||||
</Separator>
|
||||
<Separator styles={this.thoughputRangeSeparatorStyles}>Instant</Separator>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={{ width: "66%", paddingLeft: "5px" }}>
|
||||
<Separator styles={this.thoughputRangeSeparatorStyles}>
|
||||
{t(Keys.controls.settings.throughputInput.fourToSixHrs)}
|
||||
</Separator>
|
||||
<Separator styles={this.thoughputRangeSeparatorStyles}>4-6 hrs</Separator>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -657,7 +641,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
variant="small"
|
||||
style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }}
|
||||
>
|
||||
{t(Keys.controls.settings.throughputInput.minimumRuS)}
|
||||
Minimum RU/s
|
||||
</Text>
|
||||
<FontIcon iconName="Info" style={{ fontSize: 12, color: "var(--colorNeutralForeground2)" }} />
|
||||
</Stack>
|
||||
@@ -691,7 +675,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
color: "var(--colorNeutralForeground1)",
|
||||
}}
|
||||
>
|
||||
{t(Keys.controls.settings.throughputInput.x10Equals)}
|
||||
x 10 =
|
||||
</Text>
|
||||
|
||||
{/* Column 3: Maximum RU/s */}
|
||||
@@ -701,7 +685,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
variant="small"
|
||||
style={{ lineHeight: "20px", fontWeight: 600, color: "var(--colorNeutralForeground1)" }}
|
||||
>
|
||||
{t(Keys.controls.settings.throughputInput.maximumRuS)}
|
||||
Maximum RU/s
|
||||
</Text>
|
||||
<FontIcon iconName="Info" style={{ fontSize: 12, color: "var(--colorNeutralForeground2)" }} />
|
||||
</Stack>
|
||||
@@ -742,9 +726,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
onGetErrorMessage={(value: string) => {
|
||||
const sanitizedValue = getSanitizedInputValue(value);
|
||||
const errorMessage: string =
|
||||
sanitizedValue % 1000
|
||||
? t(Keys.controls.settings.throughput.throughputIncrementError)
|
||||
: this.props.throughputError;
|
||||
sanitizedValue % 1000 ? "Throughput value must be in increments of 1000" : this.props.throughputError;
|
||||
return <span data-test="autopilot-throughput-input-error">{errorMessage}</span>;
|
||||
}}
|
||||
validateOnLoad={false}
|
||||
@@ -790,9 +772,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
)}
|
||||
{this.props.isAutoPilotSelected ? (
|
||||
<Text style={{ marginTop: "40px", color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.controls.settings.throughputInput.autoscaleDescription, {
|
||||
resourceType: this.props.collectionName ? "container" : "database",
|
||||
})}{" "}
|
||||
Based on usage, your {this.props.collectionName ? "container" : "database"} throughput will scale from{" "}
|
||||
<b>
|
||||
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)} RU/s (10% of max RU/s) -{" "}
|
||||
{this.props.maxAutoPilotThroughput} RU/s
|
||||
@@ -807,19 +787,16 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
styles={this.darkThemeMessageBarStyles}
|
||||
style={{ marginTop: "40px" }}
|
||||
>
|
||||
{t(Keys.controls.settings.throughputInput.freeTierWarning, {
|
||||
ru: String(SharedConstants.FreeTierLimits.RU),
|
||||
})}
|
||||
{`Billing will apply if you provision more than ${SharedConstants.FreeTierLimits.RU} RU/s of manual throughput, or if the resource scales beyond ${SharedConstants.FreeTierLimits.RU} RU/s with autoscale.`}
|
||||
</MessageBar>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!this.overrideWithProvisionedThroughputSettings() && (
|
||||
<Text style={{ color: "var(--colorNeutralForeground1)" }}>
|
||||
{t(Keys.controls.settings.throughputInput.capacityCalculator)}
|
||||
Estimate your required RU/s with
|
||||
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
|
||||
{t(Keys.controls.settings.throughputInput.capacityCalculatorLink)}{" "}
|
||||
<FontIcon iconName="NavigateExternalInline" />
|
||||
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
@@ -832,7 +809,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
onChange={this.onSpendAckChecked}
|
||||
/>
|
||||
)}
|
||||
{this.props.isFixed && <p>{t(Keys.controls.settings.throughputInput.fixedStorageNote)}</p>}
|
||||
{this.props.isFixed && (
|
||||
<p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>
|
||||
)}
|
||||
{this.props.collectionName && (
|
||||
<Stack.Item style={{ marginTop: "40px" }}>{this.getStorageCapacityTitle()}</Stack.Item>
|
||||
)}
|
||||
|
||||
@@ -426,6 +426,15 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
>
|
||||
5,000
|
||||
</span>
|
||||
<span
|
||||
style={
|
||||
{
|
||||
"float": "right",
|
||||
}
|
||||
}
|
||||
>
|
||||
1,000,000
|
||||
</span>
|
||||
<span
|
||||
data-test="soft-allowed-maximum-throughput"
|
||||
style={
|
||||
@@ -552,7 +561,9 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
You are not able to lower throughput below your current minimum of 10000 RU/s. For more information on this limit, please refer to our service quote documentation.
|
||||
You are not able to lower throughput below your current minimum of
|
||||
10000
|
||||
RU/s. For more information on this limit, please refer to our service quote documentation.
|
||||
<StyledLinkBase
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
|
||||
target="_blank"
|
||||
@@ -570,7 +581,9 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
Based on usage, your container throughput will scale from
|
||||
Based on usage, your
|
||||
container
|
||||
throughput will scale from
|
||||
|
||||
<b>
|
||||
400
|
||||
@@ -679,8 +692,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
$
|
||||
|
||||
35.04
|
||||
|
||||
min
|
||||
min
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
@@ -693,8 +705,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
$
|
||||
|
||||
350.40
|
||||
|
||||
max
|
||||
max
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
@@ -728,8 +739,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
$
|
||||
|
||||
35.04
|
||||
|
||||
min
|
||||
min
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
@@ -742,8 +752,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
$
|
||||
|
||||
350.40
|
||||
|
||||
max
|
||||
max
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -1025,6 +1034,15 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
>
|
||||
5,000
|
||||
</span>
|
||||
<span
|
||||
style={
|
||||
{
|
||||
"float": "right",
|
||||
}
|
||||
}
|
||||
>
|
||||
1,000,000
|
||||
</span>
|
||||
<span
|
||||
data-test="soft-allowed-maximum-throughput"
|
||||
style={
|
||||
@@ -1151,7 +1169,9 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
You are not able to lower throughput below your current minimum of 10000 RU/s. For more information on this limit, please refer to our service quote documentation.
|
||||
You are not able to lower throughput below your current minimum of
|
||||
10000
|
||||
RU/s. For more information on this limit, please refer to our service quote documentation.
|
||||
<StyledLinkBase
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
|
||||
target="_blank"
|
||||
@@ -1600,6 +1620,15 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
>
|
||||
5,000
|
||||
</span>
|
||||
<span
|
||||
style={
|
||||
{
|
||||
"float": "right",
|
||||
}
|
||||
}
|
||||
>
|
||||
1,000,000
|
||||
</span>
|
||||
<span
|
||||
data-test="soft-allowed-maximum-throughput"
|
||||
style={
|
||||
@@ -1726,7 +1755,9 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
You are not able to lower throughput below your current minimum of 10000 RU/s. For more information on this limit, please refer to our service quote documentation.
|
||||
You are not able to lower throughput below your current minimum of
|
||||
10000
|
||||
RU/s. For more information on this limit, please refer to our service quote documentation.
|
||||
<StyledLinkBase
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
|
||||
target="_blank"
|
||||
|
||||
@@ -27,8 +27,7 @@ exports[`ComputedPropertiesComponent renders 1`] = `
|
||||
iconName="NavigateExternalInline"
|
||||
/>
|
||||
</StyledLinkBase>
|
||||
|
||||
about how to define computed properties and how to use them.
|
||||
about how to define computed properties and how to use them.
|
||||
</Text>
|
||||
<div
|
||||
className="settingsV2Editor"
|
||||
|
||||
@@ -33,7 +33,8 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
|
||||
}
|
||||
}
|
||||
>
|
||||
Change partition key
|
||||
Change
|
||||
partition key
|
||||
</Text>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
@@ -60,7 +61,8 @@ exports[`PartitionKeyComponent renders default component and matches snapshot 1`
|
||||
}
|
||||
}
|
||||
>
|
||||
Current partition key
|
||||
Current
|
||||
partition key
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
@@ -221,7 +223,8 @@ exports[`PartitionKeyComponent renders read-only component and matches snapshot
|
||||
}
|
||||
}
|
||||
>
|
||||
Current partition key
|
||||
Current
|
||||
partition key
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
|
||||
@@ -410,7 +410,9 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
<Text
|
||||
className="hintText-115"
|
||||
>
|
||||
Large partition key has been enabled.
|
||||
Large
|
||||
partition key
|
||||
has been enabled.
|
||||
</Text>
|
||||
<Text
|
||||
className="hintText-115"
|
||||
@@ -982,7 +984,9 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
<Text
|
||||
className="hintText-115"
|
||||
>
|
||||
Large partition key has been enabled.
|
||||
Large
|
||||
partition key
|
||||
has been enabled.
|
||||
</Text>
|
||||
<Text
|
||||
className="hintText-115"
|
||||
@@ -1518,7 +1522,9 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
|
||||
<Text
|
||||
className="hintText-115"
|
||||
>
|
||||
Large partition key has been enabled.
|
||||
Large
|
||||
partition key
|
||||
has been enabled.
|
||||
</Text>
|
||||
<Text
|
||||
className="hintText-115"
|
||||
@@ -2151,7 +2157,9 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
<Text
|
||||
className="hintText-115"
|
||||
>
|
||||
Large partition key has been enabled.
|
||||
Large
|
||||
partition key
|
||||
has been enabled.
|
||||
</Text>
|
||||
<Text
|
||||
className="hintText-115"
|
||||
@@ -2721,7 +2729,9 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
<Text
|
||||
className="hintText-115"
|
||||
>
|
||||
Large partition key has been enabled.
|
||||
Large
|
||||
partition key
|
||||
has been enabled.
|
||||
</Text>
|
||||
<Text
|
||||
className="hintText-115"
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Keys, t } from "Localization";
|
||||
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { isCapabilityEnabled } from "../../../Utils/CapabilityUtils";
|
||||
import { MongoIndex } from "../../../Utils/arm/generatedClients/cosmos/types";
|
||||
|
||||
const zeroValue = 0;
|
||||
@@ -91,19 +88,6 @@ export const hasDatabaseSharedThroughput = (collection: ViewModels.Collection):
|
||||
return database?.isDatabaseShared() && !collection.offer();
|
||||
};
|
||||
|
||||
export const isDataMaskingEnabled = (dataMaskingPolicy?: DataModels.DataMaskingPolicy): boolean => {
|
||||
const isSqlAccount = userContext.apiType === "SQL";
|
||||
if (!isSqlAccount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasDataMaskingCapability = isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking);
|
||||
const hasDataMaskingPolicyFromCollection =
|
||||
dataMaskingPolicy?.includedPaths?.length > 0 || dataMaskingPolicy?.excludedPaths?.length > 0;
|
||||
|
||||
return hasDataMaskingCapability || hasDataMaskingPolicyFromCollection;
|
||||
};
|
||||
|
||||
export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => {
|
||||
// Backend can contain different casing as it does case-insensitive comparisson
|
||||
if (!modeFromBackend) {
|
||||
@@ -176,27 +160,25 @@ const getStringValue = (value: isDirtyTypes, type: string): string => {
|
||||
export const getTabTitle = (tab: SettingsV2TabTypes): string => {
|
||||
switch (tab) {
|
||||
case SettingsV2TabTypes.ScaleTab:
|
||||
return t(Keys.controls.settings.tabTitles.scale);
|
||||
return "Scale";
|
||||
case SettingsV2TabTypes.ConflictResolutionTab:
|
||||
return t(Keys.controls.settings.tabTitles.conflictResolution);
|
||||
return "Conflict Resolution";
|
||||
case SettingsV2TabTypes.SubSettingsTab:
|
||||
return t(Keys.controls.settings.tabTitles.settings);
|
||||
return "Settings";
|
||||
case SettingsV2TabTypes.IndexingPolicyTab:
|
||||
return t(Keys.controls.settings.tabTitles.indexingPolicy);
|
||||
return "Indexing Policy";
|
||||
case SettingsV2TabTypes.PartitionKeyTab:
|
||||
return isFabricNative()
|
||||
? t(Keys.controls.settings.tabTitles.partitionKeys)
|
||||
: t(Keys.controls.settings.tabTitles.partitionKeysPreview);
|
||||
return isFabricNative() ? "Partition Keys" : "Partition Keys (preview)";
|
||||
case SettingsV2TabTypes.ComputedPropertiesTab:
|
||||
return t(Keys.controls.settings.tabTitles.computedProperties);
|
||||
return "Computed Properties";
|
||||
case SettingsV2TabTypes.ContainerVectorPolicyTab:
|
||||
return t(Keys.controls.settings.tabTitles.containerPolicies);
|
||||
return "Container Policies";
|
||||
case SettingsV2TabTypes.ThroughputBucketsTab:
|
||||
return t(Keys.controls.settings.tabTitles.throughputBuckets);
|
||||
return "Throughput Buckets";
|
||||
case SettingsV2TabTypes.GlobalSecondaryIndexTab:
|
||||
return t(Keys.controls.settings.tabTitles.globalSecondaryIndexPreview);
|
||||
return "Global Secondary Index (Preview)";
|
||||
case SettingsV2TabTypes.DataMaskingTab:
|
||||
return t(Keys.controls.settings.tabTitles.maskingPolicyPreview);
|
||||
return "Masking Policy (preview)";
|
||||
default:
|
||||
throw new Error(`Unknown tab ${tab}`);
|
||||
}
|
||||
@@ -206,19 +188,19 @@ export const getMongoNotification = (description: string, type: MongoIndexTypes)
|
||||
if (description && !type) {
|
||||
return {
|
||||
type: MongoNotificationType.Warning,
|
||||
message: t(Keys.controls.settings.mongoNotifications.selectTypeWarning),
|
||||
message: "Please select a type for each index.",
|
||||
};
|
||||
}
|
||||
|
||||
if (type && (!description || description.trim().length === 0)) {
|
||||
return {
|
||||
type: MongoNotificationType.Error,
|
||||
message: t(Keys.controls.settings.mongoNotifications.enterFieldNameError),
|
||||
message: "Please enter a field name.",
|
||||
};
|
||||
} else if (type === MongoIndexTypes.Wildcard && description?.indexOf("$**") === -1) {
|
||||
return {
|
||||
type: MongoNotificationType.Error,
|
||||
message: t(Keys.controls.settings.mongoNotifications.wildcardPathError) + MongoWildcardPlaceHolder,
|
||||
message: "Wildcard path is not present in the field name. Use a pattern like " + MongoWildcardPlaceHolder,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -252,29 +234,28 @@ export const isIndexTransforming = (indexTransformationProgress: number): boolea
|
||||
indexTransformationProgress !== undefined && indexTransformationProgress !== 100;
|
||||
|
||||
export const getPartitionKeyName = (apiType: string, isLowerCase?: boolean): string => {
|
||||
const partitionKeyName =
|
||||
apiType === "Mongo"
|
||||
? t(Keys.controls.settings.partitionKey.shardKey)
|
||||
: t(Keys.controls.settings.partitionKey.partitionKey);
|
||||
const partitionKeyName = apiType === "Mongo" ? "Shard key" : "Partition key";
|
||||
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
||||
};
|
||||
|
||||
export const getPartitionKeyTooltipText = (apiType: string): string => {
|
||||
if (apiType === "Mongo") {
|
||||
return t(Keys.controls.settings.partitionKey.shardKeyTooltip);
|
||||
return "The shard key (field) is used to split your data across many replica sets (shards) to achieve unlimited scalability. It’s critical to choose a field that will evenly distribute your data.";
|
||||
}
|
||||
let tooltipText = `The ${getPartitionKeyName(apiType, true)} ${t(
|
||||
Keys.controls.settings.partitionKey.partitionKeyTooltip,
|
||||
)}`;
|
||||
let tooltipText = `The ${getPartitionKeyName(
|
||||
apiType,
|
||||
true,
|
||||
)} is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume.`;
|
||||
if (apiType === "SQL") {
|
||||
tooltipText += t(Keys.controls.settings.partitionKey.sqlPartitionKeyTooltipSuffix);
|
||||
tooltipText += " For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice.";
|
||||
}
|
||||
return tooltipText;
|
||||
};
|
||||
|
||||
export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: string): string => {
|
||||
if (partitionKeyDefault && (apiType === "SQL" || apiType === "Mongo")) {
|
||||
return t(Keys.controls.settings.partitionKey.partitionKeySubtext);
|
||||
const subtext = "For small workloads, the item ID is a suitable choice for the partition key.";
|
||||
return subtext;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
@@ -282,18 +263,18 @@ export const getPartitionKeySubtext = (partitionKeyDefault: boolean, apiType: st
|
||||
export const getPartitionKeyPlaceHolder = (apiType: string, index?: number): string => {
|
||||
switch (apiType) {
|
||||
case "Mongo":
|
||||
return t(Keys.controls.settings.partitionKey.mongoPlaceholder);
|
||||
return "e.g., categoryId";
|
||||
case "Gremlin":
|
||||
return t(Keys.controls.settings.partitionKey.gremlinPlaceholder);
|
||||
return "e.g., /address";
|
||||
case "SQL":
|
||||
return `${
|
||||
index === undefined
|
||||
? t(Keys.controls.settings.partitionKey.sqlFirstPartitionKey)
|
||||
? "Required - first partition key e.g., /TenantId"
|
||||
: index === 0
|
||||
? t(Keys.controls.settings.partitionKey.sqlSecondPartitionKey)
|
||||
: t(Keys.controls.settings.partitionKey.sqlThirdPartitionKey)
|
||||
? "second partition key e.g., /UserId"
|
||||
: "third partition key e.g., /SessionId"
|
||||
}`;
|
||||
default:
|
||||
return t(Keys.controls.settings.partitionKey.defaultPlaceholder);
|
||||
return "e.g., /address/zipCode";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -68,6 +68,7 @@ export const collection = {
|
||||
dataMaskingPolicy: ko.observable<DataModels.DataMaskingPolicy>({
|
||||
includedPaths: [],
|
||||
excludedPaths: ["/excludedPath"],
|
||||
isPolicyEnabled: true,
|
||||
}),
|
||||
readSettings: () => {
|
||||
return;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user