mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 02:41:39 +00:00
Compare commits
55 Commits
users/aisa
...
user/bchou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
695729a8b6 | ||
|
|
a5c3e6bea0 | ||
|
|
76e63818d3 | ||
|
|
cfb5db4df6 | ||
|
|
922ca5c523 | ||
|
|
bafe002fa3 | ||
|
|
0817acf404 | ||
|
|
8e2c46301d | ||
|
|
012d043c78 | ||
|
|
3afd74a957 | ||
|
|
0ef4399ba4 | ||
|
|
870863a723 | ||
|
|
e3815734db | ||
|
|
5ea78f9abf | ||
|
|
8a56214ec2 | ||
|
|
e3ae006100 | ||
|
|
589b61afaf | ||
|
|
eb3f6bc93f | ||
|
|
6ec909a97b | ||
|
|
08a51ca6b1 | ||
|
|
30a3b5c7a4 | ||
|
|
f370507a27 | ||
|
|
e0edaf405c | ||
|
|
f8231600d6 | ||
|
|
45c8d70c77 | ||
|
|
70d7ee755b | ||
|
|
0a4aed4f47 | ||
|
|
a7d007e0dd | ||
|
|
5f4a4e5c4c | ||
|
|
1b64827c24 | ||
|
|
a6ae784a45 | ||
|
|
7458107efd | ||
|
|
64533b445f | ||
|
|
d7bdd0032e | ||
|
|
372ac6921f | ||
|
|
c6eda097fc | ||
|
|
05d02f08fa | ||
|
|
ab4f02f74a | ||
|
|
0fc6647627 | ||
|
|
c5ed537109 | ||
|
|
db322ccb59 | ||
|
|
2d7631c358 | ||
|
|
e401c88df6 | ||
|
|
f14b574527 | ||
|
|
45513e5e1b | ||
|
|
15154dfd6a | ||
|
|
7aeb682bea | ||
|
|
35051bace5 | ||
|
|
5fc53a7f89 | ||
|
|
ed83bf47e4 | ||
|
|
d657c4919e | ||
|
|
95d33356c3 | ||
|
|
1081432bbd | ||
|
|
44d815454c | ||
|
|
6d604490d3 |
@@ -23,8 +23,6 @@ src/Common/MongoUtility.ts
|
||||
src/Common/NotificationsClientBase.ts
|
||||
src/Common/QueriesClient.ts
|
||||
src/Common/Splitter.ts
|
||||
src/Controls/Heatmap/Heatmap.test.ts
|
||||
src/Controls/Heatmap/Heatmap.ts
|
||||
src/Definitions/datatables.d.ts
|
||||
src/Definitions/gif.d.ts
|
||||
src/Definitions/globals.d.ts
|
||||
|
||||
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@@ -177,9 +177,27 @@ jobs:
|
||||
- name: "Az CLI login"
|
||||
uses: Azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
# We can't use MSAL within playwright so we acquire tokens prior to running the tests
|
||||
- name: "Acquire RBAC tokens for test accounts"
|
||||
uses: azure/cli@v2
|
||||
with:
|
||||
azcliversion: latest
|
||||
inlineScript: |
|
||||
NOSQL_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$NOSQL_TESTACCOUNT_TOKEN"
|
||||
echo NOSQL_TESTACCOUNT_TOKEN=$NOSQL_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
|
||||
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
|
||||
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
|
||||
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
|
||||
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
|
||||
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
||||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
|
||||
- name: Upload blob report to GitHub Actions Artifacts
|
||||
|
||||
2
.github/workflows/cleanup.yml
vendored
2
.github/workflows/cleanup.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: "Az CLI login"
|
||||
uses: azure/login@v1
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
|
||||
2
.npmrc
2
.npmrc
@@ -1,4 +1,4 @@
|
||||
save-exact=true
|
||||
|
||||
# Ignore peer dependency conflicts
|
||||
force=true # TODO: Remove this when we update to React 17 or higher!
|
||||
force=true # TODO: Remove this when we update to React 17 or higher!
|
||||
|
||||
23
.vscode/settings.json
vendored
23
.vscode/settings.json
vendored
@@ -1,22 +1,6 @@
|
||||
// Place your settings in this file to overwrite default and user settings.
|
||||
{
|
||||
"files.exclude": {
|
||||
".vs": true,
|
||||
".vscode/**": true,
|
||||
"*.trx": true,
|
||||
"**/.DS_Store": true,
|
||||
"**/.git": true,
|
||||
"**/.hg": true,
|
||||
"**/.svn": true,
|
||||
"built/**": true,
|
||||
"coverage/**": true,
|
||||
"libs/**": true,
|
||||
"node_modules/**": true,
|
||||
"package-lock.json": true,
|
||||
"quickstart/**": true,
|
||||
"test/out/**": true,
|
||||
"workers/libs/**": true
|
||||
},
|
||||
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
@@ -24,5 +8,8 @@
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||
"isTerminalEnabled": true,
|
||||
"isPhoenixEnabled": true
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||
"isTerminalEnabled" : false,
|
||||
"isPhoenixEnabled" : false
|
||||
}
|
||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||
"isPhoenixEnabled": false
|
||||
}
|
||||
8
images/VisualStudio.svg
Normal file
8
images/VisualStudio.svg
Normal file
File diff suppressed because one or more lines are too long
@@ -2869,6 +2869,7 @@ a:link {
|
||||
z-index: 1000;
|
||||
overflow-y: auto;
|
||||
overflow-x: clip;
|
||||
min-height: fit-content;
|
||||
}
|
||||
|
||||
.uniqueIndexesContainer {
|
||||
|
||||
38098
package-lock.json
generated
38098
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
348
package.json
348
package.json
@@ -4,200 +4,270 @@
|
||||
"description": "Cosmos Explorer",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@azure/arm-cosmosdb": "9.1.0",
|
||||
"@azure/cosmos": "4.2.0-beta.1",
|
||||
"@azure/arm-cosmosdb": "16.3.0",
|
||||
"@azure/cosmos": "4.5.0",
|
||||
"@azure/cosmos-language-service": "0.0.5",
|
||||
"@azure/identity": "4.5.0",
|
||||
"@azure/msal-browser": "2.14.2",
|
||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||
"@fluentui/react": "8.119.0",
|
||||
"@fluentui/react-components": "9.54.2",
|
||||
"@jupyterlab/services": "6.0.2",
|
||||
"@jupyterlab/terminal": "3.0.3",
|
||||
"@microsoft/applicationinsights-web": "2.6.1",
|
||||
"@azure/identity": "4.10.1",
|
||||
"@azure/msal-browser": "4.24.0",
|
||||
"@babel/plugin-transform-class-properties": "^7.24.7",
|
||||
"@babel/plugin-proposal-decorators": "7.28.0",
|
||||
"@fluentui/react": "8.123.6",
|
||||
"@fluentui/react-components": "9.70.0",
|
||||
"@jupyterlab/services": "7.4.9",
|
||||
"@jupyterlab/terminal": "4.4.9",
|
||||
"@microsoft/applicationinsights-web": "3.3.10",
|
||||
"@nteract/commutable": "7.5.1",
|
||||
"@nteract/connected-components": "6.8.2",
|
||||
"@nteract/connected-components": "6.9.0",
|
||||
"@nteract/core": "15.1.9",
|
||||
"@nteract/data-explorer": "8.0.3",
|
||||
"@nteract/directory-listing": "2.0.6",
|
||||
"@nteract/dropdown-menu": "1.0.1",
|
||||
"@nteract/data-explorer": "8.2.12",
|
||||
"@nteract/directory-listing": "2.1.0",
|
||||
"@nteract/dropdown-menu": "1.1.9",
|
||||
"@nteract/editor": "10.1.12",
|
||||
"@nteract/fixtures": "2.3.0",
|
||||
"@nteract/fixtures": "2.3.19",
|
||||
"@nteract/iron-icons": "1.0.0",
|
||||
"@nteract/jupyter-widgets": "2.0.0",
|
||||
"@nteract/jupyter-widgets": "4.1.19",
|
||||
"@nteract/logos": "1.0.0",
|
||||
"@nteract/markdown": "4.6.0",
|
||||
"@nteract/markdown": "4.6.2",
|
||||
"@nteract/monaco-editor": "3.2.2",
|
||||
"@nteract/octicons": "2.0.0",
|
||||
"@nteract/outputs": "3.0.9",
|
||||
"@nteract/presentational-components": "3.0.7",
|
||||
"@nteract/stateful-components": "1.7.0",
|
||||
"@nteract/styles": "2.0.2",
|
||||
"@nteract/transform-geojson": "5.1.8",
|
||||
"@nteract/outputs": "5.1.14",
|
||||
"@nteract/presentational-components": "3.4.12",
|
||||
"@nteract/stateful-components": "1.7.15",
|
||||
"@nteract/styles": "2.2.11",
|
||||
"@nteract/transform-geojson": "5.1.13",
|
||||
"@nteract/transform-model-debug": "5.0.1",
|
||||
"@nteract/transform-plotly": "6.1.6",
|
||||
"@nteract/transform-vdom": "4.0.11",
|
||||
"@nteract/transform-plotly": "7.0.1",
|
||||
"@nteract/transform-vdom": "4.0.15",
|
||||
"@nteract/transform-vega": "7.0.6",
|
||||
"@octokit/rest": "17.9.2",
|
||||
"@octokit/rest": "21.1.1",
|
||||
"@phosphor/widgets": "1.9.3",
|
||||
"@testing-library/jest-dom": "6.4.6",
|
||||
"@types/lodash": "4.14.171",
|
||||
"@types/mkdirp": "1.0.1",
|
||||
"@types/node-fetch": "2.5.7",
|
||||
"@xmldom/xmldom": "0.7.13",
|
||||
"@testing-library/jest-dom": "6.8.0",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/mkdirp": "1.0.2",
|
||||
"@types/node-fetch": "2.6.13",
|
||||
"@xmldom/xmldom": "0.9.8",
|
||||
"@xterm/xterm": "5.5.0",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"allotment": "1.20.2",
|
||||
"applicationinsights": "1.8.0",
|
||||
"allotment": "1.20.4",
|
||||
"applicationinsights": "3.12.0",
|
||||
"bootstrap": "3.4.1",
|
||||
"canvas": "2.11.2",
|
||||
"canvas": "3.2.0",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
"clipboard-copy": "4.0.1",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"crossroads": "0.12.2",
|
||||
"css-element-queries": "1.1.1",
|
||||
"d3": "7.8.5",
|
||||
"datatables.net-colreorder-dt": "1.7.0",
|
||||
"datatables.net-dt": "1.13.8",
|
||||
"date-fns": "1.29.0",
|
||||
"dayjs": "1.8.19",
|
||||
"css-element-queries": "1.2.3",
|
||||
"d3": "7.9.0",
|
||||
"datatables.net-colreorder-dt": "2.1.1",
|
||||
"datatables.net-dt": "2.3.4",
|
||||
"date-fns": "4.1.0",
|
||||
"dayjs": "1.11.18",
|
||||
"dom-to-image": "2.6.0",
|
||||
"dotenv": "8.2.0",
|
||||
"eslint-plugin-jest": "27.4.2",
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
"dotenv": "17.2.3",
|
||||
"eslint-plugin-jest": "28.14.0",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"hasher": "1.2.0",
|
||||
"html2canvas": "1.0.0-rc.5",
|
||||
"i18next": "23.11.5",
|
||||
"i18next-browser-languagedetector": "6.0.1",
|
||||
"i18next-http-backend": "1.0.23",
|
||||
"iframe-resizer-react": "1.1.0",
|
||||
"immer": "9.0.6",
|
||||
"html2canvas": "1.4.1",
|
||||
"i18next": "25.5.2",
|
||||
"i18next-browser-languagedetector": "8.2.0",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"iframe-resizer-react": "5.1.5",
|
||||
"immer": "10.1.3",
|
||||
"immutable": "4.0.0-rc.12",
|
||||
"is-ci": "2.0.0",
|
||||
"is-ci": "4.1.0",
|
||||
"jquery": "3.7.1",
|
||||
"jquery-typeahead": "2.11.1",
|
||||
"jquery-ui-dist": "1.13.2",
|
||||
"jquery-ui-dist": "1.13.3",
|
||||
"knockout": "3.5.1",
|
||||
"loader-utils": "2.0.3",
|
||||
"mkdirp": "1.0.4",
|
||||
"monaco-editor": "0.44.0",
|
||||
"loader-utils": "3.3.1",
|
||||
"mkdirp": "3.0.1",
|
||||
"monaco-editor": "0.53.0",
|
||||
"ms": "2.1.3",
|
||||
"p-retry": "6.2.1",
|
||||
"patch-package": "8.0.0",
|
||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
||||
"patch-package": "8.0.1",
|
||||
"plotly.js-cartesian-dist-min": "3.1.1",
|
||||
"post-robot": "10.0.42",
|
||||
"q": "1.5.1",
|
||||
"react": "16.14.0",
|
||||
"react-animate-height": "2.0.8",
|
||||
"react-dnd": "14.0.2",
|
||||
"react-dnd-html5-backend": "14.0.0",
|
||||
"react-dom": "16.14.0",
|
||||
"q": "2.0.3",
|
||||
"react": "18.2.0",
|
||||
"react-animate-height": "3.2.3",
|
||||
"react-dnd": "16.0.1",
|
||||
"react-dnd-html5-backend": "16.0.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hotkeys": "2.0.0",
|
||||
"react-i18next": "14.1.2",
|
||||
"react-i18next": "16.0.0",
|
||||
"react-notification-system": "0.2.17",
|
||||
"react-redux": "7.1.3",
|
||||
"react-redux": "7.2.9",
|
||||
"react-splitter-layout": "4.0.0",
|
||||
"react-string-format": "1.0.1",
|
||||
"react-string-format": "1.2.0",
|
||||
"react-window": "1.8.10",
|
||||
"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",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"tinykeys": "2.1.0",
|
||||
"underscore": "1.12.1",
|
||||
"utility-types": "3.10.0",
|
||||
"zustand": "3.5.0"
|
||||
"react-youtube": "10.1.0",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"rx-jupyter": "5.5.21",
|
||||
"sanitize-html": "2.17.0",
|
||||
"shell-quote": "1.8.3",
|
||||
"styled-components": "6.1.19",
|
||||
"swr": "2.3.6",
|
||||
"terser-webpack-plugin": "5.3.14",
|
||||
"tinykeys": "3.0.0",
|
||||
"underscore": "1.13.7",
|
||||
"utility-types": "3.11.0",
|
||||
"zustand": "5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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.49.1",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@types/applicationinsights-js": "1.0.7",
|
||||
"@types/codemirror": "0.0.56",
|
||||
"@types/crossroads": "0.0.30",
|
||||
"@types/d3": "5.9.2",
|
||||
"@babel/core": "7.28.4",
|
||||
"@babel/preset-env": "7.28.3",
|
||||
"@babel/preset-react": "7.27.1",
|
||||
"@babel/preset-typescript": "7.27.1",
|
||||
"@playwright/test": "1.55.1",
|
||||
"@testing-library/react": "16.3.0",
|
||||
"@types/applicationinsights-js": "1.0.9",
|
||||
"@types/codemirror": "5.60.16",
|
||||
"@types/crossroads": "0.0.33",
|
||||
"@types/d3": "7.4.3",
|
||||
"@types/datatables.net": "1.10.28",
|
||||
"@types/datatables.net-colreorder": "1.4.5",
|
||||
"@types/dom-to-image": "2.6.2",
|
||||
"@types/enzyme": "3.10.12",
|
||||
"@types/dom-to-image": "2.6.7",
|
||||
"@types/enzyme": "3.10.19",
|
||||
"@types/enzyme-adapter-react-16": "1.0.9",
|
||||
"@types/hasher": "0.0.31",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/jquery": "3.5.29",
|
||||
"@types/node": "12.11.1",
|
||||
"@types/post-robot": "10.0.1",
|
||||
"@types/q": "1.5.1",
|
||||
"@types/react": "17.0.44",
|
||||
"@types/react-dom": "17.0.15",
|
||||
"@types/react-notification-system": "0.2.39",
|
||||
"@types/react-redux": "7.1.7",
|
||||
"@types/react-splitter-layout": "3.0.1",
|
||||
"@types/hasher": "0.0.35",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/jquery": "3.5.33",
|
||||
"@types/node": "24.6.0",
|
||||
"@types/post-robot": "10.0.6",
|
||||
"@types/q": "1.5.8",
|
||||
"@types/react": "18.3.7",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/react-notification-system": "0.2.46",
|
||||
"@types/react-redux": "7.1.34",
|
||||
"@types/react-splitter-layout": "4.0.0",
|
||||
"@types/react-window": "1.8.8",
|
||||
"@types/sanitize-html": "1.27.2",
|
||||
"@types/sinon": "2.3.3",
|
||||
"@types/styled-components": "5.1.1",
|
||||
"@types/underscore": "1.7.36",
|
||||
"@types/youtube-player": "5.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.4",
|
||||
"@typescript-eslint/parser": "6.7.4",
|
||||
"@webpack-cli/serve": "2.0.5",
|
||||
"babel-jest": "29.7.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"buffer": "5.1.0",
|
||||
"@types/sanitize-html": "2.16.0",
|
||||
"@types/sinon": "17.0.4",
|
||||
"@types/styled-components": "5.1.34",
|
||||
"@types/underscore": "1.13.0",
|
||||
"@types/youtube-player": "5.5.11",
|
||||
"@typescript-eslint/eslint-plugin": "8.45.0",
|
||||
"@typescript-eslint/parser": "8.45.0",
|
||||
"@webpack-cli/serve": "3.0.1",
|
||||
"babel-jest": "30.2.0",
|
||||
"babel-loader": "10.0.0",
|
||||
"buffer": "6.0.3",
|
||||
"case-sensitive-paths-webpack-plugin": "2.4.0",
|
||||
"create-file-webpack": "1.0.2",
|
||||
"css-loader": "6.8.1",
|
||||
"css-loader": "7.1.2",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.8",
|
||||
"enzyme-to-json": "3.6.2",
|
||||
"eslint": "8.50.0",
|
||||
"eslint": "9.36.0",
|
||||
"eslint-cli": "1.1.1",
|
||||
"eslint-plugin-no-null": "1.0.2",
|
||||
"eslint-plugin-prefer-arrow": "1.2.3",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"fast-glob": "3.2.5",
|
||||
"fs-extra": "7.0.0",
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"fast-glob": "3.3.3",
|
||||
"fs-extra": "11.3.2",
|
||||
"html-inline-css-webpack-plugin": "1.11.2",
|
||||
"html-loader": "5.0.0",
|
||||
"html-webpack-plugin": "5.5.3",
|
||||
"jest": "29.7.0",
|
||||
"html-loader": "5.1.0",
|
||||
"html-webpack-plugin": "5.6.4",
|
||||
"jest": "30.2.0",
|
||||
"jest-canvas-mock": "2.5.2",
|
||||
"jest-circus": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jest-circus": "30.2.0",
|
||||
"jest-environment-jsdom": "30.2.0",
|
||||
"jest-html-loader": "1.0.0",
|
||||
"jest-react-hooks-shallow": "1.5.1",
|
||||
"jest-trx-results-processor": "3.0.2",
|
||||
"less": "3.8.1",
|
||||
"less-loader": "11.1.3",
|
||||
"less": "4.4.1",
|
||||
"less-loader": "12.3.0",
|
||||
"less-vars-loader": "1.1.0",
|
||||
"mini-css-extract-plugin": "2.1.0",
|
||||
"mini-css-extract-plugin": "2.9.4",
|
||||
"monaco-editor-webpack-plugin": "7.1.0",
|
||||
"node-fetch": "2.6.7",
|
||||
"prettier": "3.0.3",
|
||||
"node-fetch": "3.3.2",
|
||||
"prettier": "3.6.2",
|
||||
"process": "0.11.10",
|
||||
"querystring-es3": "0.2.1",
|
||||
"raw-loader": "0.5.1",
|
||||
"raw-loader": "4.0.2",
|
||||
"react-dev-utils": "12.0.1",
|
||||
"rimraf": "3.0.0",
|
||||
"sinon": "3.2.1",
|
||||
"style-loader": "0.23.0",
|
||||
"ts-loader": "9.2.4",
|
||||
"typedoc": "0.26.2",
|
||||
"typescript": "4.9.5",
|
||||
"rimraf": "5.0.10",
|
||||
"sinon": "21.0.0",
|
||||
"style-loader": "4.0.0",
|
||||
"ts-loader": "9.5.4",
|
||||
"typedoc": "0.28.13",
|
||||
"typescript": "5.9.2",
|
||||
"url-loader": "4.1.1",
|
||||
"wait-on": "4.0.2",
|
||||
"webpack": "5.88.2",
|
||||
"webpack-bundle-analyzer": "4.9.1",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "4.15.2"
|
||||
"wait-on": "8.0.5",
|
||||
"webpack": "5.102.0",
|
||||
"webpack-bundle-analyzer": "4.10.2",
|
||||
"webpack-cli": "6.0.1",
|
||||
"webpack-dev-server": "5.2.2"
|
||||
},
|
||||
"overrides": {
|
||||
"@nteract/connected-components": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"@nteract/core": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"@nteract/data-explorer": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"@nteract/dropdown-menu": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"@nteract/editor": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"@nteract/iron-icons": {
|
||||
"react": "18.2.0"
|
||||
},
|
||||
"@nteract/jupyter-widgets": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"@nteract/logos": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"@nteract/markdown": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"@nteract/monaco-editor": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"@nteract/octicons": {
|
||||
"react": "18.2.0"
|
||||
},
|
||||
"@nteract/outputs": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"@nteract/presentational-components": {
|
||||
"react": "18.2.0"
|
||||
},
|
||||
"@nteract/stateful-components": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"@nteract/transform-geojson": {
|
||||
"react": "18.2.0"
|
||||
},
|
||||
"@nteract/transform-model-debug": {
|
||||
"react": "18.2.0"
|
||||
},
|
||||
"@nteract/transform-plotly": {
|
||||
"react": "18.2.0"
|
||||
},
|
||||
"@nteract/transform-vdom": {
|
||||
"react": "18.2.0"
|
||||
},
|
||||
"@nteract/transform-vega": {
|
||||
"react": "18.2.0"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
|
||||
@@ -6,9 +6,9 @@ index e5dc283..1930c2b 100644
|
||||
|
||||
/// <reference types="jquery" />
|
||||
|
||||
-import DataTables, {Api} from 'datatables.net';
|
||||
+import DataTables, { Api } from 'datatables.net';
|
||||
|
||||
-import DataTables, {Api, ColumnSelector} from 'datatables.net';
|
||||
+import DataTables, { Api, ColumnSelector } from 'datatables.net';
|
||||
|
||||
export default DataTables;
|
||||
|
||||
@@ -40,6 +40,8 @@ declare module 'datatables.net' {
|
||||
@@ -17,6 +17,6 @@ index e5dc283..1930c2b 100644
|
||||
*/
|
||||
+ // Ignore this error: error TS7013: Construct signature, which lacks return-type annotation, implicitly has an 'any' return type.
|
||||
+ // @ts-ignore
|
||||
new (dt: Api<any>, settings: boolean | ConfigColReorder);
|
||||
new (dt: Api<any>, settings: boolean | ConfigColReorder): DataTablesStatic['ColReorder'];
|
||||
|
||||
/**
|
||||
@@ -1,6 +1,6 @@
|
||||
[defaults]
|
||||
group = dataexplorer-preview
|
||||
sku = P1V2
|
||||
sku = P1v2
|
||||
appserviceplan = dataexplorer-preview
|
||||
location = westus2
|
||||
web = dataexplorer-preview
|
||||
|
||||
36205
preview/package-lock.json
generated
36205
preview/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,16 +4,18 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:18-lts\" --sku P1V2",
|
||||
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:20-lts\" --sku P1V2",
|
||||
"start": "node index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Microsoft Corporation",
|
||||
"dependencies": {
|
||||
"express": "^4.17.1",
|
||||
"body-parser": "^1.20.3",
|
||||
"express": "^4.21.2",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"node": "^18.20.6",
|
||||
"node-fetch": "^2.6.1"
|
||||
"node": "^20.19.5",
|
||||
"node-fetch": "^2.6.1",
|
||||
"path-to-regexp": "^0.1.12"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,13 +138,12 @@ export enum MongoBackendEndpointType {
|
||||
remote,
|
||||
}
|
||||
|
||||
export class BackendApi {
|
||||
public static readonly GenerateToken: string = "GenerateToken";
|
||||
public static readonly PortalSettings: string = "PortalSettings";
|
||||
public static readonly AccountRestrictions: string = "AccountRestrictions";
|
||||
public static readonly RuntimeProxy: string = "RuntimeProxy";
|
||||
public static readonly DisallowedLocations: string = "DisallowedLocations";
|
||||
public static readonly SampleData: string = "SampleData";
|
||||
export class AadScopeEndpoints {
|
||||
public static readonly Development: string = "https://cosmos.azure.com";
|
||||
public static readonly MPAC: string = "https://cosmos.azure.com";
|
||||
public static readonly Prod: string = "https://cosmos.azure.com";
|
||||
public static readonly Fairfax: string = "https://cosmos.azure.us";
|
||||
public static readonly Mooncake: string = "https://cosmos.azure.cn";
|
||||
}
|
||||
|
||||
export class PortalBackendEndpoints {
|
||||
@@ -264,6 +263,7 @@ export class HttpHeaders {
|
||||
public static activityId: string = "x-ms-activity-id";
|
||||
public static apiType: string = "x-ms-cosmos-apitype";
|
||||
public static authorization: string = "authorization";
|
||||
public static entraIdToken: string = "x-ms-entraid-token";
|
||||
public static collectionIndexTransformationProgress: string =
|
||||
"x-ms-documentdb-collection-index-transformation-progress";
|
||||
public static continuation: string = "x-ms-continuation";
|
||||
@@ -774,3 +774,10 @@ export const ShortenedQueryCopilotSampleContainerSchema = {
|
||||
|
||||
userPrompt: "find all products",
|
||||
};
|
||||
|
||||
export enum MongoGuidRepresentation {
|
||||
Standard = "Standard",
|
||||
CSharpLegacy = "CSharpLegacy",
|
||||
JavaLegacy = "JavaLegacy",
|
||||
PythonLegacy = "PythonLegacy",
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
|
||||
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { PriorityLevel } from "../Common/Constants";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { Platform, configContext } from "../ConfigContext";
|
||||
import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
|
||||
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||
@@ -20,8 +20,7 @@ const _global = typeof self === "undefined" ? window : self;
|
||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||
|
||||
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
|
||||
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
||||
if (useDataplaneRbacAuthorization(userContext)) {
|
||||
Logger.logInfo(
|
||||
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
||||
"Explorer/tokenProvider",
|
||||
|
||||
@@ -28,3 +28,39 @@ describe("Environment Utility Test", () => {
|
||||
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Development);
|
||||
});
|
||||
});
|
||||
describe("normalizeArmEndpoint", () => {
|
||||
it("should append '/' if not present", () => {
|
||||
expect(EnvironmentUtility.normalizeArmEndpoint("https://example.com")).toBe("https://example.com/");
|
||||
});
|
||||
|
||||
it("should return the same uri if '/' is present at the end", () => {
|
||||
expect(EnvironmentUtility.normalizeArmEndpoint("https://example.com/")).toBe("https://example.com/");
|
||||
});
|
||||
|
||||
it("should handle empty string", () => {
|
||||
expect(EnvironmentUtility.normalizeArmEndpoint("")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEnvironment", () => {
|
||||
it("should return Prod environment", () => {
|
||||
updateConfigContext({
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||
});
|
||||
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Prod);
|
||||
});
|
||||
|
||||
it("should return Fairfax environment", () => {
|
||||
updateConfigContext({
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Fairfax,
|
||||
});
|
||||
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Fairfax);
|
||||
});
|
||||
|
||||
it("should return Mooncake environment", () => {
|
||||
updateConfigContext({
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mooncake,
|
||||
});
|
||||
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Mooncake);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PortalBackendEndpoints } from "Common/Constants";
|
||||
import { AadScopeEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||
import * as Logger from "Common/Logger";
|
||||
import { configContext } from "ConfigContext";
|
||||
|
||||
export function normalizeArmEndpoint(uri: string): string {
|
||||
@@ -27,3 +28,17 @@ export const getEnvironment = (): Environment => {
|
||||
|
||||
return environmentMap[configContext.PORTAL_BACKEND_ENDPOINT];
|
||||
};
|
||||
|
||||
export const getEnvironmentScopeEndpoint = (): string => {
|
||||
const environment = getEnvironment();
|
||||
const endpoint = AadScopeEndpoints[environment];
|
||||
if (!endpoint) {
|
||||
throw new Error("Cannot determine AAD scope endpoint");
|
||||
}
|
||||
const hrefEndpoint = new URL(endpoint).href.replace(/\/+$/, "/.default");
|
||||
Logger.logInfo(
|
||||
`Using AAD scope endpoint: ${hrefEndpoint}, Environment: ${environment}`,
|
||||
"EnvironmentUtility/getEnvironmentScopeEndpoint",
|
||||
);
|
||||
return hrefEndpoint;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { QueryOperationOptions } from "@azure/cosmos";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import { QueryResults } from "../Contracts/ViewModels";
|
||||
@@ -14,18 +13,14 @@ interface QueryResponse {
|
||||
}
|
||||
|
||||
export interface MinimalQueryIterator {
|
||||
fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise<QueryResponse>;
|
||||
fetchNext: () => Promise<QueryResponse>;
|
||||
}
|
||||
|
||||
// Pick<QueryIterator<any>, "fetchNext">;
|
||||
|
||||
export function nextPage(
|
||||
documentsIterator: MinimalQueryIterator,
|
||||
firstItemIndex: number,
|
||||
queryOperationOptions?: QueryOperationOptions,
|
||||
): Promise<QueryResults> {
|
||||
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
|
||||
TelemetryProcessor.traceStart(Action.ExecuteQuery);
|
||||
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
|
||||
return documentsIterator.fetchNext().then((response) => {
|
||||
TelemetryProcessor.traceSuccess(Action.ExecuteQuery, { dataExplorerArea: Constants.Areas.Tab });
|
||||
const documents = response.resources;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -65,7 +65,6 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -84,7 +83,6 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
queryDocuments(databaseId, collection, true, "{}");
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
@@ -101,7 +99,6 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -120,7 +117,6 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
readDocument(databaseId, collection, documentId);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
@@ -137,7 +133,6 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -156,7 +151,6 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
readDocument(databaseId, collection, documentId);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
@@ -173,7 +167,6 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -197,7 +190,6 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||
});
|
||||
@@ -216,7 +208,6 @@ describe("MongoProxyClient", () => {
|
||||
it("builds the correct proxy URL in development", () => {
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
deleteDocuments(databaseId, collection, [documentId]);
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
@@ -233,7 +224,6 @@ describe("MongoProxyClient", () => {
|
||||
});
|
||||
updateConfigContext({
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
globallyEnabledMongoAPIs: [],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||
import { getMongoGuidRepresentation } from "Shared/StorageUtility";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
@@ -6,6 +7,7 @@ import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { userContext } from "../UserContext";
|
||||
import { isDataplaneRbacEnabledForProxyApi } from "../Utils/AuthorizationUtils";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||
@@ -21,7 +23,13 @@ function authHeaders() {
|
||||
if (userContext.authType === AuthType.EncryptedToken) {
|
||||
return { [HttpHeaders.guestAccessToken]: userContext.accessToken };
|
||||
} else {
|
||||
return { [HttpHeaders.authorization]: userContext.authorizationToken };
|
||||
const headers: { [key: string]: string } = {
|
||||
[HttpHeaders.authorization]: userContext.authorizationToken,
|
||||
};
|
||||
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
|
||||
headers[HttpHeaders.entraIdToken] = userContext.aadToken;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +147,9 @@ export function readDocument(
|
||||
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
clientSettings: {
|
||||
guidRepresentation: getMongoGuidRepresentation(),
|
||||
},
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
@@ -181,6 +192,9 @@ export function createDocument(
|
||||
partitionKey:
|
||||
collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
||||
documentContent: JSON.stringify(documentContent),
|
||||
clientSettings: {
|
||||
guidRepresentation: getMongoGuidRepresentation(),
|
||||
},
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
@@ -228,6 +242,9 @@ export function updateDocument(
|
||||
? documentId.partitionKeyProperties?.[0]
|
||||
: "",
|
||||
documentContent,
|
||||
clientSettings: {
|
||||
guidRepresentation: getMongoGuidRepresentation(),
|
||||
},
|
||||
};
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
@@ -274,6 +291,9 @@ export function deleteDocuments(
|
||||
subscriptionID: userContext.subscriptionId,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseAccountName: databaseAccount.name,
|
||||
clientSettings: {
|
||||
guidRepresentation: getMongoGuidRepresentation(),
|
||||
},
|
||||
};
|
||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { monaco } from "Explorer/LazyMonaco";
|
||||
import { getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
|
||||
|
||||
export enum QueryErrorSeverity {
|
||||
Error = "Error",
|
||||
@@ -103,20 +102,9 @@ export interface ErrorEnrichment {
|
||||
learnMoreUrl?: string;
|
||||
}
|
||||
|
||||
const REPLACEMENT_MESSAGES: Record<string, (original: string) => string> = {
|
||||
OPERATION_RU_LIMIT_EXCEEDED: (original) => {
|
||||
if (ruThresholdEnabled()) {
|
||||
const threshold = getRUThreshold();
|
||||
return `Query exceeded the Request Unit (RU) limit of ${threshold} RUs. You can change this limit in Data Explorer settings.`;
|
||||
}
|
||||
return original;
|
||||
},
|
||||
};
|
||||
const REPLACEMENT_MESSAGES: Record<string, (original: string) => string> = {};
|
||||
|
||||
const HELP_LINKS: Record<string, string> = {
|
||||
OPERATION_RU_LIMIT_EXCEEDED:
|
||||
"https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer#configure-request-unit-threshold",
|
||||
};
|
||||
const HELP_LINKS: Record<string, string> = {};
|
||||
|
||||
export default class QueryError {
|
||||
message: string;
|
||||
|
||||
@@ -4,13 +4,18 @@ import * as React from "react";
|
||||
export interface TooltipProps {
|
||||
children: string;
|
||||
className?: string;
|
||||
ariaLabelForTooltip?: string;
|
||||
}
|
||||
|
||||
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children, className }: TooltipProps) => {
|
||||
export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({
|
||||
children,
|
||||
className,
|
||||
ariaLabelForTooltip = children,
|
||||
}: TooltipProps) => {
|
||||
return (
|
||||
<span className={className}>
|
||||
<TooltipHost content={children}>
|
||||
<Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} />
|
||||
<Icon iconName="Info" aria-label={ariaLabelForTooltip} className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
|
||||
{
|
||||
"disableNonStreamingOrderByQuery": true,
|
||||
"enableQueryControl": false,
|
||||
"enableScanInQuery": true,
|
||||
"forceQueryPlan": true,
|
||||
"maxDegreeOfParallelism": 0,
|
||||
@@ -13,7 +13,7 @@ exports[`getCommonQueryOptions builds the correct default options objects 1`] =
|
||||
|
||||
exports[`getCommonQueryOptions reads from localStorage 1`] = `
|
||||
{
|
||||
"disableNonStreamingOrderByQuery": true,
|
||||
"enableQueryControl": false,
|
||||
"enableScanInQuery": true,
|
||||
"forceQueryPlan": true,
|
||||
"maxDegreeOfParallelism": 17,
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface IBulkDeleteResult {
|
||||
export const deleteDocuments = async (
|
||||
collection: CollectionBase,
|
||||
documentIds: DocumentId[],
|
||||
abortSignal: AbortSignal,
|
||||
): Promise<IBulkDeleteResult[]> => {
|
||||
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
|
||||
try {
|
||||
@@ -65,12 +66,16 @@ export const deleteDocuments = async (
|
||||
operationType: BulkOperationType.Delete,
|
||||
}));
|
||||
|
||||
const promise = v2Container.items.bulk(operations).then((bulkResults) => {
|
||||
return bulkResults.map((bulkResult, index) => {
|
||||
const documentId = documentIdsChunk[index];
|
||||
return { ...bulkResult, documentId };
|
||||
const promise = v2Container.items
|
||||
.bulk(operations, undefined, {
|
||||
abortSignal,
|
||||
})
|
||||
.then((bulkResults) => {
|
||||
return bulkResults.map((bulkResult, index) => {
|
||||
const documentId = documentIdsChunk[index];
|
||||
return { ...bulkResult, documentId };
|
||||
});
|
||||
});
|
||||
});
|
||||
promiseArray.push(promise);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import { userContext } from "../../UserContext";
|
||||
@@ -41,7 +42,7 @@ interface MetricsResponse {
|
||||
}
|
||||
|
||||
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
|
||||
if (userContext.authType !== AuthType.AAD) {
|
||||
if (userContext.authType !== AuthType.AAD || isFabricNative()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
import { Queries } from "../Constants";
|
||||
import { client } from "../CosmosClient";
|
||||
@@ -26,7 +25,7 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
|
||||
options.maxItemCount ||
|
||||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
|
||||
Queries.itemsPerPage;
|
||||
options.enableQueryControl = LocalStorageUtility.getEntryBoolean(StorageKey.QueryControlEnabled);
|
||||
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
||||
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
|
||||
return options;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { QueryOperationOptions } from "@azure/cosmos";
|
||||
import { QueryResults } from "../../Contracts/ViewModels";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
@@ -9,13 +8,12 @@ export const queryDocumentsPage = async (
|
||||
resourceName: string,
|
||||
documentsIterator: MinimalQueryIterator,
|
||||
firstItemIndex: number,
|
||||
queryOperationOptions?: QueryOperationOptions,
|
||||
): Promise<QueryResults> => {
|
||||
const entityName = getEntityName();
|
||||
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
|
||||
|
||||
try {
|
||||
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex, queryOperationOptions);
|
||||
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex);
|
||||
const itemCount = (result.documents && result.documents.length) || 0;
|
||||
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
|
||||
return result;
|
||||
|
||||
@@ -126,12 +126,5 @@ async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Co
|
||||
throw new Error(`Unsupported default experience type: ${apiType}`);
|
||||
}
|
||||
|
||||
// TO DO: Remove when we get RP API Spec with materializedViews
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
return rpResponse?.value?.map((collection: any) => {
|
||||
const collectionDataModel: DataModels.Collection = collection.properties?.resource as DataModels.Collection;
|
||||
collectionDataModel.materializedViews = collection.properties?.resource?.materializedViews;
|
||||
collectionDataModel.materializedViewDefinition = collection.properties?.resource?.materializedViewDefinition;
|
||||
return collectionDataModel;
|
||||
});
|
||||
return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||
import {
|
||||
BackendApi,
|
||||
CassandraProxyEndpoints,
|
||||
JunoEndpoints,
|
||||
MongoProxyEndpoints,
|
||||
PortalBackendEndpoints,
|
||||
} from "Common/Constants";
|
||||
import {
|
||||
allowedAadEndpoints,
|
||||
allowedArcadiaEndpoints,
|
||||
allowedEmulatorEndpoints,
|
||||
allowedGraphEndpoints,
|
||||
allowedHostedExplorerEndpoints,
|
||||
allowedJunoOrigins,
|
||||
allowedMsalRedirectEndpoints,
|
||||
defaultAllowedAadEndpoints,
|
||||
defaultAllowedArmEndpoints,
|
||||
defaultAllowedBackendEndpoints,
|
||||
defaultAllowedCassandraProxyEndpoints,
|
||||
defaultAllowedGraphEndpoints,
|
||||
defaultAllowedMongoProxyEndpoints,
|
||||
validateEndpoint,
|
||||
} from "Utils/EndpointUtils";
|
||||
@@ -29,6 +23,8 @@ export enum Platform {
|
||||
|
||||
export interface ConfigContext {
|
||||
platform: Platform;
|
||||
allowedAadEndpoints: ReadonlyArray<string>;
|
||||
allowedGraphEndpoints: ReadonlyArray<string>;
|
||||
allowedArmEndpoints: ReadonlyArray<string>;
|
||||
allowedBackendEndpoints: ReadonlyArray<string>;
|
||||
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
|
||||
@@ -37,10 +33,8 @@ export interface ConfigContext {
|
||||
gitSha?: string;
|
||||
proxyPath?: string;
|
||||
AAD_ENDPOINT: string;
|
||||
ARM_AUTH_AREA: string;
|
||||
ARM_ENDPOINT: string;
|
||||
EMULATOR_ENDPOINT?: string;
|
||||
ARM_API_VERSION: string;
|
||||
GRAPH_ENDPOINT: string;
|
||||
GRAPH_API_VERSION: string;
|
||||
// This is the endpoint to get offering Ids to be used to fetch prices. Refer to this doc: https://learn.microsoft.com/en-us/rest/api/marketplacecatalog/dataplane/skus/list?view=rest-marketplacecatalog-dataplane-2023-05-01-preview&tabs=HTTP
|
||||
@@ -50,27 +44,24 @@ export interface ConfigContext {
|
||||
ARCADIA_ENDPOINT: string;
|
||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||
PORTAL_BACKEND_ENDPOINT: string;
|
||||
NEW_BACKEND_APIS?: BackendApi[];
|
||||
MONGO_PROXY_ENDPOINT: string;
|
||||
CASSANDRA_PROXY_ENDPOINT: string;
|
||||
NEW_CASSANDRA_APIS?: string[];
|
||||
PROXY_PATH?: string;
|
||||
JUNO_ENDPOINT: string;
|
||||
GITHUB_CLIENT_ID: string;
|
||||
GITHUB_TEST_ENV_CLIENT_ID: string;
|
||||
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
||||
isTerminalEnabled: boolean;
|
||||
isPhoenixEnabled: boolean;
|
||||
hostedExplorerURL: string;
|
||||
armAPIVersion?: string;
|
||||
msalRedirectURI?: string;
|
||||
globallyEnabledCassandraAPIs?: string[];
|
||||
globallyEnabledMongoAPIs?: string[];
|
||||
}
|
||||
|
||||
// Default configuration
|
||||
let configContext: Readonly<ConfigContext> = {
|
||||
platform: Platform.Portal,
|
||||
allowedAadEndpoints: defaultAllowedAadEndpoints,
|
||||
allowedGraphEndpoints: defaultAllowedGraphEndpoints,
|
||||
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
||||
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
||||
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
|
||||
@@ -85,17 +76,12 @@ let configContext: Readonly<ConfigContext> = {
|
||||
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
|
||||
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
|
||||
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
||||
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
||||
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
|
||||
], // Webpack injects this at build time
|
||||
gitSha: process.env.GIT_SHA,
|
||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
||||
ARM_AUTH_AREA: "https://management.azure.com/",
|
||||
ARM_ENDPOINT: "https://management.azure.com/",
|
||||
ARM_API_VERSION: "2016-06-01",
|
||||
GRAPH_ENDPOINT: "https://graph.microsoft.com",
|
||||
GRAPH_API_VERSION: "1.6",
|
||||
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
|
||||
@@ -109,11 +95,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
||||
isTerminalEnabled: false,
|
||||
isPhoenixEnabled: false,
|
||||
globallyEnabledCassandraAPIs: [],
|
||||
globallyEnabledMongoAPIs: [],
|
||||
};
|
||||
|
||||
export function resetConfigContext(): void {
|
||||
@@ -128,19 +110,21 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
|
||||
delete newContext.ARM_ENDPOINT;
|
||||
if (!validateEndpoint(newContext.AAD_ENDPOINT, configContext.allowedAadEndpoints || defaultAllowedAadEndpoints)) {
|
||||
delete newContext.AAD_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.AAD_ENDPOINT, allowedAadEndpoints)) {
|
||||
delete newContext.AAD_ENDPOINT;
|
||||
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
|
||||
delete newContext.ARM_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
|
||||
delete newContext.EMULATOR_ENDPOINT;
|
||||
}
|
||||
|
||||
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) {
|
||||
if (
|
||||
!validateEndpoint(newContext.GRAPH_ENDPOINT, configContext.allowedGraphEndpoints || defaultAllowedGraphEndpoints)
|
||||
) {
|
||||
delete newContext.GRAPH_ENDPOINT;
|
||||
}
|
||||
|
||||
@@ -148,6 +132,15 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
||||
delete newContext.ARCADIA_ENDPOINT;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.PORTAL_BACKEND_ENDPOINT,
|
||||
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints,
|
||||
)
|
||||
) {
|
||||
delete newContext.PORTAL_BACKEND_ENDPOINT;
|
||||
}
|
||||
|
||||
if (
|
||||
!validateEndpoint(
|
||||
newContext.MONGO_PROXY_ENDPOINT,
|
||||
@@ -187,8 +180,7 @@ if (process.env.NODE_ENV === "development") {
|
||||
PROXY_PATH: "/proxy",
|
||||
EMULATOR_ENDPOINT: "https://localhost:8081",
|
||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mpac,
|
||||
// MONGO_PROXY_ENDPOINT: "https://cosmos-db-portal-mongoproxy1-mpac-westus.azurewebsites.net",
|
||||
MONGO_PROXY_ENDPOINT: "https://localhost:7238",
|
||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Mpac,
|
||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Mpac,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export enum PaneKind {
|
||||
GlobalSettings,
|
||||
AdHocAccess,
|
||||
SwitchDirectory,
|
||||
QuickStart,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface ArmEntity {
|
||||
type: string;
|
||||
kind: string;
|
||||
tags?: Tags;
|
||||
resourceGroup?: string;
|
||||
}
|
||||
|
||||
export interface DatabaseAccount extends ArmEntity {
|
||||
@@ -388,7 +389,7 @@ export interface VectorEmbeddingPolicy {
|
||||
}
|
||||
|
||||
export interface VectorEmbedding {
|
||||
dataType: "float16" | "float32" | "uint8" | "int8";
|
||||
dataType: "float32" | "uint8" | "int8";
|
||||
dimensions: number;
|
||||
distanceFunction: "euclidean" | "cosine" | "dotproduct";
|
||||
path: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
ItemDefinition,
|
||||
JSONObject,
|
||||
QueryMetrics,
|
||||
Resource,
|
||||
@@ -30,8 +31,11 @@ export interface UploadDetailsRecord {
|
||||
numFailed: number;
|
||||
numThrottled: number;
|
||||
errors: string[];
|
||||
resources?: ItemDefinition[];
|
||||
}
|
||||
|
||||
export type BulkInsertResult = Omit<UploadDetailsRecord, "fileName">;
|
||||
|
||||
export interface QueryResultsMetadata {
|
||||
hasMoreResults: boolean;
|
||||
firstItemIndex: number;
|
||||
@@ -46,6 +50,7 @@ export interface QueryResults extends QueryResultsMetadata {
|
||||
roundTrips?: number;
|
||||
headers?: any;
|
||||
queryMetrics?: QueryMetrics;
|
||||
ruThresholdExceeded?: boolean;
|
||||
}
|
||||
|
||||
export interface Button {
|
||||
@@ -438,6 +443,7 @@ export interface DataExplorerInputsFrame {
|
||||
[key: string]: string;
|
||||
};
|
||||
feedbackPolicies?: any;
|
||||
aadToken?: string;
|
||||
}
|
||||
|
||||
export interface SelfServeFrameInputs {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<!doctype html>
|
||||
<html class="no-js" lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="data:," />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="heatmap"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,55 +0,0 @@
|
||||
@import "../../../less/Common/Constants";
|
||||
html {
|
||||
font-family: @DataExplorerFont;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: @DataExplorerFont;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
border: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#heatmap {
|
||||
.dark-theme {
|
||||
color: @BaseLight;
|
||||
}
|
||||
|
||||
.chartTitle {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 3px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.noDataMessage {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0.97;
|
||||
div {
|
||||
border-color: rgba(204, 204, 204, 0.8);
|
||||
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.12);
|
||||
padding: 15px 10px;
|
||||
width: calc(55% - 40px);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
import { handleMessage, Heatmap, isDarkTheme } from "./Heatmap";
|
||||
import { PortalTheme } from "./HeatmapDatatypes";
|
||||
|
||||
describe("The Heatmap Control", () => {
|
||||
const dataPoints = {
|
||||
"1": {
|
||||
"2019-06-19T00:59:10Z": {
|
||||
"Normalized Throughput": 0.35,
|
||||
},
|
||||
"2019-06-19T00:48:10Z": {
|
||||
"Normalized Throughput": 0.25,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const chartCaptions = {
|
||||
chartTitle: "chart title",
|
||||
yAxisTitle: "YAxisTitle",
|
||||
tooltipText: "Tooltip text",
|
||||
timeWindow: 123456789,
|
||||
};
|
||||
|
||||
let heatmap: Heatmap;
|
||||
const theme: PortalTheme = 1;
|
||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
||||
|
||||
describe("drawHeatmap rendering", () => {
|
||||
beforeEach(() => {
|
||||
heatmap = new Heatmap(dataPoints, chartCaptions, theme);
|
||||
document.body.innerHTML = divElement;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ``;
|
||||
});
|
||||
|
||||
it("should call _getChartSettings when drawHeatmap is invoked", () => {
|
||||
const _getChartSettings = jest.spyOn(heatmap, "_getChartSettings");
|
||||
heatmap.drawHeatmap();
|
||||
expect(_getChartSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call _getLayoutSettings when drawHeatmap is invoked", () => {
|
||||
const _getLayoutSettings = jest.spyOn(heatmap, "_getLayoutSettings");
|
||||
heatmap.drawHeatmap();
|
||||
expect(_getLayoutSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call _getChartDisplaySettings when drawHeatmap is invoked", () => {
|
||||
const _getChartDisplaySettings = jest.spyOn(heatmap, "_getChartDisplaySettings");
|
||||
heatmap.drawHeatmap();
|
||||
expect(_getChartDisplaySettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("drawHeatmap should render a Heatmap inside the div element", () => {
|
||||
heatmap.drawHeatmap();
|
||||
expect(document.body.innerHTML).not.toEqual(divElement);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateMatrixFromMap", () => {
|
||||
it("should massage input data to match output expected", () => {
|
||||
expect(heatmap.generateMatrixFromMap(dataPoints).yAxisPoints).toEqual(["1"]);
|
||||
expect(heatmap.generateMatrixFromMap(dataPoints).dataPoints).toEqual([[0.25, 0.35]]);
|
||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints.length).toEqual(2);
|
||||
});
|
||||
|
||||
it("should output the date format to ISO8601 string format", () => {
|
||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints[0].slice(10, 11)).toEqual("T");
|
||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints[0].slice(-1)).toEqual("Z");
|
||||
});
|
||||
|
||||
it("should convert the time to the user's local time", () => {
|
||||
if (dayjs().utcOffset()) {
|
||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).not.toEqual([
|
||||
"2019-06-19T00:48:10Z",
|
||||
"2019-06-19T00:59:10Z",
|
||||
]);
|
||||
} else {
|
||||
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).toEqual([
|
||||
"2019-06-19T00:48:10Z",
|
||||
"2019-06-19T00:59:10Z",
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("isDarkTheme", () => {
|
||||
it("isDarkTheme should return the correct result", () => {
|
||||
expect(isDarkTheme(PortalTheme.dark)).toEqual(true);
|
||||
expect(isDarkTheme(PortalTheme.azure)).not.toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("iframe rendering when there is no data", () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ``;
|
||||
});
|
||||
|
||||
it("should show a no data message with a dark theme", () => {
|
||||
const data = {
|
||||
data: {
|
||||
signature: "pcIframe",
|
||||
data: {
|
||||
chartData: {},
|
||||
chartSettings: {},
|
||||
theme: 4,
|
||||
},
|
||||
},
|
||||
origin: "http://localhost",
|
||||
};
|
||||
|
||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
||||
document.body.innerHTML = divElement;
|
||||
|
||||
handleMessage(data as MessageEvent);
|
||||
expect(document.body.innerHTML).toContain("dark-theme");
|
||||
expect(document.body.innerHTML).toContain("noDataMessage");
|
||||
});
|
||||
|
||||
it("should show a no data message with a white theme", () => {
|
||||
const data = {
|
||||
data: {
|
||||
signature: "pcIframe",
|
||||
data: {
|
||||
chartData: {},
|
||||
chartSettings: {},
|
||||
theme: 2,
|
||||
},
|
||||
},
|
||||
origin: "http://localhost",
|
||||
};
|
||||
|
||||
const divElement = `<div id="${Heatmap.elementId}"></div>`;
|
||||
document.body.innerHTML = divElement;
|
||||
|
||||
handleMessage(data as MessageEvent);
|
||||
expect(document.body.innerHTML).not.toContain("dark-theme");
|
||||
expect(document.body.innerHTML).toContain("noDataMessage");
|
||||
});
|
||||
});
|
||||
@@ -1,272 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
import * as Plotly from "plotly.js-cartesian-dist-min";
|
||||
import { sendCachedDataMessage, sendReadyMessage } from "../../Common/MessageHandler";
|
||||
import { StyleConstants } from "../../Common/StyleConstants";
|
||||
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
||||
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
|
||||
import "./Heatmap.less";
|
||||
import {
|
||||
ChartSettings,
|
||||
DataPayload,
|
||||
DisplaySettings,
|
||||
FontSettings,
|
||||
HeatmapCaptions,
|
||||
HeatmapData,
|
||||
LayoutSettings,
|
||||
PartitionTimeStampToData,
|
||||
PortalTheme,
|
||||
} from "./HeatmapDatatypes";
|
||||
|
||||
export class Heatmap {
|
||||
public static readonly elementId: string = "heatmap";
|
||||
|
||||
private _chartData: HeatmapData;
|
||||
private _heatmapCaptions: HeatmapCaptions;
|
||||
private _theme: PortalTheme;
|
||||
private _defaultFontColor: string;
|
||||
|
||||
constructor(data: DataPayload, heatmapCaptions: HeatmapCaptions, theme: PortalTheme) {
|
||||
this._theme = theme;
|
||||
this._defaultFontColor = StyleConstants.BaseDark;
|
||||
this._setThemeColorForChart();
|
||||
this._chartData = this.generateMatrixFromMap(data);
|
||||
this._heatmapCaptions = heatmapCaptions;
|
||||
}
|
||||
|
||||
private _setThemeColorForChart() {
|
||||
if (isDarkTheme(this._theme)) {
|
||||
this._defaultFontColor = StyleConstants.BaseLight;
|
||||
}
|
||||
}
|
||||
|
||||
private _getFontStyles(size: number = StyleConstants.MediumFontSize, color = "#838383"): FontSettings {
|
||||
return {
|
||||
family: StyleConstants.DataExplorerFont,
|
||||
size,
|
||||
color,
|
||||
};
|
||||
}
|
||||
|
||||
public generateMatrixFromMap(data: DataPayload): HeatmapData {
|
||||
// all keys in data payload, sorted...
|
||||
const rows: string[] = Object.keys(data).sort((a: string, b: string) => {
|
||||
if (parseInt(a) < parseInt(b)) {
|
||||
return -1;
|
||||
} else {
|
||||
if (parseInt(a) > parseInt(b)) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
const output: HeatmapData = {
|
||||
yAxisPoints: [],
|
||||
dataPoints: [],
|
||||
xAxisPoints: Object.keys(data[rows[0]]).sort((a: string, b: string) => {
|
||||
if (a < b) {
|
||||
return -1;
|
||||
} else {
|
||||
if (a > b) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}),
|
||||
};
|
||||
// go thru all rows and create 2d matrix for heatmap...
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
output.yAxisPoints.push(rows[i]);
|
||||
const dataPoints: number[] = [];
|
||||
for (let a = 0; a < output.xAxisPoints.length; a++) {
|
||||
const row: PartitionTimeStampToData = data[rows[i]];
|
||||
dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]);
|
||||
}
|
||||
output.dataPoints.push(dataPoints);
|
||||
}
|
||||
for (let a = 0; a < output.xAxisPoints.length; a++) {
|
||||
const dateTime = output.xAxisPoints[a];
|
||||
// convert to local users timezone...
|
||||
const day = dayjs(new Date(dateTime)).format("YYYY-MM-DD");
|
||||
const hour = dayjs(new Date(dateTime)).format("HH:mm:ss");
|
||||
// coerce to ISOString format since that is what plotly wants...
|
||||
output.xAxisPoints[a] = `${day}T${hour}Z`;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
// public for testing purposes
|
||||
public _getChartSettings(): ChartSettings[] {
|
||||
return [
|
||||
{
|
||||
z: this._chartData.dataPoints,
|
||||
type: "heatmap",
|
||||
zmin: 0,
|
||||
zmid: 50,
|
||||
zmax: 100,
|
||||
colorscale: [
|
||||
[0.0, "#1FD338"],
|
||||
[0.1, "#1CAD2F"],
|
||||
[0.2, "#50A527"],
|
||||
[0.3, "#719F21"],
|
||||
[0.4, "#95991B"],
|
||||
[0.5, "#CE8F11"],
|
||||
[0.6, "#E27F0F"],
|
||||
[0.7, "#E46612"],
|
||||
[0.8, "#E64914"],
|
||||
[0.9, "#B80016"],
|
||||
[1.0, "#B80016"],
|
||||
],
|
||||
name: "",
|
||||
hovertemplate: this._heatmapCaptions.tooltipText,
|
||||
colorbar: {
|
||||
thickness: 15,
|
||||
outlinewidth: 0,
|
||||
tickcolor: StyleConstants.BaseDark,
|
||||
tickfont: this._getFontStyles(10, this._defaultFontColor),
|
||||
},
|
||||
y: this._chartData.yAxisPoints,
|
||||
x: this._chartData.xAxisPoints,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// public for testing purposes
|
||||
public _getLayoutSettings(): LayoutSettings {
|
||||
return {
|
||||
margin: {
|
||||
l: 40,
|
||||
r: 10,
|
||||
b: 35,
|
||||
t: 30,
|
||||
pad: 0,
|
||||
},
|
||||
paper_bgcolor: "transparent",
|
||||
plot_bgcolor: "transparent",
|
||||
width: 462,
|
||||
height: 240,
|
||||
yaxis: {
|
||||
title: this._heatmapCaptions.yAxisTitle,
|
||||
titlefont: this._getFontStyles(11),
|
||||
autorange: true,
|
||||
showgrid: false,
|
||||
zeroline: false,
|
||||
showline: false,
|
||||
autotick: true,
|
||||
fixedrange: true,
|
||||
ticks: "",
|
||||
showticklabels: false,
|
||||
},
|
||||
xaxis: {
|
||||
fixedrange: true,
|
||||
title: "*White area in heatmap indicates there is no available data",
|
||||
titlefont: this._getFontStyles(11),
|
||||
autorange: true,
|
||||
showgrid: false,
|
||||
zeroline: false,
|
||||
showline: false,
|
||||
autotick: true,
|
||||
tickformat: this._heatmapCaptions.timeWindow > 7 ? "%I:%M %p" : "%b %e",
|
||||
showticklabels: true,
|
||||
tickfont: this._getFontStyles(10),
|
||||
},
|
||||
title: {
|
||||
text: this._heatmapCaptions.chartTitle,
|
||||
x: 0.01,
|
||||
font: this._getFontStyles(13, this._defaultFontColor),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// public for testing purposes
|
||||
public _getChartDisplaySettings(): DisplaySettings {
|
||||
return {
|
||||
/* heatmap can be fully responsive however the min-height needed in that case is greater than the iframe portal height, hence explicit width + height have been set in _getLayoutSettings
|
||||
responsive: true,*/
|
||||
displayModeBar: false,
|
||||
};
|
||||
}
|
||||
|
||||
public drawHeatmap(): void {
|
||||
// todo - create random elementId generator so multiple heatmaps can be created - ticket # 431469
|
||||
Plotly.plot(
|
||||
Heatmap.elementId,
|
||||
this._getChartSettings(),
|
||||
this._getLayoutSettings(),
|
||||
this._getChartDisplaySettings(),
|
||||
);
|
||||
const plotDiv: any = document.getElementById(Heatmap.elementId);
|
||||
plotDiv.on("plotly_click", (data: any) => {
|
||||
let timeSelected: string = data.points[0].x;
|
||||
timeSelected = timeSelected.replace(" ", "T");
|
||||
timeSelected = `${timeSelected}Z`;
|
||||
let xAxisIndex = 0;
|
||||
for (let i = 0; i < this._chartData.xAxisPoints.length; i++) {
|
||||
if (this._chartData.xAxisPoints[i] === timeSelected) {
|
||||
xAxisIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const output = [];
|
||||
for (let i = 0; i < this._chartData.dataPoints.length; i++) {
|
||||
output.push(this._chartData.dataPoints[i][xAxisIndex]);
|
||||
}
|
||||
sendCachedDataMessage(MessageTypes.LogInfo, output);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function isDarkTheme(theme: PortalTheme) {
|
||||
return theme === PortalTheme.dark;
|
||||
}
|
||||
|
||||
export function handleMessage(event: MessageEvent) {
|
||||
if (isInvalidParentFrameOrigin(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof event.data !== "object" || event.data["signature"] !== "pcIframe") {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
typeof event.data.data !== "object" ||
|
||||
!("chartData" in event.data.data) ||
|
||||
!("chartSettings" in event.data.data)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
Plotly.purge(Heatmap.elementId);
|
||||
|
||||
document.getElementById(Heatmap.elementId)!.innerHTML = "";
|
||||
const data = event.data.data;
|
||||
const chartData: DataPayload = data.chartData;
|
||||
const chartSettings: HeatmapCaptions = data.chartSettings;
|
||||
const chartTheme: PortalTheme = data.theme;
|
||||
if (Object.keys(chartData).length) {
|
||||
new Heatmap(chartData, chartSettings, chartTheme).drawHeatmap();
|
||||
} else {
|
||||
const chartTitleElement = document.createElement("div");
|
||||
chartTitleElement.innerHTML = data.chartSettings.chartTitle;
|
||||
chartTitleElement.classList.add("chartTitle");
|
||||
|
||||
const noDataMessageElement = document.createElement("div");
|
||||
noDataMessageElement.classList.add("noDataMessage");
|
||||
const noDataMessageContent = document.createElement("div");
|
||||
noDataMessageContent.innerHTML = data.errorMessage;
|
||||
|
||||
noDataMessageElement.appendChild(noDataMessageContent);
|
||||
|
||||
if (isDarkTheme(chartTheme)) {
|
||||
chartTitleElement.classList.add("dark-theme");
|
||||
noDataMessageElement.classList.add("dark-theme");
|
||||
noDataMessageContent.classList.add("dark-theme");
|
||||
}
|
||||
|
||||
document.getElementById(Heatmap.elementId)!.appendChild(chartTitleElement);
|
||||
document.getElementById(Heatmap.elementId)!.appendChild(noDataMessageElement);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage, false);
|
||||
sendReadyMessage();
|
||||
@@ -1,106 +0,0 @@
|
||||
type dataPoint = string | number;
|
||||
|
||||
export interface DataPayload {
|
||||
[id: string]: PartitionTimeStampToData;
|
||||
}
|
||||
|
||||
export enum PortalTheme {
|
||||
blue = 1,
|
||||
azure,
|
||||
light,
|
||||
dark,
|
||||
}
|
||||
|
||||
export interface HeatmapData {
|
||||
yAxisPoints: string[];
|
||||
xAxisPoints: string[];
|
||||
dataPoints: dataPoint[][];
|
||||
}
|
||||
|
||||
export interface HeatmapCaptions {
|
||||
chartTitle: string;
|
||||
yAxisTitle: string;
|
||||
tooltipText: string;
|
||||
timeWindow: number;
|
||||
}
|
||||
|
||||
export interface FontSettings {
|
||||
family: string;
|
||||
size: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface LayoutSettings {
|
||||
paper_bgcolor?: string;
|
||||
plot_bgcolor?: string;
|
||||
margin?: {
|
||||
l: number;
|
||||
r: number;
|
||||
b: number;
|
||||
t: number;
|
||||
pad: number;
|
||||
};
|
||||
width?: number;
|
||||
height?: number;
|
||||
yaxis?: {
|
||||
fixedrange: boolean;
|
||||
title: HeatmapCaptions["yAxisTitle"];
|
||||
titlefont: FontSettings;
|
||||
autorange: boolean;
|
||||
showgrid: boolean;
|
||||
zeroline: boolean;
|
||||
showline: boolean;
|
||||
autotick: boolean;
|
||||
ticks: "";
|
||||
showticklabels: boolean;
|
||||
};
|
||||
xaxis?: {
|
||||
fixedrange: boolean;
|
||||
title: string;
|
||||
titlefont: FontSettings;
|
||||
autorange: boolean;
|
||||
showgrid: boolean;
|
||||
zeroline: boolean;
|
||||
showline: boolean;
|
||||
autotick: boolean;
|
||||
showticklabels: boolean;
|
||||
tickformat: string;
|
||||
tickfont: FontSettings;
|
||||
};
|
||||
title?: {
|
||||
text: HeatmapCaptions["chartTitle"];
|
||||
x: number;
|
||||
font?: FontSettings;
|
||||
};
|
||||
font?: FontSettings;
|
||||
}
|
||||
|
||||
export interface ChartSettings {
|
||||
z: HeatmapData["dataPoints"];
|
||||
type: "heatmap";
|
||||
zmin: number;
|
||||
zmid: number;
|
||||
zmax: number;
|
||||
colorscale: [number, string][];
|
||||
name: string;
|
||||
hovertemplate: HeatmapCaptions["tooltipText"];
|
||||
colorbar: {
|
||||
thickness: number;
|
||||
outlinewidth: number;
|
||||
tickcolor: string;
|
||||
tickfont: FontSettings;
|
||||
};
|
||||
y: HeatmapData["yAxisPoints"];
|
||||
x: HeatmapData["xAxisPoints"];
|
||||
}
|
||||
|
||||
export interface DisplaySettings {
|
||||
displayModeBar: boolean;
|
||||
responsive?: boolean;
|
||||
}
|
||||
|
||||
export interface PartitionTimeStampToData {
|
||||
[timeSeriesDates: string]: {
|
||||
[NormalizedThroughput: string]: number;
|
||||
};
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import React, { FC, useEffect } from "react";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { create } from "zustand";
|
||||
|
||||
export interface DialogState {
|
||||
visible: boolean;
|
||||
@@ -38,7 +38,7 @@ export interface DialogState {
|
||||
showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps) => void;
|
||||
}
|
||||
|
||||
export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
||||
export const useDialog = create<DialogState>((set, get) => ({
|
||||
visible: false,
|
||||
openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })),
|
||||
closeDialog: () =>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { AuthType } from "AuthType";
|
||||
import { shallow } from "enzyme";
|
||||
import ko from "knockout";
|
||||
import { Features } from "Platform/Hosted/extractFeatures";
|
||||
import React from "react";
|
||||
import { updateCollection } from "../../../Common/dataAccess/updateCollection";
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
@@ -253,7 +252,7 @@ describe("SettingsComponent", () => {
|
||||
it("should save throughput bucket changes when Save button is clicked", async () => {
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
features: { enableThroughputBuckets: true } as Features,
|
||||
throughputBucketsEnabled: true,
|
||||
authType: AuthType.AAD,
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "Explorer/Controls/Settings/SettingsSubComponents/ThroughputInputComponents/ThroughputBucketsComponent";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||
import { isFullTextSearchEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
|
||||
import * as React from "react";
|
||||
import DiscardIcon from "../../../../images/discard.svg";
|
||||
@@ -188,13 +188,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.isGlobalSecondaryIndex =
|
||||
!!this.collection?.materializedViewDefinition() || !!this.collection?.materializedViews();
|
||||
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||
this.isFullTextSearchEnabled = isFullTextSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
|
||||
this.isFullTextSearchEnabled = userContext.apiType === "SQL";
|
||||
|
||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||
this.throughputBucketsEnabled =
|
||||
userContext.apiType === "SQL" &&
|
||||
userContext.features.enableThroughputBuckets &&
|
||||
userContext.authType === AuthType.AAD;
|
||||
this.throughputBucketsEnabled = userContext.throughputBucketsEnabled;
|
||||
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
this.isFixedContainer =
|
||||
@@ -1074,11 +1071,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
databaseId: this.collection.databaseId,
|
||||
collectionId: this.collection.id(),
|
||||
currentOffer: this.collection.offer(),
|
||||
autopilotThroughput: this.collection.offer().autoscaleMaxThroughput
|
||||
? this.collection.offer().autoscaleMaxThroughput
|
||||
autopilotThroughput: this.collection.offer?.()?.autoscaleMaxThroughput
|
||||
? this.collection.offer?.()?.autoscaleMaxThroughput
|
||||
: undefined,
|
||||
manualThroughput: this.collection.offer().manualThroughput
|
||||
? this.collection.offer().manualThroughput
|
||||
manualThroughput: this.collection.offer?.()?.manualThroughput
|
||||
? this.collection.offer?.()?.manualThroughput
|
||||
: undefined,
|
||||
throughputBuckets: this.state.throughputBuckets,
|
||||
});
|
||||
@@ -1094,6 +1091,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
currentOffer: this.collection.offer(),
|
||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput,
|
||||
throughputBuckets: this.throughputBucketsEnabled ? this.state.throughputBuckets : undefined,
|
||||
};
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
@@ -1344,7 +1342,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
});
|
||||
}
|
||||
|
||||
if (this.throughputBucketsEnabled) {
|
||||
if (this.throughputBucketsEnabled && !hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.ThroughputBucketsTab,
|
||||
content: <ThroughputBucketsComponent {...throughputBucketsComponentProps} />,
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("ThroughputBucketsComponent", () => {
|
||||
|
||||
it("renders the correct number of buckets", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} />);
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
||||
expect(screen.getAllByText(/Bucket \d+/)).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("renders buckets in the correct order even if input is unordered", () => {
|
||||
@@ -36,8 +36,14 @@ describe("ThroughputBucketsComponent", () => {
|
||||
];
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={unorderedBuckets} />);
|
||||
|
||||
const bucketLabels = screen.getAllByText(/Group \d+/).map((el) => el.textContent);
|
||||
expect(bucketLabels).toEqual(["Group 1 (Data Explorer Query Bucket)", "Group 2", "Group 3", "Group 4", "Group 5"]);
|
||||
const bucketLabels = screen.getAllByText(/Bucket \d+/).map((el) => el.textContent);
|
||||
expect(bucketLabels).toEqual([
|
||||
"Bucket 1 (Data Explorer Query Bucket)",
|
||||
"Bucket 2",
|
||||
"Bucket 3",
|
||||
"Bucket 4",
|
||||
"Bucket 5",
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders all provided buckets even if they exceed the max default bucket count", () => {
|
||||
@@ -53,7 +59,7 @@ describe("ThroughputBucketsComponent", () => {
|
||||
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={oversizedBuckets} />);
|
||||
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(7);
|
||||
expect(screen.getAllByText(/Bucket \d+/)).toHaveLength(7);
|
||||
|
||||
expect(screen.getByDisplayValue("50")).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue("60")).toBeInTheDocument();
|
||||
@@ -171,7 +177,7 @@ describe("ThroughputBucketsComponent", () => {
|
||||
|
||||
it("ensures default buckets are used when no buckets are provided", () => {
|
||||
render(<ThroughputBucketsComponent {...defaultProps} currentBuckets={[]} />);
|
||||
expect(screen.getAllByText(/Group \d+/)).toHaveLength(5);
|
||||
expect(screen.getAllByText(/Bucket \d+/)).toHaveLength(5);
|
||||
expect(screen.getAllByDisplayValue("100")).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -76,7 +76,7 @@ export const ThroughputBucketsComponent: FC<ThroughputBucketsComponentProps> = (
|
||||
value={bucket.maxThroughputPercentage}
|
||||
onChange={(newValue) => handleBucketChange(bucket.id, newValue)}
|
||||
showValue={false}
|
||||
label={`Group ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
|
||||
label={`Bucket ${bucket.id}${bucket.id === 1 ? " (Data Explorer Query Bucket)" : ""}`}
|
||||
styles={{ root: { flex: 2, maxWidth: 400 } }}
|
||||
disabled={bucket.maxThroughputPercentage === 100}
|
||||
/>
|
||||
|
||||
@@ -285,7 +285,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
serverId,
|
||||
numberOfRegions,
|
||||
isMultimaster,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
@@ -559,26 +559,81 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
private getThroughputTextField = (): JSX.Element => (
|
||||
<>
|
||||
{this.props.isAutoPilotSelected ? (
|
||||
<TextField
|
||||
label="Maximum RU/s required by this resource"
|
||||
required
|
||||
type="number"
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
|
||||
disabled={this.overrideWithProvisionedThroughputSettings()}
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
||||
onChange={this.onAutoPilotThroughputChange}
|
||||
min={autoPilotThroughput1K}
|
||||
onGetErrorMessage={(value: string) => {
|
||||
const sanitizedValue = getSanitizedInputValue(value);
|
||||
return sanitizedValue % 1000
|
||||
? "Throughput value must be in increments of 1000"
|
||||
: this.props.throughputError;
|
||||
}}
|
||||
validateOnLoad={false}
|
||||
/>
|
||||
<Stack horizontal verticalAlign="end" tokens={{ childrenGap: 8 }}>
|
||||
{/* Column 1: Minimum RU/s */}
|
||||
<Stack tokens={{ childrenGap: 4 }}>
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
|
||||
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
|
||||
Minimum RU/s
|
||||
</Text>
|
||||
<FontIcon iconName="Info" style={{ fontSize: 12, color: "#666" }} />
|
||||
</Stack>
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: "Segoe UI",
|
||||
width: 70,
|
||||
height: 28,
|
||||
border: "none",
|
||||
fontSize: 14,
|
||||
backgroundColor: "transparent",
|
||||
fontWeight: 400,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
{/* Column 2: "x 10 =" Text */}
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: "Segoe UI",
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
paddingBottom: 6,
|
||||
}}
|
||||
>
|
||||
x 10 =
|
||||
</Text>
|
||||
|
||||
{/* Column 3: Maximum RU/s */}
|
||||
<Stack tokens={{ childrenGap: 4 }}>
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
|
||||
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
|
||||
Maximum RU/s
|
||||
</Text>
|
||||
<FontIcon iconName="Info" style={{ fontSize: 12, color: "#666" }} />
|
||||
</Stack>
|
||||
<TextField
|
||||
required
|
||||
type="number"
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
styles={{
|
||||
...getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline),
|
||||
fieldGroup: { width: 100, height: 28 },
|
||||
field: { fontSize: 14, fontWeight: 400 },
|
||||
}}
|
||||
disabled={this.overrideWithProvisionedThroughputSettings()}
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
value={
|
||||
this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()
|
||||
}
|
||||
onChange={this.onAutoPilotThroughputChange}
|
||||
min={autoPilotThroughput1K}
|
||||
onGetErrorMessage={(value: string) => {
|
||||
const sanitizedValue = getSanitizedInputValue(value);
|
||||
return sanitizedValue % 1000
|
||||
? "Throughput value must be in increments of 1000"
|
||||
: this.props.throughputError;
|
||||
}}
|
||||
validateOnLoad={false}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : (
|
||||
<TextField
|
||||
required
|
||||
|
||||
@@ -157,35 +157,148 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
disabled={true}
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
label="Maximum RU/s required by this resource"
|
||||
min={1000}
|
||||
onChange={[Function]}
|
||||
onGetErrorMessage={[Function]}
|
||||
required={true}
|
||||
step={1000}
|
||||
styles={
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
{
|
||||
"fieldGroup": {
|
||||
"borderColor": "",
|
||||
"height": 25,
|
||||
"selectors": {
|
||||
":disabled": {
|
||||
"backgroundColor": undefined,
|
||||
"borderColor": undefined,
|
||||
},
|
||||
},
|
||||
"width": 300,
|
||||
},
|
||||
"childrenGap": 8,
|
||||
}
|
||||
}
|
||||
type="number"
|
||||
validateOnLoad={false}
|
||||
value=""
|
||||
/>
|
||||
verticalAlign="end"
|
||||
>
|
||||
<Stack
|
||||
tokens={
|
||||
{
|
||||
"childrenGap": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
{
|
||||
"childrenGap": 4,
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"fontWeight": 600,
|
||||
"lineHeight": "20px",
|
||||
}
|
||||
}
|
||||
variant="small"
|
||||
>
|
||||
Minimum RU/s
|
||||
</Text>
|
||||
<FontIcon
|
||||
iconName="Info"
|
||||
style={
|
||||
{
|
||||
"color": "#666",
|
||||
"fontSize": 12,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "transparent",
|
||||
"border": "none",
|
||||
"boxSizing": "border-box",
|
||||
"display": "flex",
|
||||
"fontFamily": "Segoe UI",
|
||||
"fontSize": 14,
|
||||
"fontWeight": 400,
|
||||
"height": 28,
|
||||
"justifyContent": "center",
|
||||
"width": 70,
|
||||
}
|
||||
}
|
||||
>
|
||||
400
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"fontFamily": "Segoe UI",
|
||||
"fontSize": 12,
|
||||
"fontWeight": 400,
|
||||
"paddingBottom": 6,
|
||||
}
|
||||
}
|
||||
>
|
||||
x 10 =
|
||||
</Text>
|
||||
<Stack
|
||||
tokens={
|
||||
{
|
||||
"childrenGap": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
tokens={
|
||||
{
|
||||
"childrenGap": 4,
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
{
|
||||
"fontWeight": 600,
|
||||
"lineHeight": "20px",
|
||||
}
|
||||
}
|
||||
variant="small"
|
||||
>
|
||||
Maximum RU/s
|
||||
</Text>
|
||||
<FontIcon
|
||||
iconName="Info"
|
||||
style={
|
||||
{
|
||||
"color": "#666",
|
||||
"fontSize": 12,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<StyledTextFieldBase
|
||||
disabled={true}
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
min={1000}
|
||||
onChange={[Function]}
|
||||
onGetErrorMessage={[Function]}
|
||||
required={true}
|
||||
step={1000}
|
||||
styles={
|
||||
{
|
||||
"field": {
|
||||
"fontSize": 14,
|
||||
"fontWeight": 400,
|
||||
},
|
||||
"fieldGroup": {
|
||||
"height": 28,
|
||||
"width": 100,
|
||||
},
|
||||
}
|
||||
}
|
||||
type="number"
|
||||
validateOnLoad={false}
|
||||
value=""
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Stack>
|
||||
<Stack
|
||||
|
||||
@@ -198,6 +198,32 @@ exports[`SettingsComponent renders 1`] = `
|
||||
timeToLiveSecondsBaseline={5}
|
||||
/>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Container Policies"
|
||||
itemKey="ContainerVectorPolicyTab"
|
||||
key="ContainerVectorPolicyTab"
|
||||
style={
|
||||
{
|
||||
"marginTop": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ContainerPolicyComponent
|
||||
fullTextPolicy={{}}
|
||||
fullTextPolicyBaseline={{}}
|
||||
isFullTextSearchEnabled={true}
|
||||
isGlobalSecondaryIndex={true}
|
||||
isVectorSearchEnabled={false}
|
||||
onFullTextPolicyChange={[Function]}
|
||||
onFullTextPolicyDirtyChange={[Function]}
|
||||
onVectorEmbeddingPolicyChange={[Function]}
|
||||
onVectorEmbeddingPolicyDirtyChange={[Function]}
|
||||
resetShouldDiscardContainerPolicyChange={[Function]}
|
||||
shouldDiscardContainerPolicies={false}
|
||||
vectorEmbeddingPolicy={{}}
|
||||
vectorEmbeddingPolicyBaseline={{}}
|
||||
/>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Indexing Policy"
|
||||
itemKey="IndexingPolicyTab"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Text } from "@fluentui/react";
|
||||
import { Stack, Text } from "@fluentui/react";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { InfoTooltip } from "../../../../Common/Tooltip/InfoTooltip";
|
||||
import * as SharedConstants from "../../../../Shared/Constants";
|
||||
@@ -44,33 +44,42 @@ export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
|
||||
const currencySign: string = getCurrencySign(serverId);
|
||||
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
|
||||
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier);
|
||||
const estimatedMonthlyCost = "Estimated monthly cost";
|
||||
|
||||
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>{estimatedCostDisclaimer}</InfoTooltip>;
|
||||
const iconWithEstimatedCostDisclaimer: JSX.Element = (
|
||||
<InfoTooltip ariaLabelForTooltip={`${estimatedMonthlyCost} ${currency} ${estimatedCostDisclaimer}`}>
|
||||
{estimatedCostDisclaimer}
|
||||
</InfoTooltip>
|
||||
);
|
||||
|
||||
if (isAutoscale) {
|
||||
return (
|
||||
<Text variant="small">
|
||||
Estimated monthly cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
|
||||
<b>
|
||||
{currencySign + calculateEstimateNumber(monthlyPrice / 10)} -{" "}
|
||||
{currencySign + calculateEstimateNumber(monthlyPrice)}{" "}
|
||||
</b>
|
||||
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits / 10} - {requestUnits}{" "}
|
||||
RU/s, {currencySign + pricePerRu}/RU)
|
||||
</Text>
|
||||
<Stack style={{ marginBottom: 6 }}>
|
||||
<Text variant="small">
|
||||
{estimatedMonthlyCost} ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
|
||||
<b>
|
||||
{currencySign + calculateEstimateNumber(monthlyPrice / 10)} -{" "}
|
||||
{currencySign + calculateEstimateNumber(monthlyPrice)}{" "}
|
||||
</b>
|
||||
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits / 10} - {requestUnits}{" "}
|
||||
RU/s, {currencySign + pricePerRu}/RU)
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text variant="small">
|
||||
Estimated cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
|
||||
<b>
|
||||
{currencySign + calculateEstimateNumber(hourlyPrice)} hourly /{" "}
|
||||
{currencySign + calculateEstimateNumber(dailyPrice)} daily /{" "}
|
||||
{currencySign + calculateEstimateNumber(monthlyPrice)} monthly{" "}
|
||||
</b>
|
||||
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "}
|
||||
{currencySign + pricePerRu}/RU)
|
||||
</Text>
|
||||
<Stack style={{ marginBottom: 8 }}>
|
||||
<Text variant="small">
|
||||
Estimated cost ({currency}){iconWithEstimatedCostDisclaimer}:{" "}
|
||||
<b>
|
||||
{currencySign + calculateEstimateNumber(hourlyPrice)} hourly /{" "}
|
||||
{currencySign + calculateEstimateNumber(dailyPrice)} daily /{" "}
|
||||
{currencySign + calculateEstimateNumber(monthlyPrice)} monthly{" "}
|
||||
</b>
|
||||
({numberOfRegions + (numberOfRegions === 1 ? " region" : " regions")}, {requestUnits}RU/s,{" "}
|
||||
{currencySign + pricePerRu}/RU)
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Checkbox, DirectionalHint, Link, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
|
||||
import { Checkbox, DirectionalHint, Link, Separator, Stack, Text, TextField, TooltipHost } from "@fluentui/react";
|
||||
import { getWorkloadType } from "Common/DatabaseAccountUtility";
|
||||
import { CostEstimateText } from "Explorer/Controls/ThroughputInput/CostEstimateText/CostEstimateText";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
||||
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
|
||||
import * as SharedConstants from "../../../Shared/Constants";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { getCollectionName } from "../../../Utils/APITypeUtils";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import * as PricingUtils from "../../../Utils/PricingUtils";
|
||||
import { CostEstimateText } from "./CostEstimateText/CostEstimateText";
|
||||
import "./ThroughputInput.less";
|
||||
|
||||
export interface ThroughputInputProps {
|
||||
@@ -40,7 +41,9 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
let defaultThroughput: number;
|
||||
const workloadType: Constants.WorkloadType = getWorkloadType();
|
||||
|
||||
if (
|
||||
if (isFabricNative()) {
|
||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput5K;
|
||||
} else if (
|
||||
isFreeTier ||
|
||||
isQuickstart ||
|
||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
|
||||
@@ -230,53 +233,92 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{isAutoscaleSelected && (
|
||||
<Stack className="throughputInputSpacing">
|
||||
<Text variant="small" aria-label="capacity calculator of azure cosmos db">
|
||||
Estimate your required RU/s with{" "}
|
||||
<Link
|
||||
className="underlinedLink outlineNone"
|
||||
target="_blank"
|
||||
href="https://cosmos.azure.com/capacitycalculator/"
|
||||
aria-label="capacity calculator of azure cosmos db"
|
||||
>
|
||||
capacity calculator
|
||||
</Link>
|
||||
.
|
||||
<Text style={{ marginTop: -2, fontSize: 12 }}>
|
||||
Your container throughput will automatically scale up to the maximum value you select, from a minimum of 10%
|
||||
of that value.
|
||||
</Text>
|
||||
<Stack horizontal verticalAlign="end" tokens={{ childrenGap: 8 }}>
|
||||
<Stack tokens={{ childrenGap: 4 }}>
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
|
||||
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
|
||||
Minimum RU/s
|
||||
</Text>
|
||||
<InfoTooltip>The minimum RU/s your container will scale to</InfoTooltip>
|
||||
</Stack>
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: "Segoe UI",
|
||||
width: 70,
|
||||
height: 27,
|
||||
border: "none",
|
||||
fontSize: 14,
|
||||
backgroundColor: "transparent",
|
||||
fontWeight: 400,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{Math.round(throughput / 10).toString()}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal>
|
||||
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }} aria-label="maxRUDescription">
|
||||
{isDatabase ? "Database" : getCollectionName()} Max RU/s
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: "Segoe UI",
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
paddingBottom: 6,
|
||||
}}
|
||||
>
|
||||
x 10 =
|
||||
</Text>
|
||||
<InfoTooltip>{getAutoScaleTooltip()}</InfoTooltip>
|
||||
|
||||
<Stack tokens={{ childrenGap: 4 }}>
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
|
||||
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
|
||||
Maximum RU/s
|
||||
</Text>
|
||||
<InfoTooltip>{getAutoScaleTooltip()}</InfoTooltip>
|
||||
</Stack>
|
||||
<TextField
|
||||
id="autoscaleRUValueField"
|
||||
type="number"
|
||||
styles={{
|
||||
fieldGroup: { width: 100, height: 27, flexShrink: 0 },
|
||||
field: { fontSize: 14, fontWeight: 400 },
|
||||
}}
|
||||
onChange={(_event, newInput?: string) => onThroughputValueChange(newInput)}
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
min={AutoPilotUtils.autoPilotThroughput1K}
|
||||
max={isSharded ? Number.MAX_SAFE_INTEGER.toString() : "10000"}
|
||||
value={throughput.toString()}
|
||||
ariaLabel={`${isDatabase ? "Database" : getCollectionName()} max RU/s`}
|
||||
required={true}
|
||||
errorMessage={throughputError}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<TextField
|
||||
id="autoscaleRUValueField"
|
||||
type="number"
|
||||
styles={{
|
||||
fieldGroup: { width: 300, height: 27 },
|
||||
field: { fontSize: 12 },
|
||||
}}
|
||||
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
min={AutoPilotUtils.autoPilotThroughput1K}
|
||||
max={isSharded ? Number.MAX_SAFE_INTEGER.toString() : "10000"}
|
||||
value={throughput.toString()}
|
||||
ariaLabel={`${isDatabase ? "Database" : getCollectionName()} max RU/s`}
|
||||
required={true}
|
||||
errorMessage={throughputError}
|
||||
/>
|
||||
|
||||
<Text variant="small">
|
||||
Your {isDatabase ? "database" : getCollectionName().toLocaleLowerCase()} throughput will automatically scale
|
||||
from{" "}
|
||||
<b>
|
||||
{AutoPilotUtils.getMinRUsBasedOnUserInput(throughput)} RU/s (10% of max RU/s) - {throughput} RU/s
|
||||
</b>{" "}
|
||||
based on usage.
|
||||
</Text>
|
||||
<CostEstimateText requestUnits={throughput} isAutoscale={isAutoscaleSelected} />
|
||||
<Stack className="throughputInputSpacing">
|
||||
<Text variant="small" aria-label="ruDescription">
|
||||
Estimate your required RU/s with
|
||||
<Link
|
||||
className="underlinedLink"
|
||||
target="_blank"
|
||||
href="https://cosmos.azure.com/capacitycalculator/"
|
||||
aria-label="Capacity calculator"
|
||||
>
|
||||
capacity calculator
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</Stack>
|
||||
<Separator className="panelSeparator" style={{ paddingTop: -8, paddingBottom: -8 }} />
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
@@ -300,7 +342,6 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
</Text>
|
||||
<InfoTooltip>{getAutoScaleTooltip()}</InfoTooltip>
|
||||
</Stack>
|
||||
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.topLeftEdge}
|
||||
content={
|
||||
@@ -325,11 +366,10 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
errorMessage={throughputError}
|
||||
/>
|
||||
</TooltipHost>
|
||||
<CostEstimateText requestUnits={throughput} isAutoscale={isAutoscaleSelected} />
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<CostEstimateText requestUnits={throughput} isAutoscale={isAutoscaleSelected} />
|
||||
|
||||
{throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && (
|
||||
<Stack horizontal verticalAlign="start">
|
||||
<Checkbox
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
||||
import { createCollection } from "../../Common/dataAccess/createCollection";
|
||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||
import { createDocument as createMongoDocument } from "../../Common/MongoProxyClient";
|
||||
@@ -90,12 +91,13 @@ export class ContainerSampleGenerator {
|
||||
}
|
||||
const { databaseAccount: account } = userContext;
|
||||
const databaseId = collection.databaseId;
|
||||
|
||||
const gremlinClient = new GremlinClient();
|
||||
gremlinClient.initialize({
|
||||
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
|
||||
databaseId: databaseId,
|
||||
collectionId: collection.id(),
|
||||
masterKey: userContext.masterKey || "",
|
||||
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
|
||||
maxResultSize: 100,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,16 +8,22 @@ import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||
import { IGalleryItem } from "Juno/JunoClient";
|
||||
import { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
|
||||
import {
|
||||
isFabricMirrored,
|
||||
isFabricMirroredKey,
|
||||
isFabricNative,
|
||||
scheduleRefreshFabricToken,
|
||||
} from "Platform/Fabric/FabricUtil";
|
||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
||||
import { featureRegistered } from "Utils/FeatureRegistrationUtils";
|
||||
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import _ from "underscore";
|
||||
import shallow from "zustand/shallow";
|
||||
import { shallow } from "zustand/shallow";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||
import * as Constants from "../Common/Constants";
|
||||
@@ -30,6 +36,7 @@ import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { ContainerConnectionInfo, IPhoenixServiceInfo, IProvisionData, IResponse } from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { UploadDetailsRecord } from "../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { PhoenixClient } from "../Phoenix/PhoenixClient";
|
||||
import * as ExplorerSettings from "../Shared/ExplorerSettings";
|
||||
@@ -105,8 +112,8 @@ export default class Explorer {
|
||||
|
||||
this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
|
||||
useNotebook.subscribe(
|
||||
() => this.refreshCommandBarButtons(),
|
||||
(state) => state.isNotebooksEnabledForAccount,
|
||||
() => this.refreshCommandBarButtons(),
|
||||
);
|
||||
|
||||
this.queriesClient = new QueriesClient(this);
|
||||
@@ -129,13 +136,13 @@ export default class Explorer {
|
||||
});
|
||||
|
||||
useTabs.subscribe(
|
||||
(state) => state.openedTabs,
|
||||
(openedTabs: TabsBase[]) => {
|
||||
if (openedTabs.length === 0) {
|
||||
useSelectedNode.getState().setSelectedNode(undefined);
|
||||
useCommandBar.getState().setContextButtons([]);
|
||||
}
|
||||
},
|
||||
(state) => state.openedTabs,
|
||||
}
|
||||
);
|
||||
|
||||
this.isTabsContentExpanded = ko.observable(false);
|
||||
@@ -163,9 +170,9 @@ export default class Explorer {
|
||||
);
|
||||
|
||||
useNotebook.subscribe(
|
||||
async () => this.initiateAndRefreshNotebookList(),
|
||||
(state) => [state.isNotebookEnabled, state.isRefreshed],
|
||||
shallow,
|
||||
async () => this.initiateAndRefreshNotebookList(),
|
||||
{ equalityFn: shallow },
|
||||
);
|
||||
|
||||
this.resourceTree = new ResourceTreeAdapter(this);
|
||||
@@ -282,45 +289,40 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
public openInVsCode(): void {
|
||||
TelemetryProcessor.traceStart(Action.OpenVSCode);
|
||||
this.openVsCodeButtonClick();
|
||||
/**
|
||||
* Generates a VS Code DocumentDB connection URL using the current user's MongoDB connection parameters.
|
||||
* Double-encodes the updated connection string for safe usage in VS Code URLs.
|
||||
*
|
||||
* The DocumentDB VS Code extension requires double encoding for connection strings.
|
||||
* See: https://microsoft.github.io/vscode-documentdb/manual/how-to-construct-url.html#double-encoding
|
||||
*
|
||||
* @returns {string} The encoded VS Code DocumentDB connection URL.
|
||||
*/
|
||||
private getDocumentDbUrl() {
|
||||
const { adminLogin: adminLoginuserName = "", connectionString = "" } = userContext.vcoreMongoConnectionParams;
|
||||
const updatedConnectionString = connectionString.replace(/<(user|username)>:<password>/i, adminLoginuserName);
|
||||
const encodedUpdatedConnectionString = encodeURIComponent(encodeURIComponent(updatedConnectionString));
|
||||
const documentDbUrl = `vscode://ms-azuretools.vscode-documentdb?connectionString=${encodedUpdatedConnectionString}`;
|
||||
return documentDbUrl;
|
||||
}
|
||||
|
||||
private openVsCodeButtonClick(): void {
|
||||
private getCosmosDbUrl() {
|
||||
const activeTab = useTabs.getState().activeTab;
|
||||
const resourceId = encodeURIComponent(userContext.databaseAccount.id);
|
||||
const database = encodeURIComponent(activeTab?.collection?.databaseId);
|
||||
const container = encodeURIComponent(activeTab?.collection?.id());
|
||||
const baseUrl = `vscod://ms-azuretools.vscode-cosmosdb?resourceId=${resourceId}`;
|
||||
const baseUrl = `vscode://ms-azuretools.vscode-cosmosdb?resourceId=${resourceId}`;
|
||||
const vscodeUrl = activeTab ? `${baseUrl}&database=${database}&container=${container}` : baseUrl;
|
||||
const startTime = Date.now();
|
||||
let vsCodeNotOpened = false;
|
||||
return vscodeUrl;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const timeOutTime = Date.now() - startTime;
|
||||
if (!vsCodeNotOpened && timeOutTime < 1050) {
|
||||
vsCodeNotOpened = true;
|
||||
useDialog.getState().openDialog(openVSCodeDialogProps);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = vscodeUrl;
|
||||
link.rel = "noopener noreferrer";
|
||||
document.body.appendChild(link);
|
||||
|
||||
try {
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
TelemetryProcessor.traceStart(Action.OpenVSCode);
|
||||
} catch (error) {
|
||||
if (!vsCodeNotOpened) {
|
||||
vsCodeNotOpened = true;
|
||||
logConsoleError(`Failed to open VS Code: ${getErrorMessage(error)}`);
|
||||
}
|
||||
}
|
||||
private getVSCodeUrl(): string {
|
||||
const isvCore = (userContext.apiType || userContext.databaseAccount.kind) === "VCoreMongo";
|
||||
return isvCore ? this.getDocumentDbUrl() : this.getCosmosDbUrl();
|
||||
}
|
||||
|
||||
public openInVsCode(): void {
|
||||
const vscodeUrl = this.getVSCodeUrl();
|
||||
const openVSCodeDialogProps: DialogProps = {
|
||||
linkProps: {
|
||||
linkText: "Download Visual Studio Code",
|
||||
@@ -334,15 +336,19 @@ export default class Explorer {
|
||||
secondaryButtonText: "Cancel",
|
||||
|
||||
onPrimaryButtonClick: () => {
|
||||
vsCodeNotOpened = false;
|
||||
this.openVsCodeButtonClick();
|
||||
useDialog.getState().closeDialog();
|
||||
try {
|
||||
window.location.href = vscodeUrl;
|
||||
TelemetryProcessor.traceStart(Action.OpenVSCode);
|
||||
} catch (error) {
|
||||
logConsoleError(`Failed to open VS Code: ${getErrorMessage(error)}`);
|
||||
}
|
||||
},
|
||||
onSecondaryButtonClick: () => {
|
||||
useDialog.getState().closeDialog();
|
||||
TelemetryProcessor.traceCancel(Action.OpenVSCode);
|
||||
},
|
||||
};
|
||||
useDialog.getState().openDialog(openVSCodeDialogProps);
|
||||
}
|
||||
|
||||
public async openCESCVAFeedbackBlade(): Promise<void> {
|
||||
@@ -1141,8 +1147,8 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
public openUploadItemsPane(): void {
|
||||
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
|
||||
public openUploadItemsPane(onUpload?: (data: UploadDetailsRecord[]) => void): void {
|
||||
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane onUpload={onUpload} />);
|
||||
}
|
||||
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
|
||||
useSidePanel
|
||||
@@ -1150,7 +1156,7 @@ export default class Explorer {
|
||||
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
||||
}
|
||||
|
||||
public getDownloadModalConent(fileName: string): JSX.Element {
|
||||
public getDownloadModalContent(fileName: string): JSX.Element {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
return (
|
||||
<>
|
||||
@@ -1174,7 +1180,10 @@ export default class Explorer {
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow
|
||||
}
|
||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||
|
||||
if (!isFabricNative()) {
|
||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||
}
|
||||
|
||||
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||
const isNotebookEnabled =
|
||||
@@ -1196,6 +1205,11 @@ export default class Explorer {
|
||||
await this.initNotebooks(userContext.databaseAccount);
|
||||
}
|
||||
|
||||
if (userContext.authType === AuthType.AAD && userContext.apiType === "SQL" && !isFabricNative()) {
|
||||
const throughputBucketsEnabled = await featureRegistered(userContext.subscriptionId, "ThroughputBucketing");
|
||||
updateUserContext({ throughputBucketsEnabled });
|
||||
}
|
||||
|
||||
this.refreshSampleData();
|
||||
}
|
||||
|
||||
|
||||
@@ -163,8 +163,7 @@ describe("GraphExplorer", () => {
|
||||
graphBackendEndpoint: "graphBackendEndpoint",
|
||||
databaseId: "databaseId",
|
||||
collectionId: "collectionId",
|
||||
masterKey: "masterKey",
|
||||
|
||||
password: "password",
|
||||
onLoadStartKey: 0,
|
||||
onLoadStartKeyChange: (newKey: number): void => {},
|
||||
resourceId: "resourceId",
|
||||
|
||||
@@ -16,7 +16,12 @@ import * as StorageUtility from "../../../Shared/StorageUtility";
|
||||
import { LocalStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
|
||||
import {
|
||||
logConsoleError,
|
||||
logConsoleInfo,
|
||||
logConsoleProgress,
|
||||
logConsoleWarning,
|
||||
} from "../../../Utils/NotificationConsoleUtils";
|
||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
|
||||
import * as TabComponent from "../../Controls/Tabs/TabComponent";
|
||||
@@ -54,7 +59,7 @@ export interface GraphExplorerProps {
|
||||
graphBackendEndpoint: string;
|
||||
databaseId: string;
|
||||
collectionId: string;
|
||||
masterKey: string;
|
||||
password: string;
|
||||
|
||||
onLoadStartKey: number;
|
||||
onLoadStartKeyChange: (newKey: number) => void;
|
||||
@@ -1083,6 +1088,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
public static reportToConsole(type: ConsoleDataType.InProgress, msg: string, ...errorData: any[]): () => void;
|
||||
public static reportToConsole(type: ConsoleDataType.Info, msg: string, ...errorData: any[]): void;
|
||||
public static reportToConsole(type: ConsoleDataType.Error, msg: string, ...errorData: any[]): void;
|
||||
public static reportToConsole(type: ConsoleDataType.Warning, msg: string, ...errorData: any[]): void;
|
||||
public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) {
|
||||
let errorDataStr = "";
|
||||
if (errorData && errorData.length > 0) {
|
||||
@@ -1099,6 +1105,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
return logConsoleInfo(consoleMessage);
|
||||
case ConsoleDataType.InProgress:
|
||||
return logConsoleProgress(consoleMessage);
|
||||
case ConsoleDataType.Warning:
|
||||
return logConsoleWarning(consoleMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1292,7 +1300,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
endpoint: `wss://${this.props.graphBackendEndpoint}`,
|
||||
databaseId: this.props.databaseId,
|
||||
collectionId: this.props.collectionId,
|
||||
masterKey: this.props.masterKey,
|
||||
password: this.props.password,
|
||||
maxResultSize: GraphExplorer.MAX_RESULT_SIZE,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,28 +8,28 @@ describe("Gremlin Client", () => {
|
||||
endpoint: null,
|
||||
collectionId: null,
|
||||
databaseId: null,
|
||||
masterKey: null,
|
||||
maxResultSize: 10000,
|
||||
password: null,
|
||||
};
|
||||
|
||||
it("should use databaseId, collectionId and masterKey to authenticate", () => {
|
||||
it("should use databaseId, collectionId and password to authenticate", () => {
|
||||
const collectionId = "collectionId";
|
||||
const databaseId = "databaseId";
|
||||
const masterKey = "masterKey";
|
||||
const testPassword = "password";
|
||||
const gremlinClient = new GremlinClient();
|
||||
|
||||
gremlinClient.initialize({
|
||||
endpoint: null,
|
||||
collectionId,
|
||||
databaseId,
|
||||
masterKey,
|
||||
maxResultSize: 0,
|
||||
password: testPassword,
|
||||
});
|
||||
|
||||
// User must includes these values
|
||||
expect(gremlinClient.client.params.user.indexOf(collectionId)).not.toBe(-1);
|
||||
expect(gremlinClient.client.params.user.indexOf(databaseId)).not.toBe(-1);
|
||||
expect(gremlinClient.client.params.password).toEqual(masterKey);
|
||||
expect(gremlinClient.client.params.password).toEqual(testPassword);
|
||||
});
|
||||
|
||||
it("should aggregate RU charges across multiple responses", (done) => {
|
||||
|
||||
@@ -11,8 +11,8 @@ export interface GremlinClientParameters {
|
||||
endpoint: string;
|
||||
databaseId: string;
|
||||
collectionId: string;
|
||||
masterKey: string;
|
||||
maxResultSize: number;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface GremlinRequestResult {
|
||||
@@ -43,7 +43,7 @@ export class GremlinClient {
|
||||
this.client = new GremlinSimpleClient({
|
||||
endpoint: params.endpoint,
|
||||
user: `/dbs/${params.databaseId}/colls/${params.collectionId}`,
|
||||
password: params.masterKey,
|
||||
password: params.password,
|
||||
successCallback: (result: Result) => {
|
||||
this.storePendingResult(result);
|
||||
this.flushResult(result.requestId);
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
import * as sinon from "sinon";
|
||||
import {
|
||||
GremlinRequestMessage,
|
||||
GremlinResponseMessage,
|
||||
GremlinSimpleClient,
|
||||
GremlinSimpleClientParameters,
|
||||
Result,
|
||||
GremlinRequestMessage,
|
||||
GremlinResponseMessage,
|
||||
} from "./GremlinSimpleClient";
|
||||
|
||||
describe("Gremlin Simple Client", () => {
|
||||
|
||||
@@ -95,3 +95,10 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.newVertexComponent {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,12 @@
|
||||
*/
|
||||
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||
import { userContext } from "UserContext";
|
||||
import * as React from "react";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { create } from "zustand";
|
||||
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
@@ -29,8 +30,8 @@ export interface CommandBarStore {
|
||||
setIsHidden: (isHidden: boolean) => void;
|
||||
}
|
||||
|
||||
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
||||
contextButtons: [],
|
||||
export const useCommandBar = create<CommandBarStore>((set) => ({
|
||||
contextButtons: [] as CommandButtonComponentProps[],
|
||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
||||
isHidden: false,
|
||||
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
||||
@@ -43,6 +44,15 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
const backgroundColor = StyleConstants.BaseLight;
|
||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
||||
|
||||
// Subscribe to the store changes that affect button creation
|
||||
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||
|
||||
// Memoize the expensive button creation
|
||||
const staticButtons = React.useMemo(() => {
|
||||
return CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
|
||||
}, [container, selectedNodeState, dataPlaneRbacEnabled, aadTokenUpdated]);
|
||||
|
||||
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||
const buttons =
|
||||
userContext.apiType === "Postgres"
|
||||
@@ -62,7 +72,6 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
|
||||
const contextButtons = (buttons || []).concat(
|
||||
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { KeyboardAction } from "KeyboardShortcuts";
|
||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||
import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
||||
import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
|
||||
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
|
||||
@@ -68,15 +67,7 @@ export function createStaticCommandBarButtons(
|
||||
}
|
||||
|
||||
if (isDataplaneRbacSupported(userContext.apiType)) {
|
||||
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
|
||||
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
||||
|
||||
useEffect(() => {
|
||||
const buttonProps = createLoginForEntraIDButton(container);
|
||||
setLoginButtonProps(buttonProps);
|
||||
}, [dataPlaneRbacEnabled, aadTokenUpdated, container]);
|
||||
|
||||
const loginButtonProps = createLoginForEntraIDButton(container);
|
||||
if (loginButtonProps) {
|
||||
addDivider();
|
||||
buttons.push(loginButtonProps);
|
||||
|
||||
@@ -13,4 +13,5 @@ export enum ConsoleDataType {
|
||||
Info = 0,
|
||||
Error = 1,
|
||||
InProgress = 2,
|
||||
Warning = 3,
|
||||
}
|
||||
|
||||
@@ -173,8 +173,20 @@
|
||||
.message {
|
||||
flex-grow: 1;
|
||||
white-space:pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.notificationConsoleContents {
|
||||
overflow-y: auto;
|
||||
|
||||
.notificationConsoleData {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import ErrorRedIcon from "../../../../images/error_red.svg";
|
||||
import infoBubbleIcon from "../../../../images/info-bubble-9x9.svg";
|
||||
import InfoIcon from "../../../../images/info_color.svg";
|
||||
import LoadingIcon from "../../../../images/loading.svg";
|
||||
import WarningIcon from "../../../../images/warning.svg";
|
||||
import { ClientDefaults, KeyCodes } from "../../../Common/Constants";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
|
||||
@@ -91,6 +92,9 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
const numInfoItems = this.state.allConsoleData.filter(
|
||||
(data: ConsoleData) => data.type === ConsoleDataType.Info,
|
||||
).length;
|
||||
const numWarningItems = this.state.allConsoleData.filter(
|
||||
(data: ConsoleData) => data.type === ConsoleDataType.Warning,
|
||||
).length;
|
||||
|
||||
return (
|
||||
<div className="notificationConsoleContainer">
|
||||
@@ -118,6 +122,10 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
<img src={infoBubbleIcon} alt="Info items" />
|
||||
<span className="numInfoItems">{numInfoItems}</span>
|
||||
</span>
|
||||
<span className="notificationConsoleHeaderIconWithData">
|
||||
<img src={WarningIcon} alt="Warning items" />
|
||||
<span className="numWarningItems">{numWarningItems}</span>
|
||||
</span>
|
||||
</span>
|
||||
{userContext.features.pr && <PrPreview pr={userContext.features.pr} />}
|
||||
<span className="consoleSplitter" />
|
||||
@@ -198,6 +206,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
{item.type === ConsoleDataType.Info && <img className="infoIcon" src={InfoIcon} alt="info" />}
|
||||
{item.type === ConsoleDataType.Error && <img className="errorIcon" src={ErrorRedIcon} alt="error" />}
|
||||
{item.type === ConsoleDataType.InProgress && <img className="loaderIcon" src={LoaderIcon} alt="in progress" />}
|
||||
{item.type === ConsoleDataType.Warning && <img className="warningIcon" src={WarningIcon} alt="warning" />}
|
||||
<span className="date">{item.date}</span>
|
||||
<span className="message" role="alert" aria-live="assertive">
|
||||
{item.message}
|
||||
|
||||
@@ -59,6 +59,19 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
0
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="Warning items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
className="numWarningItems"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="consoleSplitter"
|
||||
@@ -229,6 +242,19 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
1
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="notificationConsoleHeaderIconWithData"
|
||||
>
|
||||
<img
|
||||
alt="Warning items"
|
||||
src={{}}
|
||||
/>
|
||||
<span
|
||||
className="numWarningItems"
|
||||
>
|
||||
0
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className="consoleSplitter"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AppState, ContentRef, selectors } from "@nteract/core";
|
||||
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
@@ -59,7 +59,7 @@ export class StatusBar extends React.Component<Props> {
|
||||
<Bar data-test="notebookStatusBar">
|
||||
<RightStatus>
|
||||
{this.props.lastSaved ? (
|
||||
<p data-test="saveStatus"> Last saved {distanceInWordsToNow(this.props.lastSaved)} </p>
|
||||
<p data-test="saveStatus"> Last saved {formatDistanceToNow(this.props.lastSaved)} ago </p>
|
||||
) : (
|
||||
<p> Not saved yet </p>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||
import { cloneDeep } from "lodash";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { create } from "zustand";
|
||||
import { subscribeWithSelector } from 'zustand/middleware';
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConnectionStatusType, HttpStatusCodes } from "../../Common/Constants";
|
||||
@@ -66,270 +67,274 @@ interface NotebookState {
|
||||
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
|
||||
}
|
||||
|
||||
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
isNotebookEnabled: false,
|
||||
isNotebooksEnabledForAccount: false,
|
||||
notebookServerInfo: {
|
||||
notebookServerEndpoint: undefined,
|
||||
authToken: undefined,
|
||||
forwardingId: undefined,
|
||||
},
|
||||
sparkClusterConnectionInfo: {
|
||||
userName: undefined,
|
||||
password: undefined,
|
||||
endpoints: [],
|
||||
},
|
||||
isSynapseLinkUpdating: false,
|
||||
memoryUsageInfo: undefined,
|
||||
isShellEnabled: false,
|
||||
notebookBasePath: Constants.Notebook.defaultBasePath,
|
||||
isInitializingNotebooks: false,
|
||||
myNotebooksContentRoot: undefined,
|
||||
gitHubNotebooksContentRoot: undefined,
|
||||
galleryContentRoot: undefined,
|
||||
connectionInfo: {
|
||||
status: ConnectionStatusType.Connect,
|
||||
},
|
||||
notebookFolderName: undefined,
|
||||
isAllocating: false,
|
||||
isRefreshed: false,
|
||||
containerStatus: {
|
||||
status: undefined,
|
||||
durationLeftInMinutes: undefined,
|
||||
phoenixServerInfo: undefined,
|
||||
},
|
||||
isPhoenixNotebooks: undefined,
|
||||
isPhoenixFeatures: undefined,
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
||||
set({ notebookServerInfo }),
|
||||
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) =>
|
||||
set({ sparkClusterConnectionInfo }),
|
||||
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
|
||||
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
|
||||
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
|
||||
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
|
||||
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
|
||||
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
|
||||
await get().getPhoenixStatus();
|
||||
const { databaseAccount, authType } = userContext;
|
||||
if (
|
||||
authType === AuthType.EncryptedToken ||
|
||||
authType === AuthType.ResourceToken ||
|
||||
authType === AuthType.MasterKey
|
||||
) {
|
||||
set({ isNotebooksEnabledForAccount: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const firstWriteLocation =
|
||||
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
|
||||
? databaseAccount?.location
|
||||
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
||||
const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`;
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
try {
|
||||
const response = await fetch(disallowedLocationsUri, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
resourceTypes: [Constants.ArmResourceTypes.notebookWorkspaces],
|
||||
}),
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[Constants.HttpHeaders.contentType]: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch disallowed locations");
|
||||
}
|
||||
|
||||
const disallowedLocations: string[] = await response.json();
|
||||
if (!disallowedLocations) {
|
||||
Logger.logInfo("No disallowed locations found", "Explorer/isNotebooksEnabledForAccount");
|
||||
set({ isNotebooksEnabledForAccount: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// firstWriteLocation should not be disallowed
|
||||
const isAccountInAllowedLocation = firstWriteLocation && disallowedLocations.indexOf(firstWriteLocation) === -1;
|
||||
set({ isNotebooksEnabledForAccount: isAccountInAllowedLocation });
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
|
||||
set({ isNotebooksEnabledForAccount: false });
|
||||
}
|
||||
},
|
||||
findItem: (root: NotebookContentItem, item: NotebookContentItem): NotebookContentItem => {
|
||||
const currentItem = root || get().myNotebooksContentRoot;
|
||||
|
||||
if (currentItem) {
|
||||
if (currentItem.path === item.path && currentItem.name === item.name) {
|
||||
return currentItem;
|
||||
}
|
||||
|
||||
if (currentItem.children) {
|
||||
for (const childItem of currentItem.children) {
|
||||
const result = get().findItem(childItem, item);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
export const useNotebook = create<NotebookState>()(
|
||||
subscribeWithSelector(
|
||||
(set, get) => ({
|
||||
isNotebookEnabled: false,
|
||||
isNotebooksEnabledForAccount: false,
|
||||
notebookServerInfo: {
|
||||
notebookServerEndpoint: "",
|
||||
authToken: "",
|
||||
forwardingId: "",
|
||||
},
|
||||
sparkClusterConnectionInfo: {
|
||||
userName: "",
|
||||
password: "",
|
||||
endpoints: [] as DataModels.SparkClusterEndpoint[],
|
||||
},
|
||||
isSynapseLinkUpdating: false,
|
||||
memoryUsageInfo: undefined as DataModels.MemoryUsageInfo,
|
||||
isShellEnabled: false,
|
||||
notebookBasePath: Constants.Notebook.defaultBasePath,
|
||||
isInitializingNotebooks: false,
|
||||
myNotebooksContentRoot: undefined as NotebookContentItem,
|
||||
gitHubNotebooksContentRoot: undefined as NotebookContentItem,
|
||||
galleryContentRoot: undefined as NotebookContentItem,
|
||||
connectionInfo: {
|
||||
status: ConnectionStatusType.Connect,
|
||||
},
|
||||
notebookFolderName: "",
|
||||
isAllocating: false,
|
||||
isRefreshed: false,
|
||||
containerStatus: {
|
||||
status: undefined,
|
||||
durationLeftInMinutes: undefined,
|
||||
phoenixServerInfo: undefined,
|
||||
} as ContainerInfo,
|
||||
isPhoenixNotebooks: undefined as boolean,
|
||||
isPhoenixFeatures: undefined as boolean,
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
||||
set({ notebookServerInfo }),
|
||||
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) =>
|
||||
set({ sparkClusterConnectionInfo }),
|
||||
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
|
||||
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
|
||||
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
|
||||
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
|
||||
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
|
||||
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
|
||||
await get().getPhoenixStatus();
|
||||
const { databaseAccount, authType } = userContext;
|
||||
if (
|
||||
authType === AuthType.EncryptedToken ||
|
||||
authType === AuthType.ResourceToken ||
|
||||
authType === AuthType.MasterKey
|
||||
) {
|
||||
set({ isNotebooksEnabledForAccount: false });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => {
|
||||
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
|
||||
const parentItem = get().findItem(root, parent);
|
||||
item.parent = parentItem;
|
||||
if (parentItem.children) {
|
||||
parentItem.children.push(item);
|
||||
} else {
|
||||
parentItem.children = [item];
|
||||
}
|
||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||
},
|
||||
updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
|
||||
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
|
||||
const parentItem = get().findItem(root, item.parent);
|
||||
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
|
||||
parentItem.children.push(item);
|
||||
item.parent = parentItem;
|
||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||
},
|
||||
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
|
||||
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
|
||||
const parentItem = get().findItem(root, item.parent);
|
||||
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
|
||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||
},
|
||||
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
|
||||
const notebookFolderName = get().isPhoenixNotebooks ? "Temporary Notebooks" : "My Notebooks";
|
||||
set({ notebookFolderName });
|
||||
const myNotebooksContentRoot = {
|
||||
name: get().notebookFolderName,
|
||||
path: get().notebookBasePath,
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
const galleryContentRoot = {
|
||||
name: "Gallery",
|
||||
path: "Gallery",
|
||||
type: NotebookContentItemType.File,
|
||||
};
|
||||
const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn()
|
||||
? {
|
||||
name: "GitHub repos",
|
||||
path: "PsuedoDir",
|
||||
type: NotebookContentItemType.Directory,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
set({
|
||||
myNotebooksContentRoot,
|
||||
galleryContentRoot,
|
||||
gitHubNotebooksContentRoot,
|
||||
});
|
||||
|
||||
if (get().notebookServerInfo?.notebookServerEndpoint) {
|
||||
const updatedRoot = await notebookManager?.notebookContentClient?.updateItemChildren(myNotebooksContentRoot);
|
||||
set({ myNotebooksContentRoot: updatedRoot });
|
||||
|
||||
if (updatedRoot?.children) {
|
||||
// Count 1st generation children (tree is lazy-loaded)
|
||||
const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
|
||||
updatedRoot.children.forEach((notebookItem) => {
|
||||
switch (notebookItem.type) {
|
||||
case NotebookContentItemType.File:
|
||||
nodeCounts.files++;
|
||||
break;
|
||||
case NotebookContentItemType.Directory:
|
||||
nodeCounts.directories++;
|
||||
break;
|
||||
case NotebookContentItemType.Notebook:
|
||||
nodeCounts.notebooks++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts });
|
||||
}
|
||||
}
|
||||
},
|
||||
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]): void => {
|
||||
const gitHubNotebooksContentRoot = cloneDeep(get().gitHubNotebooksContentRoot);
|
||||
if (gitHubNotebooksContentRoot) {
|
||||
gitHubNotebooksContentRoot.children = [];
|
||||
pinnedRepos?.forEach((pinnedRepo) => {
|
||||
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
|
||||
const repoTreeItem: NotebookContentItem = {
|
||||
name: repoFullName,
|
||||
path: "PsuedoDir",
|
||||
type: NotebookContentItemType.Directory,
|
||||
children: [],
|
||||
parent: gitHubNotebooksContentRoot,
|
||||
};
|
||||
|
||||
pinnedRepo.branches.forEach((branch) => {
|
||||
repoTreeItem.children.push({
|
||||
name: branch.name,
|
||||
path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""),
|
||||
type: NotebookContentItemType.Directory,
|
||||
parent: repoTreeItem,
|
||||
const firstWriteLocation =
|
||||
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
|
||||
? databaseAccount?.location
|
||||
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
||||
const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`;
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
try {
|
||||
const response = await fetch(disallowedLocationsUri, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
resourceTypes: [Constants.ArmResourceTypes.notebookWorkspaces],
|
||||
}),
|
||||
headers: {
|
||||
[authorizationHeader.header]: authorizationHeader.token,
|
||||
[Constants.HttpHeaders.contentType]: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch disallowed locations");
|
||||
}
|
||||
|
||||
const disallowedLocations: string[] = await response.json();
|
||||
if (!disallowedLocations) {
|
||||
Logger.logInfo("No disallowed locations found", "Explorer/isNotebooksEnabledForAccount");
|
||||
set({ isNotebooksEnabledForAccount: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// firstWriteLocation should not be disallowed
|
||||
const isAccountInAllowedLocation = firstWriteLocation && disallowedLocations.indexOf(firstWriteLocation) === -1;
|
||||
set({ isNotebooksEnabledForAccount: isAccountInAllowedLocation });
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
|
||||
set({ isNotebooksEnabledForAccount: false });
|
||||
}
|
||||
},
|
||||
findItem: (root: NotebookContentItem, item: NotebookContentItem): NotebookContentItem => {
|
||||
const currentItem = root || get().myNotebooksContentRoot;
|
||||
|
||||
if (currentItem) {
|
||||
if (currentItem.path === item.path && currentItem.name === item.name) {
|
||||
return currentItem;
|
||||
}
|
||||
|
||||
if (currentItem.children) {
|
||||
for (const childItem of currentItem.children) {
|
||||
const result = get().findItem(childItem, item);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => {
|
||||
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
|
||||
const parentItem = get().findItem(root, parent);
|
||||
item.parent = parentItem;
|
||||
if (parentItem.children) {
|
||||
parentItem.children.push(item);
|
||||
} else {
|
||||
parentItem.children = [item];
|
||||
}
|
||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||
},
|
||||
updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
|
||||
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
|
||||
const parentItem = get().findItem(root, item.parent);
|
||||
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
|
||||
parentItem.children.push(item);
|
||||
item.parent = parentItem;
|
||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||
},
|
||||
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
|
||||
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
|
||||
const parentItem = get().findItem(root, item.parent);
|
||||
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
|
||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||
},
|
||||
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
|
||||
const notebookFolderName = get().isPhoenixNotebooks ? "Temporary Notebooks" : "My Notebooks";
|
||||
set({ notebookFolderName });
|
||||
const myNotebooksContentRoot = {
|
||||
name: get().notebookFolderName,
|
||||
path: get().notebookBasePath,
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
const galleryContentRoot = {
|
||||
name: "Gallery",
|
||||
path: "Gallery",
|
||||
type: NotebookContentItemType.File,
|
||||
};
|
||||
const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn()
|
||||
? {
|
||||
name: "GitHub repos",
|
||||
path: "PsuedoDir",
|
||||
type: NotebookContentItemType.Directory,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
set({
|
||||
myNotebooksContentRoot,
|
||||
galleryContentRoot,
|
||||
gitHubNotebooksContentRoot,
|
||||
});
|
||||
|
||||
gitHubNotebooksContentRoot.children.push(repoTreeItem);
|
||||
});
|
||||
if (get().notebookServerInfo?.notebookServerEndpoint) {
|
||||
const updatedRoot = await notebookManager?.notebookContentClient?.updateItemChildren(myNotebooksContentRoot);
|
||||
set({ myNotebooksContentRoot: updatedRoot });
|
||||
|
||||
set({ gitHubNotebooksContentRoot });
|
||||
}
|
||||
},
|
||||
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
|
||||
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
|
||||
resetContainerConnection: (connectionStatus: ContainerConnectionInfo): void => {
|
||||
useTabs.getState().closeAllNotebookTabs(true);
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
useNotebook.getState().setNotebookServerInfo(undefined);
|
||||
useNotebook.getState().setIsAllocating(false);
|
||||
useNotebook.getState().setContainerStatus({
|
||||
status: undefined,
|
||||
durationLeftInMinutes: undefined,
|
||||
phoenixServerInfo: undefined,
|
||||
});
|
||||
},
|
||||
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
|
||||
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
|
||||
getPhoenixStatus: async () => {
|
||||
if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) {
|
||||
let isPhoenixNotebooks = false;
|
||||
let isPhoenixFeatures = false;
|
||||
|
||||
const isPublicInternetAllowed = isPublicInternetAccessAllowed();
|
||||
const phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
|
||||
const dbAccountAllowedInfo = await phoenixClient.getDbAccountAllowedStatus();
|
||||
|
||||
if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) {
|
||||
if (dbAccountAllowedInfo?.type === PhoenixErrorType.PhoenixFlightFallback) {
|
||||
isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks === true;
|
||||
isPhoenixFeatures =
|
||||
isPublicInternetAllowed &&
|
||||
// phoenix needs to be enabled for Postgres and VCoreMongo accounts since the PSQL and mongo shell requires phoenix containers
|
||||
(userContext.features.phoenixFeatures === true ||
|
||||
userContext.apiType === "Postgres" ||
|
||||
userContext.apiType === "VCoreMongo");
|
||||
} else {
|
||||
isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed;
|
||||
if (updatedRoot?.children) {
|
||||
// Count 1st generation children (tree is lazy-loaded)
|
||||
const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
|
||||
updatedRoot.children.forEach((notebookItem) => {
|
||||
switch (notebookItem.type) {
|
||||
case NotebookContentItemType.File:
|
||||
nodeCounts.files++;
|
||||
break;
|
||||
case NotebookContentItemType.Directory:
|
||||
nodeCounts.directories++;
|
||||
break;
|
||||
case NotebookContentItemType.Notebook:
|
||||
nodeCounts.notebooks++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
isPhoenixNotebooks = isPhoenixFeatures = false;
|
||||
}
|
||||
set({ isPhoenixNotebooks: isPhoenixNotebooks });
|
||||
set({ isPhoenixFeatures: isPhoenixFeatures });
|
||||
}
|
||||
},
|
||||
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),
|
||||
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }),
|
||||
}));
|
||||
},
|
||||
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]): void => {
|
||||
const gitHubNotebooksContentRoot = cloneDeep(get().gitHubNotebooksContentRoot);
|
||||
if (gitHubNotebooksContentRoot) {
|
||||
gitHubNotebooksContentRoot.children = [];
|
||||
pinnedRepos?.forEach((pinnedRepo) => {
|
||||
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
|
||||
const repoTreeItem: NotebookContentItem = {
|
||||
name: repoFullName,
|
||||
path: "PsuedoDir",
|
||||
type: NotebookContentItemType.Directory,
|
||||
children: [],
|
||||
parent: gitHubNotebooksContentRoot,
|
||||
};
|
||||
|
||||
pinnedRepo.branches.forEach((branch) => {
|
||||
repoTreeItem.children.push({
|
||||
name: branch.name,
|
||||
path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""),
|
||||
type: NotebookContentItemType.Directory,
|
||||
parent: repoTreeItem,
|
||||
});
|
||||
});
|
||||
|
||||
gitHubNotebooksContentRoot.children.push(repoTreeItem);
|
||||
});
|
||||
|
||||
set({ gitHubNotebooksContentRoot });
|
||||
}
|
||||
},
|
||||
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
|
||||
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
|
||||
resetContainerConnection: (connectionStatus: ContainerConnectionInfo): void => {
|
||||
useTabs.getState().closeAllNotebookTabs(true);
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
useNotebook.getState().setNotebookServerInfo(undefined);
|
||||
useNotebook.getState().setIsAllocating(false);
|
||||
useNotebook.getState().setContainerStatus({
|
||||
status: undefined,
|
||||
durationLeftInMinutes: undefined,
|
||||
phoenixServerInfo: undefined,
|
||||
});
|
||||
},
|
||||
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
|
||||
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
|
||||
getPhoenixStatus: async () => {
|
||||
if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) {
|
||||
let isPhoenixNotebooks = false;
|
||||
let isPhoenixFeatures = false;
|
||||
|
||||
const isPublicInternetAllowed = isPublicInternetAccessAllowed();
|
||||
const phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
|
||||
const dbAccountAllowedInfo = await phoenixClient.getDbAccountAllowedStatus();
|
||||
|
||||
if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) {
|
||||
if (dbAccountAllowedInfo?.type === PhoenixErrorType.PhoenixFlightFallback) {
|
||||
isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks === true;
|
||||
isPhoenixFeatures =
|
||||
isPublicInternetAllowed &&
|
||||
// phoenix needs to be enabled for Postgres and VCoreMongo accounts since the PSQL and mongo shell requires phoenix containers
|
||||
(userContext.features.phoenixFeatures === true ||
|
||||
userContext.apiType === "Postgres" ||
|
||||
userContext.apiType === "VCoreMongo");
|
||||
} else {
|
||||
isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed;
|
||||
}
|
||||
} else {
|
||||
isPhoenixNotebooks = isPhoenixFeatures = false;
|
||||
}
|
||||
set({ isPhoenixNotebooks: isPhoenixNotebooks });
|
||||
set({ isPhoenixFeatures: isPhoenixFeatures });
|
||||
}
|
||||
},
|
||||
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),
|
||||
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
@@ -188,6 +188,11 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
|
||||
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection]
|
||||
) {
|
||||
explorer.onNewCollectionClicked();
|
||||
} else if (
|
||||
action.paneKind === ActionContracts.PaneKind.QuickStart ||
|
||||
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.QuickStart]
|
||||
) {
|
||||
explorer.onNewCollectionClicked({ isQuickstart: true });
|
||||
} else if (
|
||||
action.paneKind === ActionContracts.PaneKind.CassandraAddCollection ||
|
||||
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection]
|
||||
|
||||
@@ -25,7 +25,7 @@ import { FullTextPoliciesComponent } from "Explorer/Controls/FullTextSeach/FullT
|
||||
import { VectorEmbeddingPoliciesComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingPoliciesComponent";
|
||||
import {
|
||||
AllPropertiesIndexed,
|
||||
AnalyticalStorageContent,
|
||||
AnalyticalStoreHeader,
|
||||
ContainerVectorPolicyTooltipContent,
|
||||
FullTextPolicyDefault,
|
||||
getPartitionKey,
|
||||
@@ -49,14 +49,10 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { getCollectionName } from "Utils/APITypeUtils";
|
||||
import {
|
||||
isCapabilityEnabled,
|
||||
isFullTextSearchEnabled,
|
||||
isServerlessAccount,
|
||||
isVectorSearchEnabled,
|
||||
} from "Utils/CapabilityUtils";
|
||||
import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { getUpsellMessage } from "Utils/PricingUtils";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||
import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator";
|
||||
@@ -110,6 +106,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
private collectionThroughput: number;
|
||||
private isCollectionAutoscale: boolean;
|
||||
private isCostAcknowledged: boolean;
|
||||
private showFullTextSearch: boolean;
|
||||
|
||||
constructor(props: AddCollectionPanelProps) {
|
||||
super(props);
|
||||
@@ -126,7 +123,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
isSharded: userContext.apiType !== "Tables",
|
||||
partitionKey: getPartitionKey(props.isQuickstart),
|
||||
subPartitionKeys: [],
|
||||
enableDedicatedThroughput: false,
|
||||
enableDedicatedThroughput: isFabricNative(), // Dedicated throughput is only enabled in Fabric Native by default
|
||||
createMongoWildCardIndex:
|
||||
isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport"),
|
||||
useHashV1: false,
|
||||
@@ -144,6 +141,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
fullTextIndexes: [],
|
||||
fullTextPolicyValidated: true,
|
||||
};
|
||||
|
||||
this.showFullTextSearch = userContext.apiType === "SQL";
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
@@ -266,7 +265,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
<div className="panelMainContent">
|
||||
{!(isFabricNative() && this.props.databaseId !== undefined) && (
|
||||
<Stack hidden={userContext.apiType === "Tables"}>
|
||||
<Stack hidden={userContext.apiType === "Tables"} style={{ marginBottom: -2 }}>
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
@@ -337,7 +336,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
size={40}
|
||||
className="panelTextField"
|
||||
aria-label="New database id, Type a new database id"
|
||||
autoFocus
|
||||
tabIndex={0}
|
||||
value={this.state.newDatabaseId}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
@@ -408,12 +406,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
responsiveMode={999}
|
||||
/>
|
||||
)}
|
||||
<Separator className="panelSeparator" />
|
||||
<Separator className="panelSeparator" style={{ marginTop: -4, marginBottom: -4 }} />
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<Stack horizontal style={{ marginTop: -5, marginBottom: 1 }}>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
{`${getCollectionName()} id`}
|
||||
@@ -450,11 +448,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
this.setState({ collectionId: event.target.value })
|
||||
}
|
||||
/>
|
||||
<Separator className="panelSeparator" style={{ marginTop: -5, marginBottom: -5 }} />
|
||||
</Stack>
|
||||
|
||||
{this.shouldShowIndexingOptionsForFreeTierAccount() && (
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Indexing
|
||||
@@ -500,7 +499,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
(!this.state.isSharedThroughputChecked ||
|
||||
this.props.explorer.isFixedCollectionWithSharedThroughputSupported()) && (
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<Stack horizontal style={{ marginTop: -5, marginBottom: -4 }}>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Sharding
|
||||
@@ -556,7 +555,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
{this.state.isSharded && (
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<Stack horizontal style={{ marginTop: -5, marginBottom: -4 }}>
|
||||
<span className="mandatoryStar">* </span>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
{getPartitionKeyName()}
|
||||
@@ -600,7 +599,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
{userContext.apiType === "SQL" &&
|
||||
this.state.subPartitionKeys.map((subPartitionKey: string, index: number) => {
|
||||
return (
|
||||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${index}`} horizontal>
|
||||
<Stack style={{ marginBottom: 2, marginTop: -5 }} key={`uniqueKey${index}`} horizontal>
|
||||
<div
|
||||
style={{
|
||||
width: "20px",
|
||||
@@ -646,7 +645,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||
{userContext.apiType === "SQL" && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<DefaultButton
|
||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||
@@ -668,6 +667,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
<Separator className="panelSeparator" style={{ marginTop: 2, marginBottom: -4 }} />
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
@@ -709,7 +709,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{this.shouldShowCollectionThroughputInput() && (
|
||||
{this.shouldShowCollectionThroughputInput() && !isFabricNative() && (
|
||||
<ThroughputInput
|
||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
||||
isDatabase={false}
|
||||
@@ -728,7 +728,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
)}
|
||||
|
||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||
<Stack>
|
||||
<Stack style={{ marginTop: -2, marginBottom: -4 }}>
|
||||
{UniqueKeysHeader()}
|
||||
{this.state.uniqueKeys.map((uniqueKey: string, i: number): JSX.Element => {
|
||||
return (
|
||||
@@ -742,7 +742,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
: "Comma separated paths e.g. /firstName,/address/zipCode"
|
||||
}
|
||||
className="panelTextField"
|
||||
autoFocus
|
||||
value={uniqueKey}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const uniqueKeys = this.state.uniqueKeys.map((uniqueKey: string, j: number) => {
|
||||
@@ -777,10 +776,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||
<Separator className="panelSeparator" style={{ marginTop: -15, marginBottom: -4 }} />
|
||||
)}
|
||||
|
||||
{shouldShowAnalyticalStoreOptions() && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Stack className="panelGroupSpacing" style={{ marginTop: -4 }}>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
{AnalyticalStorageContent()}
|
||||
{AnalyticalStoreHeader()}
|
||||
</Text>
|
||||
|
||||
<Stack horizontal verticalAlign="center">
|
||||
@@ -821,7 +824,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Text variant="small">
|
||||
Azure Synapse Link is required for creating an analytical store{" "}
|
||||
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account.{" "}
|
||||
{getCollectionName().toLocaleLowerCase()}. Enable Synapse Link for this Cosmos DB account. <br />
|
||||
<Link
|
||||
href="https://aka.ms/cosmosdb-synapselink"
|
||||
target="_blank"
|
||||
@@ -1131,7 +1134,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
// }
|
||||
|
||||
private shouldShowCollectionThroughputInput(): boolean {
|
||||
if (isFabricNative() || isServerlessAccount()) {
|
||||
if (isServerlessAccount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1161,7 +1164,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
|
||||
private shouldShowFullTextSearchParameters() {
|
||||
return isFullTextSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
|
||||
return !isFabricNative() && this.showFullTextSearch;
|
||||
}
|
||||
|
||||
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
|
||||
@@ -1316,7 +1319,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.shouldShowFullTextSearchParameters()) {
|
||||
if (this.showFullTextSearch) {
|
||||
indexingPolicy.fullTextIndexes = this.state.fullTextIndexes;
|
||||
}
|
||||
|
||||
@@ -1350,7 +1353,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
let offerThroughput: number;
|
||||
let autoPilotMaxThroughput: number;
|
||||
|
||||
if (databaseLevelThroughput) {
|
||||
// Throughput
|
||||
if (isFabricNative()) {
|
||||
// Fabric Native accounts are always autoscale and have a fixed throughput of 5K
|
||||
autoPilotMaxThroughput = AutoPilotUtils.autoPilotThroughput5K;
|
||||
offerThroughput = undefined;
|
||||
} else if (databaseLevelThroughput) {
|
||||
if (this.state.createNewDatabase) {
|
||||
if (this.isNewDatabaseAutoscale) {
|
||||
autoPilotMaxThroughput = this.newDatabaseThroughput;
|
||||
|
||||
@@ -73,7 +73,7 @@ export function UniqueKeysHeader(): JSX.Element {
|
||||
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key.";
|
||||
|
||||
return (
|
||||
<Stack horizontal>
|
||||
<Stack horizontal style={{ marginBottom: -2 }}>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Unique keys
|
||||
</Text>
|
||||
@@ -98,6 +98,21 @@ export function shouldShowAnalyticalStoreOptions(): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export function AnalyticalStoreHeader(): JSX.Element {
|
||||
const tooltipContent =
|
||||
"Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.";
|
||||
return (
|
||||
<Stack horizontal style={{ marginBottom: -2 }}>
|
||||
<Text className="panelTextBold" variant="small">
|
||||
Analytical Store
|
||||
</Text>
|
||||
<TooltipHost directionalHint={DirectionalHint.bottomLeftEdge} content={tooltipContent}>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} ariaLabel={tooltipContent} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export function AnalyticalStorageContent(): JSX.Element {
|
||||
return (
|
||||
<Text variant="small">
|
||||
|
||||
@@ -11,6 +11,11 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
>
|
||||
<Stack
|
||||
hidden={false}
|
||||
style={
|
||||
{
|
||||
"marginBottom": -2,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
@@ -88,7 +93,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
aria-label="New database id, Type a new database id"
|
||||
aria-required={true}
|
||||
autoComplete="off"
|
||||
autoFocus={true}
|
||||
className="panelTextField"
|
||||
id="newDatabaseId"
|
||||
name="newDatabaseId"
|
||||
@@ -140,11 +144,23 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
</Stack>
|
||||
<Separator
|
||||
className="panelSeparator"
|
||||
style={
|
||||
{
|
||||
"marginBottom": -4,
|
||||
"marginTop": -4,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
{
|
||||
"marginBottom": 1,
|
||||
"marginTop": -5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<span
|
||||
className="mandatoryStar"
|
||||
@@ -186,10 +202,25 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<Separator
|
||||
className="panelSeparator"
|
||||
style={
|
||||
{
|
||||
"marginBottom": -5,
|
||||
"marginTop": -5,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
{
|
||||
"marginBottom": -4,
|
||||
"marginTop": -5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<span
|
||||
className="mandatoryStar"
|
||||
@@ -254,6 +285,15 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
Add hierarchical partition key
|
||||
</CustomizedDefaultButton>
|
||||
</Stack>
|
||||
<Separator
|
||||
className="panelSeparator"
|
||||
style={
|
||||
{
|
||||
"marginBottom": -4,
|
||||
"marginTop": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<ThroughputInput
|
||||
isDatabase={false}
|
||||
@@ -263,9 +303,21 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
setIsThroughputCapExceeded={[Function]}
|
||||
setThroughputValue={[Function]}
|
||||
/>
|
||||
<Stack>
|
||||
<Stack
|
||||
style={
|
||||
{
|
||||
"marginBottom": -4,
|
||||
"marginTop": -2,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
{
|
||||
"marginBottom": -2,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
@@ -306,26 +358,53 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
Add unique key
|
||||
</CustomizedActionButton>
|
||||
</Stack>
|
||||
<Separator
|
||||
className="panelSeparator"
|
||||
style={
|
||||
{
|
||||
"marginBottom": -4,
|
||||
"marginTop": -15,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Stack
|
||||
className="panelGroupSpacing"
|
||||
style={
|
||||
{
|
||||
"marginTop": -4,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
<Text
|
||||
variant="small"
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
{
|
||||
"marginBottom": -2,
|
||||
}
|
||||
}
|
||||
>
|
||||
Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads.
|
||||
|
||||
<StyledLinkBase
|
||||
aria-label="Learn more about analytical store."
|
||||
href="https://aka.ms/analytical-store-overview"
|
||||
target="_blank"
|
||||
<Text
|
||||
className="panelTextBold"
|
||||
variant="small"
|
||||
>
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
Analytical Store
|
||||
</Text>
|
||||
<StyledTooltipHostBase
|
||||
content="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</Stack>
|
||||
</Text>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
@@ -381,8 +460,8 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
Azure Synapse Link is required for creating an analytical store
|
||||
|
||||
container
|
||||
. Enable Synapse Link for this Cosmos DB account.
|
||||
|
||||
. Enable Synapse Link for this Cosmos DB account.
|
||||
<br />
|
||||
<StyledLinkBase
|
||||
aria-label="Learn more about Azure Synapse Link."
|
||||
className="capacitycalculator-link"
|
||||
@@ -411,6 +490,44 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<CollapsibleSectionComponent
|
||||
isExpandedByDefault={false}
|
||||
onExpand={[Function]}
|
||||
title="Container Full Text Search Policy"
|
||||
>
|
||||
<Stack
|
||||
id="collapsibleFullTextPolicySectionContent"
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"position": "relative",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
styles={
|
||||
{
|
||||
"root": {
|
||||
"paddingLeft": 40,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<FullTextPoliciesComponent
|
||||
fullTextPolicy={
|
||||
{
|
||||
"defaultLanguage": "en-US",
|
||||
"fullTextPaths": [],
|
||||
}
|
||||
}
|
||||
onFullTextPathChange={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
</Stack>
|
||||
<CollapsibleSectionComponent
|
||||
isExpandedByDefault={false}
|
||||
onExpand={[Function]}
|
||||
|
||||
@@ -40,12 +40,12 @@ import { PanelInfoErrorComponent } from "Explorer/Panes/PanelInfoErrorComponent"
|
||||
import { PanelLoadingScreen } from "Explorer/Panes/PanelLoadingScreen";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
import { CollectionCreation } from "Shared/Constants";
|
||||
import { Action } from "Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "UserContext";
|
||||
import { isFullTextSearchEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||
|
||||
export interface AddGlobalSecondaryIndexPanelProps {
|
||||
@@ -75,6 +75,8 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
const [showErrorDetails, setShowErrorDetails] = useState<boolean>();
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>();
|
||||
|
||||
const showFullTextSearch: MutableRefObject<boolean> = useRef<boolean>(userContext.apiType === "SQL");
|
||||
|
||||
useEffect(() => {
|
||||
const sourceContainerOptions: IDropdownOption[] = [];
|
||||
useDatabases.getState().databases.forEach((database: Database) => {
|
||||
@@ -140,10 +142,6 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
return isVectorSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
|
||||
};
|
||||
|
||||
const showFullTextSearchParameters = (): boolean => {
|
||||
return isFullTextSearchEnabled() && (isServerlessAccount() || showCollectionThroughputInput());
|
||||
};
|
||||
|
||||
const getAnalyticalStorageTtl = (): number => {
|
||||
if (!isSynapseLinkEnabled()) {
|
||||
return undefined;
|
||||
@@ -228,7 +226,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
};
|
||||
}
|
||||
|
||||
if (showFullTextSearchParameters()) {
|
||||
if (showFullTextSearch) {
|
||||
indexingPolicy.fullTextIndexes = fullTextIndexes;
|
||||
}
|
||||
|
||||
@@ -387,7 +385,7 @@ export const AddGlobalSecondaryIndexPanel = (props: AddGlobalSecondaryIndexPanel
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showFullTextSearchParameters() && (
|
||||
{showFullTextSearch && (
|
||||
<FullTextSearchComponent
|
||||
{...{ fullTextPolicy, setFullTextPolicy, setFullTextIndexes, setFullTextPolicyValidated }}
|
||||
/>
|
||||
|
||||
@@ -172,6 +172,17 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
|
||||
}
|
||||
setEnableAnalyticalStore={[Function]}
|
||||
/>
|
||||
<FullTextSearchComponent
|
||||
fullTextPolicy={
|
||||
{
|
||||
"defaultLanguage": "en-US",
|
||||
"fullTextPaths": [],
|
||||
}
|
||||
}
|
||||
setFullTextIndexes={[Function]}
|
||||
setFullTextPolicy={[Function]}
|
||||
setFullTextPolicyValidated={[Function]}
|
||||
/>
|
||||
<AdvancedComponent
|
||||
setSubPartitionKeys={[Function]}
|
||||
setUseHashV1={[Function]}
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
margin: 20px 0;
|
||||
overflow-x: hidden;
|
||||
|
||||
& > :not(.collapsibleSection) {
|
||||
&> :not(.collapsibleSection) {
|
||||
margin-bottom: @DefaultSpace;
|
||||
|
||||
& > :not(:last-child) {
|
||||
&> :not(:last-child) {
|
||||
margin-bottom: @DefaultSpace;
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,14 @@
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.panelMainContent {
|
||||
padding: 0 24px;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panelHeader {
|
||||
@@ -113,70 +121,87 @@
|
||||
.deleteCollectionFeedback {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.addRemoveIcon {
|
||||
margin-left: 4px !important;
|
||||
}
|
||||
|
||||
.addRemoveIconLabel {
|
||||
margin-top: 28px;
|
||||
margin-left: 4px !important;
|
||||
}
|
||||
|
||||
.addRemoveIcon [alt="editEntity"]:focus,
|
||||
.addRemoveIconLabel [alt="editEntity"]:focus {
|
||||
border: 1px dashed #605e5c;
|
||||
}
|
||||
|
||||
.addNewParamStyle {
|
||||
margin-top: 5px;
|
||||
margin-left: 5px !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.panelGroupSpacing > :not(:last-child) {
|
||||
.panelGroupSpacing> :not(:last-child) {
|
||||
margin-bottom: @DefaultSpace;
|
||||
}
|
||||
|
||||
.fileUpload {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.customFileUpload {
|
||||
padding: 25px 0px 0px 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.fileIcon {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.panelAddIconLabel {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
margin: 30px 0 0 10px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.panelAddIcon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
margin: 30px 0 0 10px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.removeIcon {
|
||||
color: @InfoIconColor;
|
||||
}
|
||||
|
||||
.backImageIcon {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
[alt="back"]:focus {
|
||||
border: 1px solid #605e5c;
|
||||
}
|
||||
|
||||
.addEntityDatePicker {
|
||||
max-width: 145px;
|
||||
}
|
||||
|
||||
.addEntityTextField {
|
||||
width: 237px;
|
||||
}
|
||||
|
||||
.addButtonEntiy {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.column-select-view {
|
||||
margin: 20px 0px 0px 0px;
|
||||
}
|
||||
|
||||
.panelSeparator::before {
|
||||
background-color: #edebe9;
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { create } from "zustand";
|
||||
import Explorer from "../../Explorer";
|
||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||
|
||||
@@ -65,8 +65,6 @@ export interface DataPlaneRbacState {
|
||||
setAadDataPlaneUpdated: (aadTokenUpdated: boolean) => void;
|
||||
}
|
||||
|
||||
type DataPlaneRbacStore = UseStore<Partial<DataPlaneRbacState>>;
|
||||
|
||||
const useStyles = makeStyles({
|
||||
bulletList: {
|
||||
listStyleType: "disc",
|
||||
@@ -100,7 +98,7 @@ const useStyles = makeStyles({
|
||||
},
|
||||
});
|
||||
|
||||
export const useDataPlaneRbac: DataPlaneRbacStore = create(() => ({
|
||||
export const useDataPlaneRbac = create<Partial<DataPlaneRbacState>>(() => ({
|
||||
dataPlaneRbacEnabled: false,
|
||||
}));
|
||||
|
||||
@@ -180,6 +178,11 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
? LocalStorageUtility.getEntryNumber(StorageKey.MaxWaitTimeInSeconds)
|
||||
: Constants.Queries.DefaultMaxWaitTimeInSeconds,
|
||||
);
|
||||
const [queryControlEnabled, setQueryControlEnabled] = useState<boolean>(
|
||||
LocalStorageUtility.hasItem(StorageKey.QueryControlEnabled)
|
||||
? LocalStorageUtility.getEntryString(StorageKey.QueryControlEnabled) === "true"
|
||||
: false,
|
||||
);
|
||||
const [maxDegreeOfParallelism, setMaxDegreeOfParallelism] = useState<number>(
|
||||
LocalStorageUtility.hasItem(StorageKey.MaxDegreeOfParellism)
|
||||
? LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism)
|
||||
@@ -194,6 +197,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true",
|
||||
);
|
||||
|
||||
const [mongoGuidRepresentation, setMongoGuidRepresentation] = useState<Constants.MongoGuidRepresentation>(
|
||||
LocalStorageUtility.hasItem(StorageKey.MongoGuidRepresentation)
|
||||
? (LocalStorageUtility.getEntryString(StorageKey.MongoGuidRepresentation) as Constants.MongoGuidRepresentation)
|
||||
: Constants.MongoGuidRepresentation.CSharpLegacy,
|
||||
);
|
||||
|
||||
const styles = useStyles();
|
||||
|
||||
const explorerVersion = configContext.gitSha;
|
||||
@@ -204,6 +213,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
!isEmulator;
|
||||
const shouldShowGraphAutoVizOption = userContext.apiType === "Gremlin" && !isEmulator;
|
||||
const shouldShowCrossPartitionOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||
const shouldShowEnhancedQueryControl = userContext.apiType === "SQL";
|
||||
const shouldShowParallelismOption = userContext.apiType !== "Gremlin" && !isEmulator;
|
||||
const showEnableEntraIdRbac =
|
||||
isDataplaneRbacSupported(userContext.apiType) &&
|
||||
@@ -255,6 +265,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
useDatabases.getState().sampleDataResourceTokenCollection &&
|
||||
!isEmulator;
|
||||
|
||||
const shouldShowMongoGuidRepresentationOption = userContext.apiType === "Mongo";
|
||||
|
||||
const handlerOnSubmit = async () => {
|
||||
setIsExecuting(true);
|
||||
|
||||
@@ -381,6 +393,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.MaxWaitTimeInSeconds, MaxWaitTimeInSeconds);
|
||||
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
|
||||
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
|
||||
LocalStorageUtility.setEntryString(StorageKey.QueryControlEnabled, queryControlEnabled.toString());
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
|
||||
LocalStorageUtility.setEntryString(StorageKey.PriorityLevel, priorityLevel.toString());
|
||||
LocalStorageUtility.setEntryString(StorageKey.CopilotSampleDBEnabled, copilotSampleDBEnabled.toString());
|
||||
@@ -405,11 +418,16 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldShowMongoGuidRepresentationOption) {
|
||||
LocalStorageUtility.setEntryString(StorageKey.MongoGuidRepresentation, mongoGuidRepresentation);
|
||||
}
|
||||
|
||||
setIsExecuting(false);
|
||||
logConsoleInfo(
|
||||
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`,
|
||||
);
|
||||
logConsoleInfo(`${crossPartitionQueryEnabled ? "Enabled" : "Disabled"} cross-partition query feed option`);
|
||||
logConsoleInfo(`${queryControlEnabled ? "Enabled" : "Disabled"} query control option`);
|
||||
logConsoleInfo(
|
||||
`Updated the max degree of parallelism query feed option to ${LocalStorageUtility.getEntryNumber(
|
||||
StorageKey.MaxDegreeOfParellism,
|
||||
@@ -425,9 +443,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
);
|
||||
}
|
||||
|
||||
logConsoleInfo(
|
||||
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
|
||||
);
|
||||
if (shouldShowMongoGuidRepresentationOption) {
|
||||
logConsoleInfo(
|
||||
`Updated Mongo Guid Representation to ${LocalStorageUtility.getEntryString(
|
||||
StorageKey.MongoGuidRepresentation,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
refreshExplorer && (await explorer.refreshExplorer());
|
||||
closeSidePanel();
|
||||
};
|
||||
@@ -472,6 +495,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
|
||||
];
|
||||
|
||||
const mongoGuidRepresentationDropdownOptions: IDropdownOption[] = [
|
||||
{ key: Constants.MongoGuidRepresentation.CSharpLegacy, text: Constants.MongoGuidRepresentation.CSharpLegacy },
|
||||
{ key: Constants.MongoGuidRepresentation.JavaLegacy, text: Constants.MongoGuidRepresentation.JavaLegacy },
|
||||
{ key: Constants.MongoGuidRepresentation.PythonLegacy, text: Constants.MongoGuidRepresentation.PythonLegacy },
|
||||
{ key: Constants.MongoGuidRepresentation.Standard, text: Constants.MongoGuidRepresentation.Standard },
|
||||
];
|
||||
|
||||
const handleOnPriorityLevelOptionChange = (
|
||||
ev: React.FormEvent<HTMLInputElement>,
|
||||
option: IChoiceGroupOption,
|
||||
@@ -554,6 +584,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
setRefreshExplorer(false);
|
||||
};
|
||||
|
||||
const handleOnMongoGuidRepresentationOptionChange = (
|
||||
ev: React.FormEvent<HTMLInputElement>,
|
||||
option: IDropdownOption,
|
||||
): void => {
|
||||
setMongoGuidRepresentation(option.key as Constants.MongoGuidRepresentation);
|
||||
};
|
||||
|
||||
const choiceButtonStyles = {
|
||||
root: {
|
||||
clear: "both",
|
||||
@@ -760,7 +797,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="5">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>RU Limit</div>
|
||||
@@ -943,6 +979,38 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{shouldShowEnhancedQueryControl && (
|
||||
<AccordionItem value="10">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Enhanced query control</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
Query up to the max degree of parallelism.
|
||||
<a
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/performance-tips-query-sdk?tabs=v3&pivots=programming-language-nodejs#enhanced-query-control"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{" "}
|
||||
Learn more{" "}
|
||||
</a>
|
||||
</div>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="EnableQueryControl"
|
||||
checked={queryControlEnabled}
|
||||
onChange={() => setQueryControlEnabled(!queryControlEnabled)}
|
||||
label="Enable query control"
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{shouldShowParallelismOption && (
|
||||
<AccordionItem value="10">
|
||||
<AccordionHeader>
|
||||
@@ -1029,15 +1097,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
This is a sample database and collection with synthetic product data you can use to explore using
|
||||
NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and
|
||||
is created by, and maintained by Microsoft at no cost to you.
|
||||
NoSQL queries. This will appear as another database in the Data Explorer UI, and is created by,
|
||||
and maintained by Microsoft at no cost to you.
|
||||
</div>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Enable sample db for Query Advisor"
|
||||
ariaLabel="Enable sample db for query exploration"
|
||||
checked={copilotSampleDBEnabled}
|
||||
onChange={handleSampleDatabaseChange}
|
||||
label="Enable sample database"
|
||||
@@ -1046,6 +1114,27 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{shouldShowMongoGuidRepresentationOption && (
|
||||
<AccordionItem value="14">
|
||||
<AccordionHeader>
|
||||
<div className={styles.header}>Guid Representation</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div className={styles.settingsSectionContainer}>
|
||||
<div className={styles.settingsSectionDescription}>
|
||||
GuidRepresentation in MongoDB refers to how Globally Unique Identifiers (GUIDs) are serialized and
|
||||
deserialized when stored in BSON documents. This will apply to all document operations.
|
||||
</div>
|
||||
<Dropdown
|
||||
aria-labelledby="mongoGuidRepresentation"
|
||||
selectedKey={mongoGuidRepresentation}
|
||||
options={mongoGuidRepresentationDropdownOptions}
|
||||
onChange={handleOnMongoGuidRepresentationOptionChange}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
</Accordion>
|
||||
)}
|
||||
|
||||
|
||||
@@ -495,6 +495,51 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem
|
||||
value="10"
|
||||
>
|
||||
<AccordionHeader>
|
||||
<div
|
||||
className="___15c001r_0000000 fq02s40"
|
||||
>
|
||||
Enhanced query control
|
||||
</div>
|
||||
</AccordionHeader>
|
||||
<AccordionPanel>
|
||||
<div
|
||||
className="___1dfa554_0000000 fo7qwa0"
|
||||
>
|
||||
<div
|
||||
className="___10gar1i_0000000 f1fow5ox f1ugzwwg"
|
||||
>
|
||||
Query up to the max degree of parallelism.
|
||||
<a
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/performance-tips-query-sdk?tabs=v3&pivots=programming-language-nodejs#enhanced-query-control"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
Learn more
|
||||
|
||||
</a>
|
||||
</div>
|
||||
<StyledCheckboxBase
|
||||
ariaLabel="EnableQueryControl"
|
||||
checked={false}
|
||||
className="padding"
|
||||
label="Enable query control"
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
{
|
||||
"label": {
|
||||
"padding": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem
|
||||
value="10"
|
||||
>
|
||||
|
||||
@@ -48,7 +48,11 @@ const classNames = mergeStyleSets({
|
||||
warning: [{ color: theme.semanticColors.warningIcon }, iconClass],
|
||||
});
|
||||
|
||||
export const UploadItemsPane: FunctionComponent = () => {
|
||||
export type UploadItemsPaneProps = {
|
||||
onUpload?: (data: UploadDetailsRecord[]) => void;
|
||||
};
|
||||
|
||||
export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpload }) => {
|
||||
const [files, setFiles] = useState<FileList>();
|
||||
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
@@ -71,6 +75,8 @@ export const UploadItemsPane: FunctionComponent = () => {
|
||||
(uploadDetails) => {
|
||||
setUploadFileData(uploadDetails.data);
|
||||
setFiles(undefined);
|
||||
// Emit the upload details to the parent component
|
||||
onUpload && onUpload(uploadDetails.data);
|
||||
},
|
||||
(error: Error) => {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
||||
import QueryError from "Common/QueryError";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { QueryResults } from "Contracts/ViewModels";
|
||||
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||
import { guid } from "Explorer/Tables/Utilities";
|
||||
import { QueryCopilotState } from "hooks/useQueryCopilot";
|
||||
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
import create from "zustand";
|
||||
import { create } from "zustand";
|
||||
|
||||
const context = createContext(null);
|
||||
const useCopilotStore = (): Partial<QueryCopilotState> => useContext(context);
|
||||
|
||||
@@ -24,12 +26,12 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
||||
isGeneratingQuery: false,
|
||||
isGeneratingExplanation: false,
|
||||
isExecuting: false,
|
||||
dislikeQuery: undefined,
|
||||
dislikeQuery: undefined as boolean,
|
||||
showCallout: false,
|
||||
showSamplePrompts: false,
|
||||
queryIterator: undefined,
|
||||
queryResults: undefined,
|
||||
errors: [],
|
||||
queryIterator: undefined as MinimalQueryIterator,
|
||||
queryResults: undefined as QueryResults,
|
||||
errors: [] as QueryError[],
|
||||
isSamplePromptsOpen: false,
|
||||
showPromptTeachingBubble: true,
|
||||
showDeletePopup: false,
|
||||
@@ -41,7 +43,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
||||
wasCopilotUsed: false,
|
||||
showWelcomeSidebar: true,
|
||||
showCopilotSidebar: false,
|
||||
chatMessages: [],
|
||||
chatMessages: [] as CopilotMessage[],
|
||||
shouldIncludeInMessages: true,
|
||||
showExplanationBubble: false,
|
||||
isAllocatingContainer: false,
|
||||
@@ -86,7 +88,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
||||
},
|
||||
|
||||
resetQueryCopilotStates: () => {
|
||||
set((state) => ({
|
||||
set((state: QueryCopilotState) => ({
|
||||
...state,
|
||||
generatedQuery: "",
|
||||
likeQuery: false,
|
||||
@@ -99,11 +101,11 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
||||
isGeneratingQuery: false,
|
||||
isGeneratingExplanation: false,
|
||||
isExecuting: false,
|
||||
dislikeQuery: undefined,
|
||||
dislikeQuery: undefined as boolean,
|
||||
showCallout: false,
|
||||
showSamplePrompts: false,
|
||||
queryIterator: undefined,
|
||||
queryResults: undefined,
|
||||
queryIterator: undefined as MinimalQueryIterator,
|
||||
queryResults: undefined as QueryResults,
|
||||
errorMessage: "",
|
||||
isSamplePromptsOpen: false,
|
||||
showPromptTeachingBubble: true,
|
||||
@@ -115,19 +117,19 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
||||
generatedQueryComments: "",
|
||||
wasCopilotUsed: false,
|
||||
showCopilotSidebar: false,
|
||||
chatMessages: [],
|
||||
chatMessages: [] as CopilotMessage[],
|
||||
shouldIncludeInMessages: true,
|
||||
showExplanationBubble: false,
|
||||
notebookServerInfo: {
|
||||
notebookServerEndpoint: undefined,
|
||||
authToken: undefined,
|
||||
forwardingId: undefined,
|
||||
},
|
||||
} as DataModels.NotebookWorkspaceConnectionInfo,
|
||||
containerStatus: {
|
||||
status: undefined,
|
||||
durationLeftInMinutes: undefined,
|
||||
phoenixServerInfo: undefined,
|
||||
},
|
||||
} as DataModels.ContainerInfo,
|
||||
isAllocatingContainer: false,
|
||||
}));
|
||||
},
|
||||
@@ -137,3 +139,4 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
||||
};
|
||||
|
||||
export { CopilotProvider, useCopilotStore };
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
/* eslint-disable no-console */
|
||||
import { Stack } from "@fluentui/react";
|
||||
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
||||
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||
@@ -13,7 +11,6 @@ import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotRe
|
||||
import { userContext } from "UserContext";
|
||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import { ReactTabKind, TabsState, useTabs } from "hooks/useTabs";
|
||||
import React, { useState } from "react";
|
||||
import SplitterLayout from "react-splitter-layout";
|
||||
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
||||
@@ -26,7 +23,8 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
const [copilotActive, setCopilotActive] = useState<boolean>(() =>
|
||||
readCopilotToggleStatus(userContext.databaseAccount),
|
||||
);
|
||||
const [tabActive, setTabActive] = useState<boolean>(true);
|
||||
//TODO: Uncomment this useState when query copilot is reinstated in DE
|
||||
// const [tabActive, setTabActive] = useState<boolean>(true);
|
||||
|
||||
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
||||
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
||||
@@ -70,17 +68,18 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
useCommandBar.getState().setContextButtons(getCommandbarButtons());
|
||||
}, [query, selectedQuery, copilotActive]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
useTabs.subscribe((state: TabsState) => {
|
||||
if (state.activeReactTab === ReactTabKind.QueryCopilot) {
|
||||
setTabActive(true);
|
||||
} else {
|
||||
setTabActive(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
//TODO: Uncomment this effect when query copilot is reinstated in DE
|
||||
// React.useEffect(() => {
|
||||
// return () => {
|
||||
// useTabs.subscribe((state: TabsState) => {
|
||||
// if (state.activeReactTab === ReactTabKind.QueryCopilot) {
|
||||
// setTabActive(true);
|
||||
// } else {
|
||||
// setTabActive(false);
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
const toggleCopilot = (toggle: boolean) => {
|
||||
setCopilotActive(toggle);
|
||||
@@ -90,6 +89,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
return (
|
||||
<Stack className="tab-pane" style={{ width: "100%" }}>
|
||||
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
|
||||
{/*TODO: Uncomment this section when query copilot is reinstated in DE
|
||||
{tabActive && copilotActive && (
|
||||
<QueryCopilotPromptbar
|
||||
explorer={explorer}
|
||||
@@ -97,7 +97,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
||||
databaseId={QueryCopilotSampleDatabaseId}
|
||||
containerId={QueryCopilotSampleContainerId}
|
||||
></QueryCopilotPromptbar>
|
||||
)}
|
||||
)} */}
|
||||
<Stack className="tabPaneContentContainer">
|
||||
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
|
||||
<EditorReact
|
||||
|
||||
@@ -30,8 +30,10 @@ import { KeyboardAction, KeyboardActionGroup, KeyboardActionHandler, useKeyboard
|
||||
import { isFabric, isFabricMirrored, isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
|
||||
import { userContext } from "UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "Utils/APITypeUtils";
|
||||
import { conditionalClass } from "Utils/StyleUtils";
|
||||
import { Allotment, AllotmentHandle } from "allotment";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import useZoomLevel from "hooks/useZoomLevel";
|
||||
import { debounce } from "lodash";
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
@@ -104,6 +106,23 @@ const useSidebarStyles = makeStyles({
|
||||
display: "flex",
|
||||
},
|
||||
},
|
||||
accessibleContent: {
|
||||
"@media (max-width: 420px)": {
|
||||
overflow: "scroll",
|
||||
},
|
||||
},
|
||||
minHeightResponsive: {
|
||||
"@media (max-width: 420px)": {
|
||||
minHeight: "400px",
|
||||
},
|
||||
},
|
||||
accessibleContentZoom: {
|
||||
overflow: "scroll",
|
||||
},
|
||||
|
||||
minHeightZoom: {
|
||||
minHeight: "400px",
|
||||
},
|
||||
});
|
||||
|
||||
interface GlobalCommandsProps {
|
||||
@@ -275,6 +294,7 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
||||
const [expandedSize, setExpandedSize] = React.useState(300);
|
||||
const hasSidebar = userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo";
|
||||
const allotment = useRef<AllotmentHandle>(null);
|
||||
const isZoomed = useZoomLevel();
|
||||
|
||||
const expand = useCallback(() => {
|
||||
if (!expanded) {
|
||||
@@ -325,11 +345,23 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
||||
|
||||
return (
|
||||
<div className="sidebarContainer">
|
||||
<Allotment ref={allotment} onChange={onChange} onDragEnd={onDragEnd} className="resourceTreeAndTabs">
|
||||
<Allotment
|
||||
ref={allotment}
|
||||
onChange={onChange}
|
||||
onDragEnd={onDragEnd}
|
||||
className={`resourceTreeAndTabs ${styles.accessibleContent} ${conditionalClass(
|
||||
isZoomed,
|
||||
styles.accessibleContentZoom,
|
||||
)}`}
|
||||
>
|
||||
{/* Collections Tree - Start */}
|
||||
{hasSidebar && (
|
||||
// When collapsed, we force the pane to 24 pixels wide and make it non-resizable.
|
||||
<Allotment.Pane minSize={24} preferredSize={250}>
|
||||
<Allotment.Pane
|
||||
className={`${styles.minHeightResponsive} ${conditionalClass(isZoomed, styles.minHeightZoom)}`}
|
||||
minSize={24}
|
||||
preferredSize={250}
|
||||
>
|
||||
<CosmosFluentProvider className={mergeClasses(styles.sidebar)}>
|
||||
<div className={styles.sidebarContainer}>
|
||||
{loading && (
|
||||
@@ -385,7 +417,10 @@ export const SidebarContainer: React.FC<SidebarProps> = ({ explorer }) => {
|
||||
</CosmosFluentProvider>
|
||||
</Allotment.Pane>
|
||||
)}
|
||||
<Allotment.Pane minSize={200}>
|
||||
<Allotment.Pane
|
||||
className={`${styles.minHeightResponsive} ${conditionalClass(isZoomed, styles.minHeightZoom)}`}
|
||||
minSize={200}
|
||||
>
|
||||
<Tabs explorer={explorer} />
|
||||
</Allotment.Pane>
|
||||
</Allotment>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Accordion top class
|
||||
*/
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
|
||||
import { Link, makeStyles, tokens } from "@fluentui/react-components";
|
||||
import { DocumentAddRegular, LinkMultipleRegular, OpenRegular } from "@fluentui/react-icons";
|
||||
import { SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
|
||||
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
||||
import { isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
|
||||
@@ -119,7 +119,7 @@ const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick}>
|
||||
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick} tabIndex={0}>
|
||||
<div className={styles.buttonUpperPart}>{icon}</div>
|
||||
<div aria-label={title} className={styles.buttonLowerPart}>
|
||||
<div>{title}</div>
|
||||
@@ -147,7 +147,7 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
{
|
||||
title: "Sample data",
|
||||
description: "Automatically load sample data in your database",
|
||||
icon: <img src={CosmosDbBlackIcon} />,
|
||||
icon: <img src={CosmosDbBlackIcon} alt={"Azure Cosmos DB icon"} aria-hidden="true" />,
|
||||
onClick: () => setOpenSampleDataImportDialog(true),
|
||||
},
|
||||
{
|
||||
@@ -181,16 +181,18 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
||||
explorer={props.explorer}
|
||||
databaseName={userContext.fabricContext?.databaseName}
|
||||
/>
|
||||
<div className={styles.title} role="heading" aria-label={title}>
|
||||
<div className={styles.title} role="heading" aria-label={title} aria-level={1}>
|
||||
{title}
|
||||
</div>
|
||||
{getSplashScreenButtons()}
|
||||
{/* <div className={styles.footer}>
|
||||
Need help?{" "}
|
||||
<Link href="https://aka.ms/cosmosdbfabricdocs" target="_blank">
|
||||
Learn more <img src={LinkIcon} alt="Learn more" />
|
||||
</Link>
|
||||
</div> */}
|
||||
{
|
||||
<div className={styles.footer}>
|
||||
Need help?{" "}
|
||||
<Link href="https://learn.microsoft.com/fabric/database/cosmos-db/overview" target="_blank">
|
||||
Learn more <OpenRegular />
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
</CosmosFluentProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -30,6 +30,21 @@
|
||||
margin: 0px auto;
|
||||
text-align: center;
|
||||
}
|
||||
.splashStackContainer {
|
||||
.splashStackRow {
|
||||
display: flex;
|
||||
gap: 0 16px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 85% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mainButtonsContainer {
|
||||
.flex-display();
|
||||
|
||||
@@ -24,10 +24,12 @@ import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import * as React from "react";
|
||||
import ConnectIcon from "../../../images/Connect_color.svg";
|
||||
import ContainersIcon from "../../../images/Containers.svg";
|
||||
import CosmosDBIcon from "../../../images/CosmosDB-logo.svg";
|
||||
import LinkIcon from "../../../images/Link_blue.svg";
|
||||
import PowerShellIcon from "../../../images/PowerShell.svg";
|
||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
|
||||
import VisualStudioIcon from "../../../images/VisualStudio.svg";
|
||||
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
|
||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@@ -75,39 +77,39 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
this.subscriptions.push(
|
||||
{
|
||||
dispose: useNotebook.subscribe(
|
||||
() => this.setState({}),
|
||||
(state) => state.isNotebookEnabled,
|
||||
() => this.setState({}),
|
||||
),
|
||||
},
|
||||
{ dispose: useSelectedNode.subscribe(() => this.setState({})) },
|
||||
{
|
||||
dispose: useCarousel.subscribe(
|
||||
() => this.setState({}),
|
||||
(state) => state.showCoachMark,
|
||||
() => this.setState({}),
|
||||
),
|
||||
},
|
||||
{
|
||||
dispose: usePostgres.subscribe(
|
||||
() => this.setState({}),
|
||||
(state) => state.showPostgreTeachingBubble,
|
||||
() => this.setState({}),
|
||||
),
|
||||
},
|
||||
{
|
||||
dispose: usePostgres.subscribe(
|
||||
() => this.setState({}),
|
||||
(state) => state.showResetPasswordBubble,
|
||||
() => this.setState({}),
|
||||
),
|
||||
},
|
||||
{
|
||||
dispose: useDatabases.subscribe(
|
||||
() => this.setState({}),
|
||||
(state) => state.sampleDataResourceTokenCollection,
|
||||
() => this.setState({}),
|
||||
),
|
||||
},
|
||||
{
|
||||
dispose: useQueryCopilot.subscribe(
|
||||
() => this.setState({}),
|
||||
(state) => state.copilotEnabled,
|
||||
() => this.setState({}),
|
||||
),
|
||||
},
|
||||
);
|
||||
@@ -119,14 +121,14 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
};
|
||||
|
||||
private getSplashScreenButtons = (): JSX.Element => {
|
||||
if (
|
||||
userContext.apiType === "SQL" &&
|
||||
useQueryCopilot.getState().copilotEnabled &&
|
||||
useDatabases.getState().sampleDataResourceTokenCollection
|
||||
) {
|
||||
if (userContext.apiType === "SQL") {
|
||||
return (
|
||||
<Stack style={{ width: "66%", cursor: "pointer", margin: "40px auto" }} tokens={{ childrenGap: 16 }}>
|
||||
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
||||
<Stack
|
||||
className="splashStackContainer"
|
||||
style={{ width: "66%", cursor: "pointer", margin: "40px auto" }}
|
||||
tokens={{ childrenGap: 16 }}
|
||||
>
|
||||
<Stack className="splashStackRow" horizontal>
|
||||
<SplashScreenButton
|
||||
imgSrc={QuickStartIcon}
|
||||
title={"Launch quick start"}
|
||||
@@ -146,26 +148,19 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack horizontal tokens={{ childrenGap: 16 }}>
|
||||
{useQueryCopilot.getState().copilotEnabled && (
|
||||
<SplashScreenButton
|
||||
imgSrc={CopilotIcon}
|
||||
title={"Query faster with Query Advisor"}
|
||||
description={
|
||||
"Query Advisor is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
|
||||
}
|
||||
onClick={() => {
|
||||
const copilotVersion = userContext.features.copilotVersion;
|
||||
if (copilotVersion === "v1.0") {
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||
} else if (copilotVersion === "v2.0") {
|
||||
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
|
||||
sampleCollection.onNewQueryClick(sampleCollection, undefined);
|
||||
}
|
||||
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Stack className="splashStackRow" horizontal>
|
||||
<SplashScreenButton
|
||||
imgSrc={CosmosDBIcon}
|
||||
imgSize={35}
|
||||
title={"Azure Cosmos DB Samples Gallery"}
|
||||
description={
|
||||
"Discover samples that showcase scalable, intelligent app patterns. Try one now to see how fast you can go from concept to code with Cosmos DB"
|
||||
}
|
||||
onClick={() => {
|
||||
window.open("https://azurecosmosdb.github.io/gallery/?tags=example", "_blank");
|
||||
traceOpen(Action.LearningResourcesClicked, { apiType: userContext.apiType });
|
||||
}}
|
||||
/>
|
||||
<SplashScreenButton
|
||||
imgSrc={ConnectIcon}
|
||||
title={"Connect"}
|
||||
@@ -207,6 +202,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
sample data, query.
|
||||
</TeachingBubble>
|
||||
)}
|
||||
{/*TODO: convert below to use SplashScreenButton */}
|
||||
{mainItems.map((item) => (
|
||||
<Stack
|
||||
id={`mainButton-${item.id}`}
|
||||
@@ -290,10 +286,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
<form className="connectExplorerFormContainer">
|
||||
<div className="splashScreenContainer">
|
||||
<div className="splashScreen">
|
||||
<h1 className="title" role="heading" aria-label={title}>
|
||||
<h2 className="title" role="heading" aria-label={title}>
|
||||
{title}
|
||||
<FeaturePanelLauncher />
|
||||
</h1>
|
||||
</h2>
|
||||
<div className="subtitle">{subtitle}</div>
|
||||
{this.getSplashScreenButtons()}
|
||||
{useCarousel.getState().showCoachMark && (
|
||||
@@ -458,10 +454,10 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
}
|
||||
|
||||
if (userContext.apiType === "VCoreMongo") {
|
||||
icon = ContainersIcon;
|
||||
title = "Connect with Studio 3T";
|
||||
description = "Prefer Studio 3T? Find your connection strings here";
|
||||
onClick = () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect);
|
||||
icon = VisualStudioIcon;
|
||||
title = "Connect with VS Code";
|
||||
description = "Query and Manage your MongoDB cluster in Visual Studio Code";
|
||||
onClick = () => this.container.openInVsCode();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -472,6 +468,34 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
};
|
||||
}
|
||||
|
||||
//TODO: Re-enable lint rule when query copilot is reinstated in DE
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
private getQueryCopilotCard = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
{useQueryCopilot.getState().copilotEnabled && (
|
||||
<SplashScreenButton
|
||||
imgSrc={CopilotIcon}
|
||||
title={"Query faster with Query Advisor"}
|
||||
description={
|
||||
"Query Advisor is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
|
||||
}
|
||||
onClick={() => {
|
||||
const copilotVersion = userContext.features.copilotVersion;
|
||||
if (copilotVersion === "v1.0") {
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||
} else if (copilotVersion === "v2.0") {
|
||||
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
|
||||
sampleCollection.onNewQueryClick(sampleCollection, undefined);
|
||||
}
|
||||
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
|
||||
return {
|
||||
iconSrc: CollectionIcon,
|
||||
|
||||
@@ -7,6 +7,7 @@ interface SplashScreenButtonProps {
|
||||
title: string;
|
||||
description: string;
|
||||
onClick: () => void;
|
||||
imgSize?: number;
|
||||
}
|
||||
|
||||
export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
||||
@@ -14,6 +15,7 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
||||
title,
|
||||
description,
|
||||
onClick,
|
||||
imgSize,
|
||||
}: SplashScreenButtonProps): JSX.Element => {
|
||||
return (
|
||||
<Stack
|
||||
@@ -39,7 +41,7 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
||||
role="button"
|
||||
>
|
||||
<div>
|
||||
<img src={imgSrc} alt={title} aria-hidden="true" />
|
||||
<img src={imgSrc} alt={title} aria-hidden="true" {...(imgSize ? { height: imgSize, width: imgSize } : {})} />
|
||||
</div>
|
||||
<Stack style={{ marginLeft: 16 }}>
|
||||
<Text style={{ fontSize: 18, fontWeight: 600 }}>{title}</Text>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||
import { getAuthorizationHeader, isDataplaneRbacEnabledForProxyApi } from "../../Utils/AuthorizationUtils";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../Explorer";
|
||||
@@ -551,6 +551,10 @@ export class CassandraAPIDataClient extends TableDataClient {
|
||||
const authorizationHeaderMetadata: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||
xhr.setRequestHeader(authorizationHeaderMetadata.header, authorizationHeaderMetadata.token);
|
||||
|
||||
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
|
||||
xhr.setRequestHeader(Constants.HttpHeaders.entraIdToken, userContext.aadToken);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import "xterm/css/xterm.css";
|
||||
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
||||
import { TerminalKind } from "../../../Contracts/ViewModels";
|
||||
import { startCloudShellTerminal } from "./CloudShellTerminalCore";
|
||||
|
||||
@@ -22,6 +22,7 @@ import { formatErrorMessage, formatInfoMessage, formatWarningMessage } from "./U
|
||||
|
||||
// Constants
|
||||
const DEFAULT_CLOUDSHELL_REGION = "westus";
|
||||
const DEFAULT_FAIRFAX_CLOUDSHELL_REGION = "usgovvirginia";
|
||||
const POLLING_INTERVAL_MS = 2000;
|
||||
const MAX_RETRY_COUNT = 10;
|
||||
const MAX_PING_COUNT = 120 * 60; // 120 minutes (60 seconds/minute)
|
||||
@@ -44,32 +45,26 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter
|
||||
|
||||
resolvedRegion = determineCloudShellRegion();
|
||||
|
||||
resolvedRegion = determineCloudShellRegion();
|
||||
|
||||
terminal.writeln(formatWarningMessage("⚠️ IMPORTANT: Azure Cloud Shell Region Notice ⚠️"));
|
||||
terminal.writeln(
|
||||
formatInfoMessage(
|
||||
"The Cloud Shell environment will operate in a region that may differ from your database's region.",
|
||||
),
|
||||
);
|
||||
terminal.writeln(formatInfoMessage("This has two potential implications:"));
|
||||
terminal.writeln(formatInfoMessage("By using this feature, you acknowledge and agree to the following"));
|
||||
terminal.writeln(formatInfoMessage("1. Performance Impact:"));
|
||||
terminal.writeln(
|
||||
formatInfoMessage(" Commands may experience higher latency due to geographic distance between regions."),
|
||||
);
|
||||
terminal.writeln(formatInfoMessage("2. Data Compliance Considerations:"));
|
||||
terminal.writeln(formatInfoMessage("2. Data Transfers:"));
|
||||
terminal.writeln(
|
||||
formatInfoMessage(
|
||||
" Data processed through this shell could temporarily reside in a different geographic region,",
|
||||
" Data processed through this Cloud Shell service can be processed outside of your tenant's geographical region, compliance boundary or national cloud instance.",
|
||||
),
|
||||
);
|
||||
terminal.writeln(
|
||||
formatInfoMessage(" which may affect compliance with data residency requirements or regulations specific"),
|
||||
);
|
||||
terminal.writeln(formatInfoMessage(" to your organization."));
|
||||
terminal.writeln("");
|
||||
|
||||
terminal.writeln("\x1b[94mFor more information on Azure Cosmos DB data governance and compliance, please visit:");
|
||||
terminal.writeln("\x1b[94mFor more information on Azure Cosmos DB data residency, please visit:");
|
||||
terminal.writeln("\x1b[94mhttps://learn.microsoft.com/en-us/azure/cosmos-db/data-residency\x1b[0m");
|
||||
|
||||
// Ask for user consent for region
|
||||
@@ -159,7 +154,9 @@ export const ensureCloudShellProviderRegistered = async (): Promise<void> => {
|
||||
* Determines the appropriate CloudShell region
|
||||
*/
|
||||
export const determineCloudShellRegion = (): string => {
|
||||
return getNormalizedRegion(userContext.databaseAccount?.location, DEFAULT_CLOUDSHELL_REGION);
|
||||
const defaultRegion =
|
||||
userContext.portalEnv === "fairfax" ? DEFAULT_FAIRFAX_CLOUDSHELL_REGION : DEFAULT_CLOUDSHELL_REGION;
|
||||
return getNormalizedRegion(userContext.databaseAccount?.location, defaultRegion);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -258,14 +258,7 @@ Key limitations:
|
||||
|
||||
### Data Residency
|
||||
|
||||
Data residency requirements may not be fully satisfied when using CloudShell due to limited regional availability. CloudShell services are currently available in the following regions:
|
||||
|
||||
| Geography | Regions |
|
||||
|-----------|---------|
|
||||
| Americas | East US, West US 2, South Central US, West Central US |
|
||||
| Europe | West Europe, North Europe |
|
||||
| Asia Pacific | Southeast Asia, Japan East, Australia East |
|
||||
| Middle East | UAE North |
|
||||
Data residency requirements may not be fully satisfied when using CloudShell due to limited regional availability.
|
||||
|
||||
**Note:** For up-to-date supported regions, refer to the region configuration in:
|
||||
`src/Explorer/CloudShell/Configuration/RegionConfig.ts`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AbstractShellHandler, DISABLE_HISTORY, START_MARKER, EXIT_COMMAND } from "./AbstractShellHandler";
|
||||
import { AbstractShellHandler, DISABLE_HISTORY, EXIT_COMMAND, START_MARKER } from "./AbstractShellHandler";
|
||||
|
||||
// Mock implementation for testing
|
||||
class MockShellHandler extends AbstractShellHandler {
|
||||
@@ -18,8 +18,8 @@ class MockShellHandler extends AbstractShellHandler {
|
||||
return "mock-endpoint";
|
||||
}
|
||||
|
||||
getTerminalSuppressedData(): string {
|
||||
return "suppressed-data";
|
||||
getTerminalSuppressedData(): string[] {
|
||||
return ["suppressed-data"];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ describe("AbstractShellHandler", () => {
|
||||
});
|
||||
|
||||
it("should return the terminal suppressed data", () => {
|
||||
expect(shellHandler.getTerminalSuppressedData()).toBe("suppressed-data");
|
||||
expect(shellHandler.getTerminalSuppressedData()).toEqual(["suppressed-data"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,18 @@ export const DISABLE_HISTORY = `set +o history`;
|
||||
* Command that displays an error message and exits the shell session.
|
||||
* Used when shell initialization or connection fails.
|
||||
*/
|
||||
export const EXIT_COMMAND = ` printf "\\033[1;31mSession ended. Please close this tab and initiate a new shell session if needed.\\033[0m\\n" && exit`;
|
||||
export const EXIT_COMMAND = ` printf "\\033[1;31mSession ended. Please close this tab and initiate a new shell session if needed.\\033[0m\\n" && disown -a && exit`;
|
||||
/**
|
||||
* Command that displays error message with MongoDB networking guidance and exits the shell session.
|
||||
* Used when MongoDB shell connection fails due to networking issues.
|
||||
*/
|
||||
export const EXIT_COMMAND_MONGO = ` printf "\\033[1;31mSession ended. Please close this tab and initiate a new shell session if needed.\\033[0m\\n" && printf "\\033[1;36mPlease use the 'Add Azure Cloud Shell IPs' button in the Networking blade to allow Cloud Shell access, if not already configured.\\033[0m\\n" && disown -a && exit`;
|
||||
|
||||
/**
|
||||
* This command runs mongosh in no-database and quiet mode,
|
||||
* and evaluates the `disableTelemetry()` function to turn off telemetry collection.
|
||||
*/
|
||||
export const DISABLE_TELEMETRY_COMMAND = `mongosh --nodb --quiet --eval 'disableTelemetry()'`;
|
||||
|
||||
/**
|
||||
* Abstract class that defines the interface for shell-specific handlers
|
||||
@@ -31,7 +42,16 @@ export abstract class AbstractShellHandler {
|
||||
abstract getShellName(): string;
|
||||
abstract getSetUpCommands(): string[];
|
||||
abstract getConnectionCommand(): string;
|
||||
abstract getTerminalSuppressedData(): string;
|
||||
abstract getTerminalSuppressedData(): string[];
|
||||
updateTerminalData?(data: string): string;
|
||||
|
||||
/**
|
||||
* Gets the exit command to use when connection fails.
|
||||
* Can be overridden by subclasses to provide custom exit commands.
|
||||
*/
|
||||
protected getExitCommand(): string {
|
||||
return EXIT_COMMAND;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the complete initialization command sequence for the shell.
|
||||
@@ -57,7 +77,7 @@ export abstract class AbstractShellHandler {
|
||||
START_MARKER,
|
||||
DISABLE_HISTORY,
|
||||
...setupCommands,
|
||||
`{ ${connectionCommand}; } || true;${EXIT_COMMAND}`,
|
||||
`{ ${connectionCommand}; } || true;${this.getExitCommand()}`,
|
||||
];
|
||||
|
||||
return allCommands.join("\n").concat("\n");
|
||||
@@ -77,7 +97,7 @@ export abstract class AbstractShellHandler {
|
||||
* is not already present in the environment.
|
||||
*/
|
||||
protected mongoShellSetupCommands(): string[] {
|
||||
const PACKAGE_VERSION: string = "2.5.0";
|
||||
const PACKAGE_VERSION: string = "2.5.6";
|
||||
return [
|
||||
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
|
||||
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
||||
@@ -85,7 +105,7 @@ export abstract class AbstractShellHandler {
|
||||
`if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh/bin && mv mongosh-${PACKAGE_VERSION}-linux-x64/bin/mongosh ~/mongosh/bin/ && chmod +x ~/mongosh/bin/mongosh; fi`,
|
||||
`if ! command -v mongosh &> /dev/null; then rm -rf mongosh-${PACKAGE_VERSION}-linux-x64 mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
|
||||
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
|
||||
"source ~/.bashrc",
|
||||
"if ! command -v mongosh &> /dev/null; then source ~/.bashrc; fi",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("CassandraShellHandler", () => {
|
||||
});
|
||||
|
||||
test("should return the correct terminal suppressed data", () => {
|
||||
expect(handler.getTerminalSuppressedData()).toBe("");
|
||||
expect(handler.getTerminalSuppressedData()).toEqual([""]);
|
||||
});
|
||||
|
||||
test("should include the correct package version in setup commands", () => {
|
||||
|
||||
@@ -41,7 +41,7 @@ export class CassandraShellHandler extends AbstractShellHandler {
|
||||
return `cqlsh ${getHostFromUrl(this._endpoint)} 10350 -u ${dbName} -p ${this._key} --ssl`;
|
||||
}
|
||||
|
||||
public getTerminalSuppressedData(): string {
|
||||
return "";
|
||||
public getTerminalSuppressedData(): string[] {
|
||||
return [""];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,12 @@ interface DatabaseAccount {
|
||||
|
||||
interface UserContextType {
|
||||
databaseAccount: DatabaseAccount;
|
||||
features: {
|
||||
enableAadDataPlane: boolean;
|
||||
};
|
||||
apiType: string;
|
||||
dataPlaneRbacEnabled: boolean;
|
||||
aadToken?: string;
|
||||
}
|
||||
|
||||
// Mock dependencies
|
||||
@@ -29,10 +35,13 @@ jest.mock("../../../../UserContext", () => ({
|
||||
mongoEndpoint: "https://test-mongo.documents.azure.com:443/",
|
||||
},
|
||||
},
|
||||
features: { enableAadDataPlane: false },
|
||||
apiType: "Mongo",
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../Utils/CommonUtils", () => ({
|
||||
...jest.requireActual("../Utils/CommonUtils"),
|
||||
getHostFromUrl: jest.fn().mockReturnValue("test-mongo.documents.azure.com"),
|
||||
}));
|
||||
|
||||
@@ -69,7 +78,7 @@ describe("MongoShellHandler", () => {
|
||||
|
||||
expect(Array.isArray(commands)).toBe(true);
|
||||
expect(commands.length).toBe(7);
|
||||
expect(commands[1]).toContain("mongosh-2.5.0-linux-x64.tgz");
|
||||
expect(commands[1]).toContain("mongosh-2.5.6-linux-x64.tgz");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -87,11 +96,12 @@ describe("MongoShellHandler", () => {
|
||||
kind: "test-kind",
|
||||
properties: { mongoEndpoint: "https://test-mongo.documents.azure.com:443/" },
|
||||
};
|
||||
(userContext as UserContextType).dataPlaneRbacEnabled = false;
|
||||
|
||||
const command = mongoShellHandler.getConnectionCommand();
|
||||
|
||||
expect(command).toBe(
|
||||
"mongosh mongodb://test-mongo.documents.azure.com:10255?appName=CosmosExplorerTerminal --username test-account --password test-key --tls --tlsAllowInvalidCertificates",
|
||||
"mongosh --nodb --quiet --eval 'disableTelemetry()'; mongosh mongodb://test-mongo.documents.azure.com:10255?appName=CosmosExplorerTerminal --username test-account --password test-key --tls --tlsAllowInvalidCertificates",
|
||||
);
|
||||
expect(CommonUtils.getHostFromUrl).toHaveBeenCalledWith("https://test-mongo.documents.azure.com:443/");
|
||||
|
||||
@@ -114,17 +124,55 @@ describe("MongoShellHandler", () => {
|
||||
};
|
||||
|
||||
const command = mongoShellHandler.getConnectionCommand();
|
||||
|
||||
expect(command).toBe("echo 'Database name not found.'");
|
||||
|
||||
// Restore original
|
||||
(userContext as UserContextType).databaseAccount = originalDatabaseAccount;
|
||||
});
|
||||
|
||||
it("should return echo if endpoint is missing", () => {
|
||||
const testKey = "test-key";
|
||||
(userContext as UserContextType).databaseAccount = {
|
||||
id: "test-id",
|
||||
name: "", // Empty name to simulate missing name
|
||||
location: "test-location",
|
||||
type: "test-type",
|
||||
kind: "test-kind",
|
||||
properties: { mongoEndpoint: "" },
|
||||
};
|
||||
const mongoShellHandler = new MongoShellHandler(testKey);
|
||||
const command = mongoShellHandler.getConnectionCommand();
|
||||
expect(command).toBe("echo 'MongoDB endpoint not found.'");
|
||||
});
|
||||
|
||||
it("should use _getAadConnectionCommand when _isEntraIdEnabled is true", () => {
|
||||
const testKey = "aad-key";
|
||||
(userContext as UserContextType).databaseAccount = {
|
||||
id: "test-id",
|
||||
name: "test-account",
|
||||
location: "test-location",
|
||||
type: "test-type",
|
||||
kind: "test-kind",
|
||||
properties: { mongoEndpoint: "https://test-mongo.documents.azure.com:443/" },
|
||||
};
|
||||
(userContext as UserContextType).dataPlaneRbacEnabled = true;
|
||||
|
||||
const mongoShellHandler = new MongoShellHandler(testKey);
|
||||
|
||||
const command = mongoShellHandler.getConnectionCommand();
|
||||
expect(command).toContain(
|
||||
"mongosh 'mongodb://test-account:aad-key@test-account.mongo.cosmos.azure.com:10255/?ssl=true&replicaSet=globaldb&authMechanism=PLAIN&retryWrites=false' --tls --tlsAllowInvalidCertificates",
|
||||
);
|
||||
expect(command.startsWith("mongosh --nodb")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTerminalSuppressedData", () => {
|
||||
it("should return the correct warning message", () => {
|
||||
expect(mongoShellHandler.getTerminalSuppressedData()).toBe("Warning: Non-Genuine MongoDB Detected");
|
||||
expect(mongoShellHandler.getTerminalSuppressedData()).toEqual([
|
||||
"Warning: Non-Genuine MongoDB Detected",
|
||||
"Telemetry is now disabled.",
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
import { userContext } from "../../../../UserContext";
|
||||
import { getHostFromUrl } from "../Utils/CommonUtils";
|
||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
||||
import { isDataplaneRbacEnabledForProxyApi } from "../../../../Utils/AuthorizationUtils";
|
||||
import { filterAndCleanTerminalOutput, getHostFromUrl, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
|
||||
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND, EXIT_COMMAND_MONGO } from "./AbstractShellHandler";
|
||||
|
||||
export class MongoShellHandler extends AbstractShellHandler {
|
||||
private _key: string;
|
||||
private _endpoint: string | undefined;
|
||||
private _removeInfoText: string[] = getMongoShellRemoveInfoText();
|
||||
private _isEntraIdEnabled: boolean = isDataplaneRbacEnabledForProxyApi(userContext);
|
||||
constructor(private key: string) {
|
||||
super();
|
||||
this._key = key;
|
||||
this._endpoint = userContext?.databaseAccount?.properties?.mongoEndpoint;
|
||||
}
|
||||
|
||||
private _getKeyConnectionCommand(dbName: string): string {
|
||||
return `mongosh mongodb://${getHostFromUrl(this._endpoint)}:10255?appName=${
|
||||
this.APP_NAME
|
||||
} --username ${dbName} --password ${this._key} --tls --tlsAllowInvalidCertificates`;
|
||||
}
|
||||
|
||||
private _getAadConnectionCommand(dbName: string): string {
|
||||
return `mongosh 'mongodb://${dbName}:${this._key}@${dbName}.mongo.cosmos.azure.com:10255/?ssl=true&replicaSet=globaldb&authMechanism=PLAIN&retryWrites=false' --tls --tlsAllowInvalidCertificates`;
|
||||
}
|
||||
|
||||
public getShellName(): string {
|
||||
return "MongoDB";
|
||||
}
|
||||
@@ -28,20 +41,22 @@ export class MongoShellHandler extends AbstractShellHandler {
|
||||
if (!dbName) {
|
||||
return "echo 'Database name not found.'";
|
||||
}
|
||||
return (
|
||||
"mongosh mongodb://" +
|
||||
getHostFromUrl(this._endpoint) +
|
||||
":10255?appName=" +
|
||||
this.APP_NAME +
|
||||
" --username " +
|
||||
dbName +
|
||||
" --password " +
|
||||
this._key +
|
||||
" --tls --tlsAllowInvalidCertificates"
|
||||
);
|
||||
const connectionCommand = this._isEntraIdEnabled
|
||||
? this._getAadConnectionCommand(dbName)
|
||||
: this._getKeyConnectionCommand(dbName);
|
||||
const fullCommand = `${DISABLE_TELEMETRY_COMMAND}; ${connectionCommand}`;
|
||||
return fullCommand;
|
||||
}
|
||||
|
||||
public getTerminalSuppressedData(): string {
|
||||
return "Warning: Non-Genuine MongoDB Detected";
|
||||
public getTerminalSuppressedData(): string[] {
|
||||
return ["Warning: Non-Genuine MongoDB Detected", "Telemetry is now disabled."];
|
||||
}
|
||||
|
||||
protected getExitCommand(): string {
|
||||
return EXIT_COMMAND_MONGO;
|
||||
}
|
||||
|
||||
updateTerminalData(data: string): string {
|
||||
return filterAndCleanTerminalOutput(data, this._removeInfoText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ describe("PostgresShellHandler", () => {
|
||||
});
|
||||
|
||||
it("should return empty string for terminal suppressed data", () => {
|
||||
expect(postgresShellHandler.getTerminalSuppressedData()).toBe("");
|
||||
expect(postgresShellHandler.getTerminalSuppressedData()).toEqual([""]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ export class PostgresShellHandler extends AbstractShellHandler {
|
||||
return `psql -h "${this._endpoint}" -p 5432 -d "citus" -U "${loginName}" --set=sslmode=require --set=application_name=${this.APP_NAME}`;
|
||||
}
|
||||
|
||||
public getTerminalSuppressedData(): string {
|
||||
return "";
|
||||
public getTerminalSuppressedData(): string[] {
|
||||
return [""];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,24 @@ import { PostgresShellHandler } from "./PostgresShellHandler";
|
||||
import { getHandler, getKey } from "./ShellTypeFactory";
|
||||
import { VCoreMongoShellHandler } from "./VCoreMongoShellHandler";
|
||||
|
||||
interface UserContextType {
|
||||
databaseAccount: { name: string };
|
||||
subscriptionId: string;
|
||||
resourceGroup: string;
|
||||
features: { enableAadDataPlane: boolean };
|
||||
dataPlaneRbacEnabled: boolean;
|
||||
aadToken?: string;
|
||||
apiType?: string;
|
||||
}
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock("../../../../UserContext", () => ({
|
||||
userContext: {
|
||||
databaseAccount: { name: "testDbName" },
|
||||
subscriptionId: "testSubId",
|
||||
resourceGroup: "testResourceGroup",
|
||||
features: { enableAadDataPlane: false },
|
||||
dataPlaneRbacEnabled: false,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -109,5 +121,33 @@ describe("ShellTypeHandlerFactory", () => {
|
||||
expect(key).toBe(mockKey);
|
||||
expect(listKeys).toHaveBeenCalledWith("testSubId", "testResourceGroup", "testDbName");
|
||||
});
|
||||
|
||||
it("should return MongoShellHandler with primaryMasterKey for TerminalKind.Mongo when RBAC is disabled", async () => {
|
||||
(listKeys as jest.Mock).mockResolvedValue({ primaryMasterKey: "primaryKey123" });
|
||||
(userContext as UserContextType).features.enableAadDataPlane = false;
|
||||
(userContext as UserContextType).dataPlaneRbacEnabled = false;
|
||||
const handler = await getHandler(TerminalKind.Mongo);
|
||||
expect(handler).toBeInstanceOf(MongoShellHandler);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
expect(handler.key).toBe("primaryKey123");
|
||||
});
|
||||
|
||||
it("should return MongoShellHandler with aadToken for TerminalKind.Mongo when RBAC is enabled", async () => {
|
||||
(userContext as UserContextType).aadToken = "aadToken123";
|
||||
(userContext as UserContextType).features.enableAadDataPlane = true;
|
||||
(userContext as UserContextType).dataPlaneRbacEnabled = true;
|
||||
(userContext as UserContextType).apiType = "Mongo";
|
||||
const handler = await getHandler(TerminalKind.Mongo);
|
||||
expect(handler).toBeInstanceOf(MongoShellHandler);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
expect(handler.key).toBe("aadToken123");
|
||||
});
|
||||
it("should throw error for unsupported shell type", async () => {
|
||||
await expect(getHandler("UnknownShell" as unknown as TerminalKind)).rejects.toThrow(
|
||||
"Unsupported shell type: UnknownShell",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TerminalKind } from "../../../../Contracts/ViewModels";
|
||||
import { userContext } from "../../../../UserContext";
|
||||
import { listKeys } from "../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import { isDataplaneRbacEnabledForProxyApi } from "../../../../Utils/AuthorizationUtils";
|
||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
||||
import { CassandraShellHandler } from "./CassandraShellHandler";
|
||||
import { MongoShellHandler } from "./MongoShellHandler";
|
||||
@@ -30,6 +31,9 @@ export async function getKey(): Promise<string> {
|
||||
if (!dbName) {
|
||||
return "";
|
||||
}
|
||||
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
|
||||
return userContext.aadToken || "";
|
||||
}
|
||||
|
||||
const keys = await listKeys(userContext.subscriptionId, userContext.resourceGroup, dbName);
|
||||
return keys?.primaryMasterKey || "";
|
||||
|
||||
@@ -45,7 +45,7 @@ describe("VCoreMongoShellHandler", () => {
|
||||
|
||||
expect(Array.isArray(commands)).toBe(true);
|
||||
expect(commands.length).toBe(7);
|
||||
expect(commands[1]).toContain("mongosh-2.5.0-linux-x64.tgz");
|
||||
expect(commands[1]).toContain("mongosh-2.5.6-linux-x64.tgz");
|
||||
expect(commands[0]).toContain("mongosh not found");
|
||||
});
|
||||
|
||||
@@ -57,7 +57,10 @@ describe("VCoreMongoShellHandler", () => {
|
||||
});
|
||||
|
||||
it("should return the correct terminal suppressed data", () => {
|
||||
expect(vcoreMongoShellHandler.getTerminalSuppressedData()).toBe("Warning: Non-Genuine MongoDB Detected");
|
||||
expect(vcoreMongoShellHandler.getTerminalSuppressedData()).toEqual([
|
||||
"Warning: Non-Genuine MongoDB Detected",
|
||||
"Telemetry is now disabled.",
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { userContext } from "../../../../UserContext";
|
||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
||||
import { filterAndCleanTerminalOutput, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
|
||||
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND, EXIT_COMMAND_MONGO } from "./AbstractShellHandler";
|
||||
|
||||
export class VCoreMongoShellHandler extends AbstractShellHandler {
|
||||
private _endpoint: string | undefined;
|
||||
private _removeInfoText: string[] = getMongoShellRemoveInfoText();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -23,10 +25,24 @@ export class VCoreMongoShellHandler extends AbstractShellHandler {
|
||||
}
|
||||
|
||||
const userName = userContext.vcoreMongoConnectionParams.adminLogin;
|
||||
return `mongosh "mongodb+srv://${userName}:@${this._endpoint}/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000&appName=${this.APP_NAME}"`;
|
||||
|
||||
const connectionUri = `mongodb+srv://${userName}:@${this._endpoint}/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000&appName=${this.APP_NAME}`;
|
||||
|
||||
return `${DISABLE_TELEMETRY_COMMAND} && mongosh "${connectionUri}"`;
|
||||
}
|
||||
|
||||
public getTerminalSuppressedData(): string {
|
||||
return "Warning: Non-Genuine MongoDB Detected";
|
||||
public getTerminalSuppressedData(): string[] {
|
||||
return ["Warning: Non-Genuine MongoDB Detected", "Telemetry is now disabled."];
|
||||
}
|
||||
|
||||
/**
|
||||
* Override getExitCommand to include MongoDB networking guidance
|
||||
*/
|
||||
protected getExitCommand(): string {
|
||||
return EXIT_COMMAND_MONGO;
|
||||
}
|
||||
|
||||
updateTerminalData(data: string): string {
|
||||
return filterAndCleanTerminalOutput(data, this._removeInfoText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,18 @@ export class AttachAddon implements ITerminalAddon {
|
||||
* @param {Terminal} terminal - The XTerm terminal instance
|
||||
*/
|
||||
public addMessageListener(terminal: Terminal): void {
|
||||
let messageBuffer = "";
|
||||
let bufferTimeout: NodeJS.Timeout | null = null;
|
||||
const BUFFER_TIMEOUT = 50; // ms - short timeout for prompt detection
|
||||
|
||||
const processBuffer = () => {
|
||||
if (messageBuffer.length > 0) {
|
||||
this.handleCompleteTerminalData(terminal, messageBuffer);
|
||||
messageBuffer = "";
|
||||
}
|
||||
bufferTimeout = null;
|
||||
};
|
||||
|
||||
this._disposables.push(
|
||||
addSocketListener(this._socket, "message", (ev) => {
|
||||
let data: ArrayBuffer | string = ev.data;
|
||||
@@ -103,51 +115,136 @@ export class AttachAddon implements ITerminalAddon {
|
||||
data = enc.decode(ev.data as ArrayBuffer);
|
||||
}
|
||||
|
||||
// for example of json object look in TerminalHelper in the socket.onMessage
|
||||
if (data.includes(startStatusJson) && data.includes(endStatusJson)) {
|
||||
// process as one line
|
||||
const statusData = data.split(startStatusJson)[1].split(endStatusJson)[0];
|
||||
data = data.replace(statusData, "");
|
||||
data = data.replace(startStatusJson, "");
|
||||
data = data.replace(endStatusJson, "");
|
||||
} else if (data.includes(startStatusJson)) {
|
||||
// check for start
|
||||
const partialStatusData = data.split(startStatusJson)[1];
|
||||
this._socketData += partialStatusData;
|
||||
data = data.replace(partialStatusData, "");
|
||||
data = data.replace(startStatusJson, "");
|
||||
} else if (data.includes(endStatusJson)) {
|
||||
// check for end and process the command
|
||||
const partialStatusData = data.split(endStatusJson)[0];
|
||||
this._socketData += partialStatusData;
|
||||
data = data.replace(partialStatusData, "");
|
||||
data = data.replace(endStatusJson, "");
|
||||
this._socketData = "";
|
||||
} else if (this._socketData.length > 0) {
|
||||
// check if the line is all data then just concatenate
|
||||
this._socketData += data;
|
||||
data = "";
|
||||
}
|
||||
// Handle status messages
|
||||
let processedStatusData = data;
|
||||
|
||||
if (this._allowTerminalWrite && data.includes(this._startMarker)) {
|
||||
this._allowTerminalWrite = false;
|
||||
terminal.write(`Preparing ${this._shellHandler.getShellName()} environment...\r\n`);
|
||||
}
|
||||
|
||||
if (this._allowTerminalWrite) {
|
||||
const suppressedData = this._shellHandler?.getTerminalSuppressedData();
|
||||
const hasSuppressedData = suppressedData && suppressedData.length > 0;
|
||||
|
||||
if (!hasSuppressedData || !data.includes(suppressedData)) {
|
||||
terminal.write(data);
|
||||
// Process status messages with delimiters
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const startIndex = processedStatusData.indexOf(startStatusJson);
|
||||
if (startIndex === -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
const afterStart = processedStatusData.substring(startIndex + startStatusJson.length);
|
||||
const endIndex = afterStart.indexOf(endStatusJson);
|
||||
|
||||
if (endIndex === -1) {
|
||||
// Incomplete status message
|
||||
this._socketData += processedStatusData.substring(startIndex);
|
||||
processedStatusData = processedStatusData.substring(0, startIndex);
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove processed status message
|
||||
processedStatusData =
|
||||
processedStatusData.substring(0, startIndex) + afterStart.substring(endIndex + endStatusJson.length);
|
||||
}
|
||||
|
||||
if (data.includes(this._shellHandler.getConnectionCommand())) {
|
||||
this._allowTerminalWrite = true;
|
||||
// Add to message buffer
|
||||
messageBuffer += processedStatusData;
|
||||
|
||||
// Clear existing timeout
|
||||
if (bufferTimeout) {
|
||||
clearTimeout(bufferTimeout);
|
||||
bufferTimeout = null;
|
||||
}
|
||||
|
||||
// Check if this looks like a complete message/command
|
||||
const isComplete = this.isMessageComplete(messageBuffer, processedStatusData);
|
||||
|
||||
if (isComplete) {
|
||||
// Message marked as complete, processing immediately
|
||||
processBuffer();
|
||||
} else {
|
||||
// Set timeout to process buffer after delay
|
||||
bufferTimeout = setTimeout(processBuffer, BUFFER_TIMEOUT);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Clean up timeout on dispose
|
||||
this._disposables.push({
|
||||
dispose: () => {
|
||||
if (bufferTimeout) {
|
||||
clearTimeout(bufferTimeout);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private isMessageComplete(fullBuffer: string, currentChunk: string): boolean {
|
||||
// Immediate completion indicators
|
||||
const immediateCompletionPatterns = [
|
||||
/\n$/, // Ends with newline
|
||||
/\r$/, // Ends with carriage return
|
||||
/\r\n$/, // Ends with CRLF
|
||||
/; \} \|\| true;$/, // Your command pattern
|
||||
/disown -a && exit$/, // Exit commands
|
||||
/printf.*?\\033\[0m\\n"$/, // Your printf pattern
|
||||
];
|
||||
|
||||
// Check current chunk for immediate completion
|
||||
for (const pattern of immediateCompletionPatterns) {
|
||||
if (pattern.test(currentChunk)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ANSI sequence detection - these might be complete prompts
|
||||
const ansiPromptPatterns = [
|
||||
/\[\d+G\[0J.*>\s*\[\d+G$/, // Your specific pattern: [1G[0J...> [26G
|
||||
/\[\d+;\d+H/, // Cursor position sequences
|
||||
/\]\s*\[\d+G$/, // Ends with cursor positioning
|
||||
/>\s*\[\d+G$/, // Prompt followed by cursor position
|
||||
];
|
||||
|
||||
// Check if buffer ends with what looks like a complete prompt
|
||||
for (const pattern of ansiPromptPatterns) {
|
||||
if (pattern.test(fullBuffer)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for MongoDB shell prompts specifically
|
||||
const mongoPromptPatterns = [
|
||||
/globaldb \[primary\] \w+>\s*\[\d+G$/, // MongoDB replica set prompt
|
||||
/>\s*\[\d+G$/, // General prompt with cursor positioning
|
||||
/\w+>\s*$/, // Simple shell prompt
|
||||
];
|
||||
|
||||
for (const pattern of mongoPromptPatterns) {
|
||||
if (pattern.test(fullBuffer)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private handleCompleteTerminalData(terminal: Terminal, data: string): void {
|
||||
if (this._allowTerminalWrite && data.includes(this._startMarker)) {
|
||||
this._allowTerminalWrite = false;
|
||||
terminal.write(`Preparing ${this._shellHandler.getShellName()} environment...\r\n`);
|
||||
}
|
||||
|
||||
if (this._allowTerminalWrite) {
|
||||
const updatedData =
|
||||
typeof this._shellHandler?.updateTerminalData === "function"
|
||||
? this._shellHandler.updateTerminalData(data)
|
||||
: data;
|
||||
|
||||
const suppressedData = this._shellHandler?.getTerminalSuppressedData();
|
||||
const shouldNotWrite = suppressedData.filter(Boolean).some((item) => updatedData.includes(item));
|
||||
|
||||
if (!shouldNotWrite) {
|
||||
terminal.write(updatedData);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.includes(this._shellHandler.getConnectionCommand())) {
|
||||
this._allowTerminalWrite = true;
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user