mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-07 19:46:53 +00:00
Compare commits
32 Commits
index-arch
...
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 |
@@ -23,8 +23,6 @@ src/Common/MongoUtility.ts
|
|||||||
src/Common/NotificationsClientBase.ts
|
src/Common/NotificationsClientBase.ts
|
||||||
src/Common/QueriesClient.ts
|
src/Common/QueriesClient.ts
|
||||||
src/Common/Splitter.ts
|
src/Common/Splitter.ts
|
||||||
src/Controls/Heatmap/Heatmap.test.ts
|
|
||||||
src/Controls/Heatmap/Heatmap.ts
|
|
||||||
src/Definitions/datatables.d.ts
|
src/Definitions/datatables.d.ts
|
||||||
src/Definitions/gif.d.ts
|
src/Definitions/gif.d.ts
|
||||||
src/Definitions/globals.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"
|
- name: "Az CLI login"
|
||||||
uses: Azure/login@v2
|
uses: Azure/login@v2
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_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']}}
|
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
|
||||||
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
|
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
|
||||||
- name: Upload blob report to GitHub Actions Artifacts
|
- 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"
|
- name: "Az CLI login"
|
||||||
uses: azure/login@v1
|
uses: azure/login@v1
|
||||||
with:
|
with:
|
||||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
|
||||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||||
|
|
||||||
|
|||||||
2
.npmrc
2
.npmrc
@@ -1,4 +1,4 @@
|
|||||||
save-exact=true
|
save-exact=true
|
||||||
|
|
||||||
# Ignore peer dependency conflicts
|
# 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.
|
// 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",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
@@ -24,5 +8,8 @@
|
|||||||
"source.organizeImports": "explicit"
|
"source.organizeImports": "explicit"
|
||||||
},
|
},
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"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",
|
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||||
"isTerminalEnabled": true,
|
|
||||||
"isPhoenixEnabled": true
|
"isPhoenixEnabled": true
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
|
||||||
"isTerminalEnabled" : false,
|
"isPhoenixEnabled": false
|
||||||
"isPhoenixEnabled" : false
|
}
|
||||||
}
|
|
||||||
38181
package-lock.json
generated
38181
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",
|
"description": "Cosmos Explorer",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/arm-cosmosdb": "9.1.0",
|
"@azure/arm-cosmosdb": "16.3.0",
|
||||||
"@azure/cosmos": "4.3.0",
|
"@azure/cosmos": "4.5.0",
|
||||||
"@azure/cosmos-language-service": "0.0.5",
|
"@azure/cosmos-language-service": "0.0.5",
|
||||||
"@azure/identity": "4.5.0",
|
"@azure/identity": "4.10.1",
|
||||||
"@azure/msal-browser": "2.14.2",
|
"@azure/msal-browser": "4.24.0",
|
||||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
"@babel/plugin-transform-class-properties": "^7.24.7",
|
||||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
"@babel/plugin-proposal-decorators": "7.28.0",
|
||||||
"@fluentui/react": "8.119.0",
|
"@fluentui/react": "8.123.6",
|
||||||
"@fluentui/react-components": "9.54.2",
|
"@fluentui/react-components": "9.70.0",
|
||||||
"@jupyterlab/services": "6.0.2",
|
"@jupyterlab/services": "7.4.9",
|
||||||
"@jupyterlab/terminal": "3.0.3",
|
"@jupyterlab/terminal": "4.4.9",
|
||||||
"@microsoft/applicationinsights-web": "2.6.1",
|
"@microsoft/applicationinsights-web": "3.3.10",
|
||||||
"@nteract/commutable": "7.5.1",
|
"@nteract/commutable": "7.5.1",
|
||||||
"@nteract/connected-components": "6.8.2",
|
"@nteract/connected-components": "6.9.0",
|
||||||
"@nteract/core": "15.1.9",
|
"@nteract/core": "15.1.9",
|
||||||
"@nteract/data-explorer": "8.0.3",
|
"@nteract/data-explorer": "8.2.12",
|
||||||
"@nteract/directory-listing": "2.0.6",
|
"@nteract/directory-listing": "2.1.0",
|
||||||
"@nteract/dropdown-menu": "1.0.1",
|
"@nteract/dropdown-menu": "1.1.9",
|
||||||
"@nteract/editor": "10.1.12",
|
"@nteract/editor": "10.1.12",
|
||||||
"@nteract/fixtures": "2.3.0",
|
"@nteract/fixtures": "2.3.19",
|
||||||
"@nteract/iron-icons": "1.0.0",
|
"@nteract/iron-icons": "1.0.0",
|
||||||
"@nteract/jupyter-widgets": "2.0.0",
|
"@nteract/jupyter-widgets": "4.1.19",
|
||||||
"@nteract/logos": "1.0.0",
|
"@nteract/logos": "1.0.0",
|
||||||
"@nteract/markdown": "4.6.0",
|
"@nteract/markdown": "4.6.2",
|
||||||
"@nteract/monaco-editor": "3.2.2",
|
"@nteract/monaco-editor": "3.2.2",
|
||||||
"@nteract/octicons": "2.0.0",
|
"@nteract/octicons": "2.0.0",
|
||||||
"@nteract/outputs": "3.0.9",
|
"@nteract/outputs": "5.1.14",
|
||||||
"@nteract/presentational-components": "3.0.7",
|
"@nteract/presentational-components": "3.4.12",
|
||||||
"@nteract/stateful-components": "1.7.0",
|
"@nteract/stateful-components": "1.7.15",
|
||||||
"@nteract/styles": "2.0.2",
|
"@nteract/styles": "2.2.11",
|
||||||
"@nteract/transform-geojson": "5.1.8",
|
"@nteract/transform-geojson": "5.1.13",
|
||||||
"@nteract/transform-model-debug": "5.0.1",
|
"@nteract/transform-model-debug": "5.0.1",
|
||||||
"@nteract/transform-plotly": "6.1.6",
|
"@nteract/transform-plotly": "7.0.1",
|
||||||
"@nteract/transform-vdom": "4.0.11",
|
"@nteract/transform-vdom": "4.0.15",
|
||||||
"@nteract/transform-vega": "7.0.6",
|
"@nteract/transform-vega": "7.0.6",
|
||||||
"@octokit/rest": "17.9.2",
|
"@octokit/rest": "21.1.1",
|
||||||
"@phosphor/widgets": "1.9.3",
|
"@phosphor/widgets": "1.9.3",
|
||||||
"@testing-library/jest-dom": "6.4.6",
|
"@testing-library/jest-dom": "6.8.0",
|
||||||
"@types/lodash": "4.14.171",
|
"@types/lodash": "4.17.20",
|
||||||
"@types/mkdirp": "1.0.1",
|
"@types/mkdirp": "1.0.2",
|
||||||
"@types/node-fetch": "2.5.7",
|
"@types/node-fetch": "2.6.13",
|
||||||
"@xmldom/xmldom": "0.7.13",
|
"@xmldom/xmldom": "0.9.8",
|
||||||
"@xterm/xterm": "5.5.0",
|
"@xterm/xterm": "5.5.0",
|
||||||
"@xterm/addon-fit": "0.10.0",
|
"@xterm/addon-fit": "0.10.0",
|
||||||
"allotment": "1.20.2",
|
"allotment": "1.20.4",
|
||||||
"applicationinsights": "1.8.0",
|
"applicationinsights": "3.12.0",
|
||||||
"bootstrap": "3.4.1",
|
"bootstrap": "3.4.1",
|
||||||
"canvas": "2.11.2",
|
"canvas": "3.2.0",
|
||||||
"clean-webpack-plugin": "4.0.0",
|
"clean-webpack-plugin": "4.0.0",
|
||||||
"clipboard-copy": "4.0.1",
|
"clipboard-copy": "4.0.1",
|
||||||
"copy-webpack-plugin": "11.0.0",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"crossroads": "0.12.2",
|
"crossroads": "0.12.2",
|
||||||
"css-element-queries": "1.1.1",
|
"css-element-queries": "1.2.3",
|
||||||
"d3": "7.8.5",
|
"d3": "7.9.0",
|
||||||
"datatables.net-colreorder-dt": "1.7.0",
|
"datatables.net-colreorder-dt": "2.1.1",
|
||||||
"datatables.net-dt": "1.13.8",
|
"datatables.net-dt": "2.3.4",
|
||||||
"date-fns": "1.29.0",
|
"date-fns": "4.1.0",
|
||||||
"dayjs": "1.8.19",
|
"dayjs": "1.11.18",
|
||||||
"dom-to-image": "2.6.0",
|
"dom-to-image": "2.6.0",
|
||||||
"dotenv": "8.2.0",
|
"dotenv": "17.2.3",
|
||||||
"eslint-plugin-jest": "27.4.2",
|
"eslint-plugin-jest": "28.14.0",
|
||||||
"eslint-plugin-react": "7.33.2",
|
"eslint-plugin-react": "7.37.5",
|
||||||
"hasher": "1.2.0",
|
"hasher": "1.2.0",
|
||||||
"html2canvas": "1.0.0-rc.5",
|
"html2canvas": "1.4.1",
|
||||||
"i18next": "23.11.5",
|
"i18next": "25.5.2",
|
||||||
"i18next-browser-languagedetector": "6.0.1",
|
"i18next-browser-languagedetector": "8.2.0",
|
||||||
"i18next-http-backend": "1.0.23",
|
"i18next-http-backend": "3.0.2",
|
||||||
"iframe-resizer-react": "1.1.0",
|
"iframe-resizer-react": "5.1.5",
|
||||||
"immer": "9.0.6",
|
"immer": "10.1.3",
|
||||||
"immutable": "4.0.0-rc.12",
|
"immutable": "4.0.0-rc.12",
|
||||||
"is-ci": "2.0.0",
|
"is-ci": "4.1.0",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"jquery-typeahead": "2.11.1",
|
"jquery-typeahead": "2.11.1",
|
||||||
"jquery-ui-dist": "1.13.2",
|
"jquery-ui-dist": "1.13.3",
|
||||||
"knockout": "3.5.1",
|
"knockout": "3.5.1",
|
||||||
"loader-utils": "2.0.3",
|
"loader-utils": "3.3.1",
|
||||||
"mkdirp": "1.0.4",
|
"mkdirp": "3.0.1",
|
||||||
"monaco-editor": "0.44.0",
|
"monaco-editor": "0.53.0",
|
||||||
"ms": "2.1.3",
|
"ms": "2.1.3",
|
||||||
"p-retry": "6.2.1",
|
"p-retry": "6.2.1",
|
||||||
"patch-package": "8.0.0",
|
"patch-package": "8.0.1",
|
||||||
"plotly.js-cartesian-dist-min": "1.52.3",
|
"plotly.js-cartesian-dist-min": "3.1.1",
|
||||||
"post-robot": "10.0.42",
|
"post-robot": "10.0.42",
|
||||||
"q": "1.5.1",
|
"q": "2.0.3",
|
||||||
"react": "16.14.0",
|
"react": "18.2.0",
|
||||||
"react-animate-height": "2.0.8",
|
"react-animate-height": "3.2.3",
|
||||||
"react-dnd": "14.0.2",
|
"react-dnd": "16.0.1",
|
||||||
"react-dnd-html5-backend": "14.0.0",
|
"react-dnd-html5-backend": "16.0.1",
|
||||||
"react-dom": "16.14.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hotkeys": "2.0.0",
|
"react-hotkeys": "2.0.0",
|
||||||
"react-i18next": "14.1.2",
|
"react-i18next": "16.0.0",
|
||||||
"react-notification-system": "0.2.17",
|
"react-notification-system": "0.2.17",
|
||||||
"react-redux": "7.1.3",
|
"react-redux": "7.2.9",
|
||||||
"react-splitter-layout": "4.0.0",
|
"react-splitter-layout": "4.0.0",
|
||||||
"react-string-format": "1.0.1",
|
"react-string-format": "1.2.0",
|
||||||
"react-window": "1.8.10",
|
"react-window": "1.8.10",
|
||||||
"react-youtube": "9.0.1",
|
"react-youtube": "10.1.0",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.2.2",
|
||||||
"rx-jupyter": "5.5.12",
|
"rx-jupyter": "5.5.21",
|
||||||
"sanitize-html": "2.3.3",
|
"sanitize-html": "2.17.0",
|
||||||
"shell-quote": "1.7.3",
|
"shell-quote": "1.8.3",
|
||||||
"styled-components": "5.0.1",
|
"styled-components": "6.1.19",
|
||||||
"swr": "0.4.0",
|
"swr": "2.3.6",
|
||||||
"terser-webpack-plugin": "5.3.9",
|
"terser-webpack-plugin": "5.3.14",
|
||||||
"tinykeys": "2.1.0",
|
"tinykeys": "3.0.0",
|
||||||
"underscore": "1.12.1",
|
"underscore": "1.13.7",
|
||||||
"utility-types": "3.10.0",
|
"utility-types": "3.11.0",
|
||||||
"zustand": "3.5.0"
|
"zustand": "5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.24.7",
|
"@babel/core": "7.28.4",
|
||||||
"@babel/preset-env": "7.24.7",
|
"@babel/preset-env": "7.28.3",
|
||||||
"@babel/preset-react": "7.24.7",
|
"@babel/preset-react": "7.27.1",
|
||||||
"@babel/preset-typescript": "7.24.7",
|
"@babel/preset-typescript": "7.27.1",
|
||||||
"@playwright/test": "1.49.1",
|
"@playwright/test": "1.55.1",
|
||||||
"@testing-library/react": "11.2.3",
|
"@testing-library/react": "16.3.0",
|
||||||
"@types/applicationinsights-js": "1.0.7",
|
"@types/applicationinsights-js": "1.0.9",
|
||||||
"@types/codemirror": "0.0.56",
|
"@types/codemirror": "5.60.16",
|
||||||
"@types/crossroads": "0.0.30",
|
"@types/crossroads": "0.0.33",
|
||||||
"@types/d3": "5.9.2",
|
"@types/d3": "7.4.3",
|
||||||
"@types/datatables.net": "1.10.28",
|
"@types/datatables.net": "1.10.28",
|
||||||
"@types/datatables.net-colreorder": "1.4.5",
|
"@types/datatables.net-colreorder": "1.4.5",
|
||||||
"@types/dom-to-image": "2.6.2",
|
"@types/dom-to-image": "2.6.7",
|
||||||
"@types/enzyme": "3.10.12",
|
"@types/enzyme": "3.10.19",
|
||||||
"@types/enzyme-adapter-react-16": "1.0.9",
|
"@types/enzyme-adapter-react-16": "1.0.9",
|
||||||
"@types/hasher": "0.0.31",
|
"@types/hasher": "0.0.35",
|
||||||
"@types/jest": "29.5.12",
|
"@types/jest": "30.0.0",
|
||||||
"@types/jquery": "3.5.29",
|
"@types/jquery": "3.5.33",
|
||||||
"@types/node": "12.11.1",
|
"@types/node": "24.6.0",
|
||||||
"@types/post-robot": "10.0.1",
|
"@types/post-robot": "10.0.6",
|
||||||
"@types/q": "1.5.1",
|
"@types/q": "1.5.8",
|
||||||
"@types/react": "17.0.44",
|
"@types/react": "18.3.7",
|
||||||
"@types/react-dom": "17.0.15",
|
"@types/react-dom": "18.3.7",
|
||||||
"@types/react-notification-system": "0.2.39",
|
"@types/react-notification-system": "0.2.46",
|
||||||
"@types/react-redux": "7.1.7",
|
"@types/react-redux": "7.1.34",
|
||||||
"@types/react-splitter-layout": "3.0.1",
|
"@types/react-splitter-layout": "4.0.0",
|
||||||
"@types/react-window": "1.8.8",
|
"@types/react-window": "1.8.8",
|
||||||
"@types/sanitize-html": "1.27.2",
|
"@types/sanitize-html": "2.16.0",
|
||||||
"@types/sinon": "2.3.3",
|
"@types/sinon": "17.0.4",
|
||||||
"@types/styled-components": "5.1.1",
|
"@types/styled-components": "5.1.34",
|
||||||
"@types/underscore": "1.7.36",
|
"@types/underscore": "1.13.0",
|
||||||
"@types/youtube-player": "5.5.6",
|
"@types/youtube-player": "5.5.11",
|
||||||
"@typescript-eslint/eslint-plugin": "6.7.4",
|
"@typescript-eslint/eslint-plugin": "8.45.0",
|
||||||
"@typescript-eslint/parser": "6.7.4",
|
"@typescript-eslint/parser": "8.45.0",
|
||||||
"@webpack-cli/serve": "2.0.5",
|
"@webpack-cli/serve": "3.0.1",
|
||||||
"babel-jest": "29.7.0",
|
"babel-jest": "30.2.0",
|
||||||
"babel-loader": "8.1.0",
|
"babel-loader": "10.0.0",
|
||||||
"buffer": "5.1.0",
|
"buffer": "6.0.3",
|
||||||
"case-sensitive-paths-webpack-plugin": "2.4.0",
|
"case-sensitive-paths-webpack-plugin": "2.4.0",
|
||||||
"create-file-webpack": "1.0.2",
|
"create-file-webpack": "1.0.2",
|
||||||
"css-loader": "6.8.1",
|
"css-loader": "7.1.2",
|
||||||
"enzyme": "3.11.0",
|
"enzyme": "3.11.0",
|
||||||
"enzyme-adapter-react-16": "1.15.8",
|
"enzyme-adapter-react-16": "1.15.8",
|
||||||
"enzyme-to-json": "3.6.2",
|
"enzyme-to-json": "3.6.2",
|
||||||
"eslint": "8.50.0",
|
"eslint": "9.36.0",
|
||||||
"eslint-cli": "1.1.1",
|
"eslint-cli": "1.1.1",
|
||||||
"eslint-plugin-no-null": "1.0.2",
|
"eslint-plugin-no-null": "1.0.2",
|
||||||
"eslint-plugin-prefer-arrow": "1.2.3",
|
"eslint-plugin-prefer-arrow": "1.2.3",
|
||||||
"eslint-plugin-react-hooks": "4.6.0",
|
"eslint-plugin-react-hooks": "5.2.0",
|
||||||
"fast-glob": "3.2.5",
|
"fast-glob": "3.3.3",
|
||||||
"fs-extra": "7.0.0",
|
"fs-extra": "11.3.2",
|
||||||
"html-inline-css-webpack-plugin": "1.11.2",
|
"html-inline-css-webpack-plugin": "1.11.2",
|
||||||
"html-loader": "5.0.0",
|
"html-loader": "5.1.0",
|
||||||
"html-webpack-plugin": "5.5.3",
|
"html-webpack-plugin": "5.6.4",
|
||||||
"jest": "29.7.0",
|
"jest": "30.2.0",
|
||||||
"jest-canvas-mock": "2.5.2",
|
"jest-canvas-mock": "2.5.2",
|
||||||
"jest-circus": "29.7.0",
|
"jest-circus": "30.2.0",
|
||||||
"jest-environment-jsdom": "29.7.0",
|
"jest-environment-jsdom": "30.2.0",
|
||||||
"jest-html-loader": "1.0.0",
|
"jest-html-loader": "1.0.0",
|
||||||
"jest-react-hooks-shallow": "1.5.1",
|
"jest-react-hooks-shallow": "1.5.1",
|
||||||
"jest-trx-results-processor": "3.0.2",
|
"jest-trx-results-processor": "3.0.2",
|
||||||
"less": "3.8.1",
|
"less": "4.4.1",
|
||||||
"less-loader": "11.1.3",
|
"less-loader": "12.3.0",
|
||||||
"less-vars-loader": "1.1.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",
|
"monaco-editor-webpack-plugin": "7.1.0",
|
||||||
"node-fetch": "2.6.7",
|
"node-fetch": "3.3.2",
|
||||||
"prettier": "3.0.3",
|
"prettier": "3.6.2",
|
||||||
"process": "0.11.10",
|
"process": "0.11.10",
|
||||||
"querystring-es3": "0.2.1",
|
"querystring-es3": "0.2.1",
|
||||||
"raw-loader": "0.5.1",
|
"raw-loader": "4.0.2",
|
||||||
"react-dev-utils": "12.0.1",
|
"react-dev-utils": "12.0.1",
|
||||||
"rimraf": "3.0.0",
|
"rimraf": "5.0.10",
|
||||||
"sinon": "3.2.1",
|
"sinon": "21.0.0",
|
||||||
"style-loader": "0.23.0",
|
"style-loader": "4.0.0",
|
||||||
"ts-loader": "9.2.4",
|
"ts-loader": "9.5.4",
|
||||||
"typedoc": "0.26.2",
|
"typedoc": "0.28.13",
|
||||||
"typescript": "4.9.5",
|
"typescript": "5.9.2",
|
||||||
"url-loader": "4.1.1",
|
"url-loader": "4.1.1",
|
||||||
"wait-on": "4.0.2",
|
"wait-on": "8.0.5",
|
||||||
"webpack": "5.88.2",
|
"webpack": "5.102.0",
|
||||||
"webpack-bundle-analyzer": "4.9.1",
|
"webpack-bundle-analyzer": "4.10.2",
|
||||||
"webpack-cli": "5.1.4",
|
"webpack-cli": "6.0.1",
|
||||||
"webpack-dev-server": "4.15.2"
|
"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": {
|
"scripts": {
|
||||||
"postinstall": "patch-package",
|
"postinstall": "patch-package",
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ index e5dc283..1930c2b 100644
|
|||||||
|
|
||||||
/// <reference types="jquery" />
|
/// <reference types="jquery" />
|
||||||
|
|
||||||
-import DataTables, {Api} from 'datatables.net';
|
-import DataTables, {Api, ColumnSelector} from 'datatables.net';
|
||||||
+import DataTables, { Api } from 'datatables.net';
|
+import DataTables, { Api, ColumnSelector } from 'datatables.net';
|
||||||
|
|
||||||
export default DataTables;
|
export default DataTables;
|
||||||
|
|
||||||
@@ -40,6 +40,8 @@ declare module 'datatables.net' {
|
@@ -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.
|
+ // Ignore this error: error TS7013: Construct signature, which lacks return-type annotation, implicitly has an 'any' return type.
|
||||||
+ // @ts-ignore
|
+ // @ts-ignore
|
||||||
new (dt: Api<any>, settings: boolean | ConfigColReorder);
|
new (dt: Api<any>, settings: boolean | ConfigColReorder): DataTablesStatic['ColReorder'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[defaults]
|
[defaults]
|
||||||
group = dataexplorer-preview
|
group = dataexplorer-preview
|
||||||
sku = P1V2
|
sku = P1v2
|
||||||
appserviceplan = dataexplorer-preview
|
appserviceplan = dataexplorer-preview
|
||||||
location = westus2
|
location = westus2
|
||||||
web = dataexplorer-preview
|
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": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"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",
|
"start": "node index.js",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Microsoft Corporation",
|
"author": "Microsoft Corporation",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.17.1",
|
"body-parser": "^1.20.3",
|
||||||
|
"express": "^4.21.2",
|
||||||
"http-proxy-middleware": "^3.0.3",
|
"http-proxy-middleware": "^3.0.3",
|
||||||
"node": "^18.20.6",
|
"node": "^20.19.5",
|
||||||
"node-fetch": "^2.6.1"
|
"node-fetch": "^2.6.1",
|
||||||
|
"path-to-regexp": "^0.1.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,13 +138,12 @@ export enum MongoBackendEndpointType {
|
|||||||
remote,
|
remote,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BackendApi {
|
export class AadScopeEndpoints {
|
||||||
public static readonly GenerateToken: string = "GenerateToken";
|
public static readonly Development: string = "https://cosmos.azure.com";
|
||||||
public static readonly PortalSettings: string = "PortalSettings";
|
public static readonly MPAC: string = "https://cosmos.azure.com";
|
||||||
public static readonly AccountRestrictions: string = "AccountRestrictions";
|
public static readonly Prod: string = "https://cosmos.azure.com";
|
||||||
public static readonly RuntimeProxy: string = "RuntimeProxy";
|
public static readonly Fairfax: string = "https://cosmos.azure.us";
|
||||||
public static readonly DisallowedLocations: string = "DisallowedLocations";
|
public static readonly Mooncake: string = "https://cosmos.azure.cn";
|
||||||
public static readonly SampleData: string = "SampleData";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PortalBackendEndpoints {
|
export class PortalBackendEndpoints {
|
||||||
@@ -264,6 +263,7 @@ export class HttpHeaders {
|
|||||||
public static activityId: string = "x-ms-activity-id";
|
public static activityId: string = "x-ms-activity-id";
|
||||||
public static apiType: string = "x-ms-cosmos-apitype";
|
public static apiType: string = "x-ms-cosmos-apitype";
|
||||||
public static authorization: string = "authorization";
|
public static authorization: string = "authorization";
|
||||||
|
public static entraIdToken: string = "x-ms-entraid-token";
|
||||||
public static collectionIndexTransformationProgress: string =
|
public static collectionIndexTransformationProgress: string =
|
||||||
"x-ms-documentdb-collection-index-transformation-progress";
|
"x-ms-documentdb-collection-index-transformation-progress";
|
||||||
public static continuation: string = "x-ms-continuation";
|
public static continuation: string = "x-ms-continuation";
|
||||||
@@ -774,3 +774,10 @@ export const ShortenedQueryCopilotSampleContainerSchema = {
|
|||||||
|
|
||||||
userPrompt: "find all products",
|
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 { AuthorizationToken } from "Contracts/FabricMessageTypes";
|
||||||
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
|
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { PriorityLevel } from "../Common/Constants";
|
import { PriorityLevel } from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { Platform, configContext } from "../ConfigContext";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
|
import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
|
||||||
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
|
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
|
||||||
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
|
||||||
@@ -20,8 +20,7 @@ const _global = typeof self === "undefined" ? window : self;
|
|||||||
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
|
||||||
const { verb, resourceId, resourceType, headers } = requestInfo;
|
const { verb, resourceId, resourceType, headers } = requestInfo;
|
||||||
|
|
||||||
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
|
if (useDataplaneRbacAuthorization(userContext)) {
|
||||||
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
|
|
||||||
Logger.logInfo(
|
Logger.logInfo(
|
||||||
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
|
||||||
"Explorer/tokenProvider",
|
"Explorer/tokenProvider",
|
||||||
|
|||||||
@@ -28,3 +28,39 @@ describe("Environment Utility Test", () => {
|
|||||||
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Development);
|
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";
|
import { configContext } from "ConfigContext";
|
||||||
|
|
||||||
export function normalizeArmEndpoint(uri: string): string {
|
export function normalizeArmEndpoint(uri: string): string {
|
||||||
@@ -27,3 +28,17 @@ export const getEnvironment = (): Environment => {
|
|||||||
|
|
||||||
return environmentMap[configContext.PORTAL_BACKEND_ENDPOINT];
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -84,7 +83,6 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
queryDocuments(databaseId, collection, true, "{}");
|
queryDocuments(databaseId, collection, true, "{}");
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
@@ -101,7 +99,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -120,7 +117,6 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
@@ -137,7 +133,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -156,7 +151,6 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
readDocument(databaseId, collection, documentId);
|
readDocument(databaseId, collection, documentId);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
@@ -173,7 +167,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -197,7 +190,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
window.fetch = jest.fn().mockImplementation(fetchMock);
|
window.fetch = jest.fn().mockImplementation(fetchMock);
|
||||||
});
|
});
|
||||||
@@ -216,7 +208,6 @@ describe("MongoProxyClient", () => {
|
|||||||
it("builds the correct proxy URL in development", () => {
|
it("builds the correct proxy URL in development", () => {
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
deleteDocuments(databaseId, collection, [documentId]);
|
deleteDocuments(databaseId, collection, [documentId]);
|
||||||
expect(window.fetch).toHaveBeenCalledWith(
|
expect(window.fetch).toHaveBeenCalledWith(
|
||||||
@@ -233,7 +224,6 @@ describe("MongoProxyClient", () => {
|
|||||||
});
|
});
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
|
||||||
|
import { getMongoGuidRepresentation } from "Shared/StorageUtility";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { configContext } from "../ConfigContext";
|
import { configContext } from "../ConfigContext";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
@@ -6,6 +7,7 @@ import { MessageTypes } from "../Contracts/ExplorerContracts";
|
|||||||
import { Collection } from "../Contracts/ViewModels";
|
import { Collection } from "../Contracts/ViewModels";
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
import { isDataplaneRbacEnabledForProxyApi } from "../Utils/AuthorizationUtils";
|
||||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||||
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||||
import { MinimalQueryIterator } from "./IteratorUtilities";
|
import { MinimalQueryIterator } from "./IteratorUtilities";
|
||||||
@@ -21,7 +23,13 @@ function authHeaders() {
|
|||||||
if (userContext.authType === AuthType.EncryptedToken) {
|
if (userContext.authType === AuthType.EncryptedToken) {
|
||||||
return { [HttpHeaders.guestAccessToken]: userContext.accessToken };
|
return { [HttpHeaders.guestAccessToken]: userContext.accessToken };
|
||||||
} else {
|
} 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 && documentId.partitionKey && !documentId.partitionKey.systemKey
|
||||||
? documentId.partitionKeyProperties?.[0]
|
? documentId.partitionKeyProperties?.[0]
|
||||||
: "",
|
: "",
|
||||||
|
clientSettings: {
|
||||||
|
guidRepresentation: getMongoGuidRepresentation(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
@@ -181,6 +192,9 @@ export function createDocument(
|
|||||||
partitionKey:
|
partitionKey:
|
||||||
collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
||||||
documentContent: JSON.stringify(documentContent),
|
documentContent: JSON.stringify(documentContent),
|
||||||
|
clientSettings: {
|
||||||
|
guidRepresentation: getMongoGuidRepresentation(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
@@ -228,6 +242,9 @@ export function updateDocument(
|
|||||||
? documentId.partitionKeyProperties?.[0]
|
? documentId.partitionKeyProperties?.[0]
|
||||||
: "",
|
: "",
|
||||||
documentContent,
|
documentContent,
|
||||||
|
clientSettings: {
|
||||||
|
guidRepresentation: getMongoGuidRepresentation(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
|
|
||||||
@@ -274,6 +291,9 @@ export function deleteDocuments(
|
|||||||
subscriptionID: userContext.subscriptionId,
|
subscriptionID: userContext.subscriptionId,
|
||||||
resourceGroup: userContext.resourceGroup,
|
resourceGroup: userContext.resourceGroup,
|
||||||
databaseAccountName: databaseAccount.name,
|
databaseAccountName: databaseAccount.name,
|
||||||
|
clientSettings: {
|
||||||
|
guidRepresentation: getMongoGuidRepresentation(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
|
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
|
||||||
{
|
{
|
||||||
"disableNonStreamingOrderByQuery": true,
|
|
||||||
"enableQueryControl": false,
|
"enableQueryControl": false,
|
||||||
"enableScanInQuery": true,
|
"enableScanInQuery": true,
|
||||||
"forceQueryPlan": true,
|
"forceQueryPlan": true,
|
||||||
@@ -14,7 +13,6 @@ exports[`getCommonQueryOptions builds the correct default options objects 1`] =
|
|||||||
|
|
||||||
exports[`getCommonQueryOptions reads from localStorage 1`] = `
|
exports[`getCommonQueryOptions reads from localStorage 1`] = `
|
||||||
{
|
{
|
||||||
"disableNonStreamingOrderByQuery": true,
|
|
||||||
"enableQueryControl": false,
|
"enableQueryControl": false,
|
||||||
"enableScanInQuery": true,
|
"enableScanInQuery": true,
|
||||||
"forceQueryPlan": true,
|
"forceQueryPlan": true,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
@@ -41,7 +42,7 @@ interface MetricsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
|
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
|
||||||
if (userContext.authType !== AuthType.AAD) {
|
if (userContext.authType !== AuthType.AAD || isFabricNative()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
|
||||||
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||||
import { Queries } from "../Constants";
|
import { Queries } from "../Constants";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
@@ -28,6 +27,5 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
|
|||||||
Queries.itemsPerPage;
|
Queries.itemsPerPage;
|
||||||
options.enableQueryControl = LocalStorageUtility.getEntryBoolean(StorageKey.QueryControlEnabled);
|
options.enableQueryControl = LocalStorageUtility.getEntryBoolean(StorageKey.QueryControlEnabled);
|
||||||
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
|
||||||
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
|
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -126,12 +126,5 @@ async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Co
|
|||||||
throw new Error(`Unsupported default experience type: ${apiType}`);
|
throw new Error(`Unsupported default experience type: ${apiType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TO DO: Remove when we get RP API Spec with materializedViews
|
return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection);
|
||||||
/* 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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
|
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
|
||||||
import {
|
import {
|
||||||
BackendApi,
|
|
||||||
CassandraProxyEndpoints,
|
|
||||||
JunoEndpoints,
|
|
||||||
MongoProxyEndpoints,
|
|
||||||
PortalBackendEndpoints,
|
|
||||||
} from "Common/Constants";
|
|
||||||
import {
|
|
||||||
allowedAadEndpoints,
|
|
||||||
allowedArcadiaEndpoints,
|
allowedArcadiaEndpoints,
|
||||||
allowedEmulatorEndpoints,
|
allowedEmulatorEndpoints,
|
||||||
allowedGraphEndpoints,
|
|
||||||
allowedHostedExplorerEndpoints,
|
allowedHostedExplorerEndpoints,
|
||||||
allowedJunoOrigins,
|
allowedJunoOrigins,
|
||||||
allowedMsalRedirectEndpoints,
|
allowedMsalRedirectEndpoints,
|
||||||
|
defaultAllowedAadEndpoints,
|
||||||
defaultAllowedArmEndpoints,
|
defaultAllowedArmEndpoints,
|
||||||
defaultAllowedBackendEndpoints,
|
defaultAllowedBackendEndpoints,
|
||||||
defaultAllowedCassandraProxyEndpoints,
|
defaultAllowedCassandraProxyEndpoints,
|
||||||
|
defaultAllowedGraphEndpoints,
|
||||||
defaultAllowedMongoProxyEndpoints,
|
defaultAllowedMongoProxyEndpoints,
|
||||||
validateEndpoint,
|
validateEndpoint,
|
||||||
} from "Utils/EndpointUtils";
|
} from "Utils/EndpointUtils";
|
||||||
@@ -29,6 +23,8 @@ export enum Platform {
|
|||||||
|
|
||||||
export interface ConfigContext {
|
export interface ConfigContext {
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
|
allowedAadEndpoints: ReadonlyArray<string>;
|
||||||
|
allowedGraphEndpoints: ReadonlyArray<string>;
|
||||||
allowedArmEndpoints: ReadonlyArray<string>;
|
allowedArmEndpoints: ReadonlyArray<string>;
|
||||||
allowedBackendEndpoints: ReadonlyArray<string>;
|
allowedBackendEndpoints: ReadonlyArray<string>;
|
||||||
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
|
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
|
||||||
@@ -37,10 +33,8 @@ export interface ConfigContext {
|
|||||||
gitSha?: string;
|
gitSha?: string;
|
||||||
proxyPath?: string;
|
proxyPath?: string;
|
||||||
AAD_ENDPOINT: string;
|
AAD_ENDPOINT: string;
|
||||||
ARM_AUTH_AREA: string;
|
|
||||||
ARM_ENDPOINT: string;
|
ARM_ENDPOINT: string;
|
||||||
EMULATOR_ENDPOINT?: string;
|
EMULATOR_ENDPOINT?: string;
|
||||||
ARM_API_VERSION: string;
|
|
||||||
GRAPH_ENDPOINT: string;
|
GRAPH_ENDPOINT: string;
|
||||||
GRAPH_API_VERSION: 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
|
// 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_ENDPOINT: string;
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||||
PORTAL_BACKEND_ENDPOINT: string;
|
PORTAL_BACKEND_ENDPOINT: string;
|
||||||
NEW_BACKEND_APIS?: BackendApi[];
|
|
||||||
MONGO_PROXY_ENDPOINT: string;
|
MONGO_PROXY_ENDPOINT: string;
|
||||||
CASSANDRA_PROXY_ENDPOINT: string;
|
CASSANDRA_PROXY_ENDPOINT: string;
|
||||||
NEW_CASSANDRA_APIS?: string[];
|
|
||||||
PROXY_PATH?: string;
|
PROXY_PATH?: string;
|
||||||
JUNO_ENDPOINT: string;
|
JUNO_ENDPOINT: string;
|
||||||
GITHUB_CLIENT_ID: string;
|
GITHUB_CLIENT_ID: string;
|
||||||
GITHUB_TEST_ENV_CLIENT_ID: string;
|
GITHUB_TEST_ENV_CLIENT_ID: string;
|
||||||
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
||||||
isTerminalEnabled: boolean;
|
|
||||||
isPhoenixEnabled: boolean;
|
isPhoenixEnabled: boolean;
|
||||||
hostedExplorerURL: string;
|
hostedExplorerURL: string;
|
||||||
armAPIVersion?: string;
|
armAPIVersion?: string;
|
||||||
msalRedirectURI?: string;
|
msalRedirectURI?: string;
|
||||||
globallyEnabledCassandraAPIs?: string[];
|
|
||||||
globallyEnabledMongoAPIs?: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration
|
||||||
let configContext: Readonly<ConfigContext> = {
|
let configContext: Readonly<ConfigContext> = {
|
||||||
platform: Platform.Portal,
|
platform: Platform.Portal,
|
||||||
|
allowedAadEndpoints: defaultAllowedAadEndpoints,
|
||||||
|
allowedGraphEndpoints: defaultAllowedGraphEndpoints,
|
||||||
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
allowedArmEndpoints: defaultAllowedArmEndpoints,
|
||||||
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
|
||||||
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
|
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
|
||||||
@@ -85,17 +76,12 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
|
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
|
||||||
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
|
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
|
||||||
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
`^https:\\/\\/.*\\.powerbi\\.com$`,
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.net$`,
|
|
||||||
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
|
|
||||||
`^https:\\/\\/.*\\.azure-test\\.net$`,
|
|
||||||
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
|
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
|
||||||
], // Webpack injects this at build time
|
], // Webpack injects this at build time
|
||||||
gitSha: process.env.GIT_SHA,
|
gitSha: process.env.GIT_SHA,
|
||||||
hostedExplorerURL: "https://cosmos.azure.com/",
|
hostedExplorerURL: "https://cosmos.azure.com/",
|
||||||
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
AAD_ENDPOINT: "https://login.microsoftonline.com/",
|
||||||
ARM_AUTH_AREA: "https://management.azure.com/",
|
|
||||||
ARM_ENDPOINT: "https://management.azure.com/",
|
ARM_ENDPOINT: "https://management.azure.com/",
|
||||||
ARM_API_VERSION: "2016-06-01",
|
|
||||||
GRAPH_ENDPOINT: "https://graph.microsoft.com",
|
GRAPH_ENDPOINT: "https://graph.microsoft.com",
|
||||||
GRAPH_API_VERSION: "1.6",
|
GRAPH_API_VERSION: "1.6",
|
||||||
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
|
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
|
||||||
@@ -109,11 +95,7 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||||
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
|
|
||||||
isTerminalEnabled: false,
|
|
||||||
isPhoenixEnabled: false,
|
isPhoenixEnabled: false,
|
||||||
globallyEnabledCassandraAPIs: [],
|
|
||||||
globallyEnabledMongoAPIs: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resetConfigContext(): void {
|
export function resetConfigContext(): void {
|
||||||
@@ -128,19 +110,21 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
|
if (!validateEndpoint(newContext.AAD_ENDPOINT, configContext.allowedAadEndpoints || defaultAllowedAadEndpoints)) {
|
||||||
delete newContext.ARM_ENDPOINT;
|
delete newContext.AAD_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.AAD_ENDPOINT, allowedAadEndpoints)) {
|
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
|
||||||
delete newContext.AAD_ENDPOINT;
|
delete newContext.ARM_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
|
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
|
||||||
delete newContext.EMULATOR_ENDPOINT;
|
delete newContext.EMULATOR_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) {
|
if (
|
||||||
|
!validateEndpoint(newContext.GRAPH_ENDPOINT, configContext.allowedGraphEndpoints || defaultAllowedGraphEndpoints)
|
||||||
|
) {
|
||||||
delete newContext.GRAPH_ENDPOINT;
|
delete newContext.GRAPH_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +132,15 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
|
|||||||
delete newContext.ARCADIA_ENDPOINT;
|
delete newContext.ARCADIA_ENDPOINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!validateEndpoint(
|
||||||
|
newContext.PORTAL_BACKEND_ENDPOINT,
|
||||||
|
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
delete newContext.PORTAL_BACKEND_ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!validateEndpoint(
|
!validateEndpoint(
|
||||||
newContext.MONGO_PROXY_ENDPOINT,
|
newContext.MONGO_PROXY_ENDPOINT,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export enum PaneKind {
|
|||||||
GlobalSettings,
|
GlobalSettings,
|
||||||
AdHocAccess,
|
AdHocAccess,
|
||||||
SwitchDirectory,
|
SwitchDirectory,
|
||||||
|
QuickStart,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface ArmEntity {
|
|||||||
type: string;
|
type: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
tags?: Tags;
|
tags?: Tags;
|
||||||
|
resourceGroup?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatabaseAccount extends ArmEntity {
|
export interface DatabaseAccount extends ArmEntity {
|
||||||
@@ -388,7 +389,7 @@ export interface VectorEmbeddingPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface VectorEmbedding {
|
export interface VectorEmbedding {
|
||||||
dataType: "float16" | "float32" | "uint8" | "int8";
|
dataType: "float32" | "uint8" | "int8";
|
||||||
dimensions: number;
|
dimensions: number;
|
||||||
distanceFunction: "euclidean" | "cosine" | "dotproduct";
|
distanceFunction: "euclidean" | "cosine" | "dotproduct";
|
||||||
path: string;
|
path: string;
|
||||||
|
|||||||
@@ -443,6 +443,7 @@ export interface DataExplorerInputsFrame {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
feedbackPolicies?: any;
|
feedbackPolicies?: any;
|
||||||
|
aadToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelfServeFrameInputs {
|
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,
|
TextField,
|
||||||
} from "@fluentui/react";
|
} from "@fluentui/react";
|
||||||
import React, { FC, useEffect } from "react";
|
import React, { FC, useEffect } from "react";
|
||||||
import create, { UseStore } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
export interface DialogState {
|
export interface DialogState {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -38,7 +38,7 @@ export interface DialogState {
|
|||||||
showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps) => void;
|
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,
|
visible: false,
|
||||||
openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })),
|
openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })),
|
||||||
closeDialog: () =>
|
closeDialog: () =>
|
||||||
|
|||||||
@@ -559,26 +559,81 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
private getThroughputTextField = (): JSX.Element => (
|
private getThroughputTextField = (): JSX.Element => (
|
||||||
<>
|
<>
|
||||||
{this.props.isAutoPilotSelected ? (
|
{this.props.isAutoPilotSelected ? (
|
||||||
<TextField
|
<Stack horizontal verticalAlign="end" tokens={{ childrenGap: 8 }}>
|
||||||
label="Maximum RU/s required by this resource"
|
{/* Column 1: Minimum RU/s */}
|
||||||
required
|
<Stack tokens={{ childrenGap: 4 }}>
|
||||||
type="number"
|
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
|
||||||
id="autopilotInput"
|
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
|
||||||
key="auto pilot throughput input"
|
Minimum RU/s
|
||||||
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
|
</Text>
|
||||||
disabled={this.overrideWithProvisionedThroughputSettings()}
|
<FontIcon iconName="Info" style={{ fontSize: 12, color: "#666" }} />
|
||||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
</Stack>
|
||||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
<Text
|
||||||
onChange={this.onAutoPilotThroughputChange}
|
style={{
|
||||||
min={autoPilotThroughput1K}
|
fontFamily: "Segoe UI",
|
||||||
onGetErrorMessage={(value: string) => {
|
width: 70,
|
||||||
const sanitizedValue = getSanitizedInputValue(value);
|
height: 28,
|
||||||
return sanitizedValue % 1000
|
border: "none",
|
||||||
? "Throughput value must be in increments of 1000"
|
fontSize: 14,
|
||||||
: this.props.throughputError;
|
backgroundColor: "transparent",
|
||||||
}}
|
fontWeight: 400,
|
||||||
validateOnLoad={false}
|
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
|
<TextField
|
||||||
required
|
required
|
||||||
|
|||||||
@@ -157,35 +157,148 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StyledTextFieldBase
|
<Stack
|
||||||
disabled={true}
|
horizontal={true}
|
||||||
id="autopilotInput"
|
tokens={
|
||||||
key="auto pilot throughput input"
|
|
||||||
label="Maximum RU/s required by this resource"
|
|
||||||
min={1000}
|
|
||||||
onChange={[Function]}
|
|
||||||
onGetErrorMessage={[Function]}
|
|
||||||
required={true}
|
|
||||||
step={1000}
|
|
||||||
styles={
|
|
||||||
{
|
{
|
||||||
"fieldGroup": {
|
"childrenGap": 8,
|
||||||
"borderColor": "",
|
|
||||||
"height": 25,
|
|
||||||
"selectors": {
|
|
||||||
":disabled": {
|
|
||||||
"backgroundColor": undefined,
|
|
||||||
"borderColor": undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"width": 300,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
type="number"
|
verticalAlign="end"
|
||||||
validateOnLoad={false}
|
>
|
||||||
value=""
|
<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>
|
<Stack>
|
||||||
<Stack
|
<Stack
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import { useDatabases } from "Explorer/useDatabases";
|
|||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { FunctionComponent, useEffect, useState } from "react";
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
||||||
|
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
|
||||||
import * as SharedConstants from "../../../Shared/Constants";
|
import * as SharedConstants from "../../../Shared/Constants";
|
||||||
import { userContext } from "../../../UserContext";
|
import { userContext } from "../../../UserContext";
|
||||||
import { getCollectionName } from "../../../Utils/APITypeUtils";
|
import { getCollectionName } from "../../../Utils/APITypeUtils";
|
||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
import * as PricingUtils from "../../../Utils/PricingUtils";
|
import * as PricingUtils from "../../../Utils/PricingUtils";
|
||||||
import "./ThroughputInput.less";
|
import "./ThroughputInput.less";
|
||||||
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
|
|
||||||
|
|
||||||
export interface ThroughputInputProps {
|
export interface ThroughputInputProps {
|
||||||
isDatabase: boolean;
|
isDatabase: boolean;
|
||||||
@@ -41,11 +41,12 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
|||||||
let defaultThroughput: number;
|
let defaultThroughput: number;
|
||||||
const workloadType: Constants.WorkloadType = getWorkloadType();
|
const workloadType: Constants.WorkloadType = getWorkloadType();
|
||||||
|
|
||||||
if (
|
if (isFabricNative()) {
|
||||||
|
defaultThroughput = AutoPilotUtils.autoPilotThroughput5K;
|
||||||
|
} else if (
|
||||||
isFreeTier ||
|
isFreeTier ||
|
||||||
isQuickstart ||
|
isQuickstart ||
|
||||||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType) ||
|
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
|
||||||
isFabricNative()
|
|
||||||
) {
|
) {
|
||||||
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
||||||
} else if (workloadType === Constants.WorkloadType.Production) {
|
} else if (workloadType === Constants.WorkloadType.Production) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
||||||
import { createCollection } from "../../Common/dataAccess/createCollection";
|
import { createCollection } from "../../Common/dataAccess/createCollection";
|
||||||
import { createDocument } from "../../Common/dataAccess/createDocument";
|
import { createDocument } from "../../Common/dataAccess/createDocument";
|
||||||
import { createDocument as createMongoDocument } from "../../Common/MongoProxyClient";
|
import { createDocument as createMongoDocument } from "../../Common/MongoProxyClient";
|
||||||
@@ -90,12 +91,13 @@ export class ContainerSampleGenerator {
|
|||||||
}
|
}
|
||||||
const { databaseAccount: account } = userContext;
|
const { databaseAccount: account } = userContext;
|
||||||
const databaseId = collection.databaseId;
|
const databaseId = collection.databaseId;
|
||||||
|
|
||||||
const gremlinClient = new GremlinClient();
|
const gremlinClient = new GremlinClient();
|
||||||
gremlinClient.initialize({
|
gremlinClient.initialize({
|
||||||
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
|
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
|
||||||
databaseId: databaseId,
|
databaseId: databaseId,
|
||||||
collectionId: collection.id(),
|
collectionId: collection.id(),
|
||||||
masterKey: userContext.masterKey || "",
|
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
|
||||||
maxResultSize: 100,
|
maxResultSize: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ import { MessageTypes } from "Contracts/ExplorerContracts";
|
|||||||
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||||
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { IGalleryItem } from "Juno/JunoClient";
|
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 { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
|
||||||
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
|
||||||
@@ -18,7 +23,7 @@ import { useQueryCopilot } from "hooks/useQueryCopilot";
|
|||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
import shallow from "zustand/shallow";
|
import { shallow } from "zustand/shallow";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
@@ -107,8 +112,8 @@ export default class Explorer {
|
|||||||
|
|
||||||
this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
|
this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
|
||||||
useNotebook.subscribe(
|
useNotebook.subscribe(
|
||||||
() => this.refreshCommandBarButtons(),
|
|
||||||
(state) => state.isNotebooksEnabledForAccount,
|
(state) => state.isNotebooksEnabledForAccount,
|
||||||
|
() => this.refreshCommandBarButtons(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.queriesClient = new QueriesClient(this);
|
this.queriesClient = new QueriesClient(this);
|
||||||
@@ -131,13 +136,13 @@ export default class Explorer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useTabs.subscribe(
|
useTabs.subscribe(
|
||||||
|
(state) => state.openedTabs,
|
||||||
(openedTabs: TabsBase[]) => {
|
(openedTabs: TabsBase[]) => {
|
||||||
if (openedTabs.length === 0) {
|
if (openedTabs.length === 0) {
|
||||||
useSelectedNode.getState().setSelectedNode(undefined);
|
useSelectedNode.getState().setSelectedNode(undefined);
|
||||||
useCommandBar.getState().setContextButtons([]);
|
useCommandBar.getState().setContextButtons([]);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
(state) => state.openedTabs,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.isTabsContentExpanded = ko.observable(false);
|
this.isTabsContentExpanded = ko.observable(false);
|
||||||
@@ -165,9 +170,9 @@ export default class Explorer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useNotebook.subscribe(
|
useNotebook.subscribe(
|
||||||
async () => this.initiateAndRefreshNotebookList(),
|
|
||||||
(state) => [state.isNotebookEnabled, state.isRefreshed],
|
(state) => [state.isNotebookEnabled, state.isRefreshed],
|
||||||
shallow,
|
async () => this.initiateAndRefreshNotebookList(),
|
||||||
|
{ equalityFn: shallow },
|
||||||
);
|
);
|
||||||
|
|
||||||
this.resourceTree = new ResourceTreeAdapter(this);
|
this.resourceTree = new ResourceTreeAdapter(this);
|
||||||
@@ -284,14 +289,40 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public openInVsCode(): void {
|
/**
|
||||||
|
* 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 getCosmosDbUrl() {
|
||||||
const activeTab = useTabs.getState().activeTab;
|
const activeTab = useTabs.getState().activeTab;
|
||||||
const resourceId = encodeURIComponent(userContext.databaseAccount.id);
|
const resourceId = encodeURIComponent(userContext.databaseAccount.id);
|
||||||
const database = encodeURIComponent(activeTab?.collection?.databaseId);
|
const database = encodeURIComponent(activeTab?.collection?.databaseId);
|
||||||
const container = encodeURIComponent(activeTab?.collection?.id());
|
const container = encodeURIComponent(activeTab?.collection?.id());
|
||||||
const baseUrl = `vscode://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 vscodeUrl = activeTab ? `${baseUrl}&database=${database}&container=${container}` : baseUrl;
|
||||||
|
return vscodeUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = {
|
const openVSCodeDialogProps: DialogProps = {
|
||||||
linkProps: {
|
linkProps: {
|
||||||
linkText: "Download Visual Studio Code",
|
linkText: "Download Visual Studio Code",
|
||||||
@@ -1149,7 +1180,10 @@ export default class Explorer {
|
|||||||
? this.refreshDatabaseForResourceToken()
|
? this.refreshDatabaseForResourceToken()
|
||||||
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow
|
: 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
|
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||||
const isNotebookEnabled =
|
const isNotebookEnabled =
|
||||||
@@ -1171,7 +1205,7 @@ export default class Explorer {
|
|||||||
await this.initNotebooks(userContext.databaseAccount);
|
await this.initNotebooks(userContext.databaseAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userContext.authType === AuthType.AAD && userContext.apiType === "SQL") {
|
if (userContext.authType === AuthType.AAD && userContext.apiType === "SQL" && !isFabricNative()) {
|
||||||
const throughputBucketsEnabled = await featureRegistered(userContext.subscriptionId, "ThroughputBucketing");
|
const throughputBucketsEnabled = await featureRegistered(userContext.subscriptionId, "ThroughputBucketing");
|
||||||
updateUserContext({ throughputBucketsEnabled });
|
updateUserContext({ throughputBucketsEnabled });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,8 +163,7 @@ describe("GraphExplorer", () => {
|
|||||||
graphBackendEndpoint: "graphBackendEndpoint",
|
graphBackendEndpoint: "graphBackendEndpoint",
|
||||||
databaseId: "databaseId",
|
databaseId: "databaseId",
|
||||||
collectionId: "collectionId",
|
collectionId: "collectionId",
|
||||||
masterKey: "masterKey",
|
password: "password",
|
||||||
|
|
||||||
onLoadStartKey: 0,
|
onLoadStartKey: 0,
|
||||||
onLoadStartKeyChange: (newKey: number): void => {},
|
onLoadStartKeyChange: (newKey: number): void => {},
|
||||||
resourceId: "resourceId",
|
resourceId: "resourceId",
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export interface GraphExplorerProps {
|
|||||||
graphBackendEndpoint: string;
|
graphBackendEndpoint: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
collectionId: string;
|
collectionId: string;
|
||||||
masterKey: string;
|
password: string;
|
||||||
|
|
||||||
onLoadStartKey: number;
|
onLoadStartKey: number;
|
||||||
onLoadStartKeyChange: (newKey: number) => void;
|
onLoadStartKeyChange: (newKey: number) => void;
|
||||||
@@ -1300,7 +1300,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
|||||||
endpoint: `wss://${this.props.graphBackendEndpoint}`,
|
endpoint: `wss://${this.props.graphBackendEndpoint}`,
|
||||||
databaseId: this.props.databaseId,
|
databaseId: this.props.databaseId,
|
||||||
collectionId: this.props.collectionId,
|
collectionId: this.props.collectionId,
|
||||||
masterKey: this.props.masterKey,
|
password: this.props.password,
|
||||||
maxResultSize: GraphExplorer.MAX_RESULT_SIZE,
|
maxResultSize: GraphExplorer.MAX_RESULT_SIZE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,28 +8,28 @@ describe("Gremlin Client", () => {
|
|||||||
endpoint: null,
|
endpoint: null,
|
||||||
collectionId: null,
|
collectionId: null,
|
||||||
databaseId: null,
|
databaseId: null,
|
||||||
masterKey: null,
|
|
||||||
maxResultSize: 10000,
|
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 collectionId = "collectionId";
|
||||||
const databaseId = "databaseId";
|
const databaseId = "databaseId";
|
||||||
const masterKey = "masterKey";
|
const testPassword = "password";
|
||||||
const gremlinClient = new GremlinClient();
|
const gremlinClient = new GremlinClient();
|
||||||
|
|
||||||
gremlinClient.initialize({
|
gremlinClient.initialize({
|
||||||
endpoint: null,
|
endpoint: null,
|
||||||
collectionId,
|
collectionId,
|
||||||
databaseId,
|
databaseId,
|
||||||
masterKey,
|
|
||||||
maxResultSize: 0,
|
maxResultSize: 0,
|
||||||
|
password: testPassword,
|
||||||
});
|
});
|
||||||
|
|
||||||
// User must includes these values
|
// User must includes these values
|
||||||
expect(gremlinClient.client.params.user.indexOf(collectionId)).not.toBe(-1);
|
expect(gremlinClient.client.params.user.indexOf(collectionId)).not.toBe(-1);
|
||||||
expect(gremlinClient.client.params.user.indexOf(databaseId)).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) => {
|
it("should aggregate RU charges across multiple responses", (done) => {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ export interface GremlinClientParameters {
|
|||||||
endpoint: string;
|
endpoint: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
collectionId: string;
|
collectionId: string;
|
||||||
masterKey: string;
|
|
||||||
maxResultSize: number;
|
maxResultSize: number;
|
||||||
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GremlinRequestResult {
|
export interface GremlinRequestResult {
|
||||||
@@ -43,7 +43,7 @@ export class GremlinClient {
|
|||||||
this.client = new GremlinSimpleClient({
|
this.client = new GremlinSimpleClient({
|
||||||
endpoint: params.endpoint,
|
endpoint: params.endpoint,
|
||||||
user: `/dbs/${params.databaseId}/colls/${params.collectionId}`,
|
user: `/dbs/${params.databaseId}/colls/${params.collectionId}`,
|
||||||
password: params.masterKey,
|
password: params.password,
|
||||||
successCallback: (result: Result) => {
|
successCallback: (result: Result) => {
|
||||||
this.storePendingResult(result);
|
this.storePendingResult(result);
|
||||||
this.flushResult(result.requestId);
|
this.flushResult(result.requestId);
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
|
|
||||||
import * as sinon from "sinon";
|
import * as sinon from "sinon";
|
||||||
import {
|
import {
|
||||||
|
GremlinRequestMessage,
|
||||||
|
GremlinResponseMessage,
|
||||||
GremlinSimpleClient,
|
GremlinSimpleClient,
|
||||||
GremlinSimpleClientParameters,
|
GremlinSimpleClientParameters,
|
||||||
Result,
|
Result,
|
||||||
GremlinRequestMessage,
|
|
||||||
GremlinResponseMessage,
|
|
||||||
} from "./GremlinSimpleClient";
|
} from "./GremlinSimpleClient";
|
||||||
|
|
||||||
describe("Gremlin Simple Client", () => {
|
describe("Gremlin Simple Client", () => {
|
||||||
|
|||||||
@@ -95,3 +95,10 @@
|
|||||||
white-space: nowrap;
|
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 { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
|
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
|
||||||
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
|
||||||
import { isFabric } from "Platform/Fabric/FabricUtil";
|
import { isFabric } from "Platform/Fabric/FabricUtil";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import create, { UseStore } from "zustand";
|
import { create } from "zustand";
|
||||||
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
|
||||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
import { StyleConstants } from "../../../Common/StyleConstants";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
@@ -29,8 +30,8 @@ export interface CommandBarStore {
|
|||||||
setIsHidden: (isHidden: boolean) => void;
|
setIsHidden: (isHidden: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
|
export const useCommandBar = create<CommandBarStore>((set) => ({
|
||||||
contextButtons: [],
|
contextButtons: [] as CommandButtonComponentProps[],
|
||||||
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
|
||||||
isHidden: false,
|
isHidden: false,
|
||||||
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
|
||||||
@@ -43,6 +44,15 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
|||||||
const backgroundColor = StyleConstants.BaseLight;
|
const backgroundColor = StyleConstants.BaseLight;
|
||||||
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
|
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") {
|
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
|
||||||
const buttons =
|
const buttons =
|
||||||
userContext.apiType === "Postgres"
|
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(
|
const contextButtons = (buttons || []).concat(
|
||||||
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState),
|
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { KeyboardAction } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
|
||||||
import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
|
import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
|
||||||
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
|
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
|
||||||
@@ -68,15 +67,7 @@ export function createStaticCommandBarButtons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isDataplaneRbacSupported(userContext.apiType)) {
|
if (isDataplaneRbacSupported(userContext.apiType)) {
|
||||||
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
|
const loginButtonProps = createLoginForEntraIDButton(container);
|
||||||
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
|
|
||||||
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const buttonProps = createLoginForEntraIDButton(container);
|
|
||||||
setLoginButtonProps(buttonProps);
|
|
||||||
}, [dataPlaneRbacEnabled, aadTokenUpdated, container]);
|
|
||||||
|
|
||||||
if (loginButtonProps) {
|
if (loginButtonProps) {
|
||||||
addDivider();
|
addDivider();
|
||||||
buttons.push(loginButtonProps);
|
buttons.push(loginButtonProps);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AppState, ContentRef, selectors } from "@nteract/core";
|
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 React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
@@ -59,7 +59,7 @@ export class StatusBar extends React.Component<Props> {
|
|||||||
<Bar data-test="notebookStatusBar">
|
<Bar data-test="notebookStatusBar">
|
||||||
<RightStatus>
|
<RightStatus>
|
||||||
{this.props.lastSaved ? (
|
{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>
|
<p> Not saved yet </p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
|
||||||
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
import { PhoenixClient } from "Phoenix/PhoenixClient";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import create, { UseStore } from "zustand";
|
import { create } from "zustand";
|
||||||
|
import { subscribeWithSelector } from 'zustand/middleware';
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { ConnectionStatusType, HttpStatusCodes } from "../../Common/Constants";
|
import { ConnectionStatusType, HttpStatusCodes } from "../../Common/Constants";
|
||||||
@@ -66,270 +67,274 @@ interface NotebookState {
|
|||||||
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
|
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
export const useNotebook = create<NotebookState>()(
|
||||||
isNotebookEnabled: false,
|
subscribeWithSelector(
|
||||||
isNotebooksEnabledForAccount: false,
|
(set, get) => ({
|
||||||
notebookServerInfo: {
|
isNotebookEnabled: false,
|
||||||
notebookServerEndpoint: undefined,
|
isNotebooksEnabledForAccount: false,
|
||||||
authToken: undefined,
|
notebookServerInfo: {
|
||||||
forwardingId: undefined,
|
notebookServerEndpoint: "",
|
||||||
},
|
authToken: "",
|
||||||
sparkClusterConnectionInfo: {
|
forwardingId: "",
|
||||||
userName: undefined,
|
},
|
||||||
password: undefined,
|
sparkClusterConnectionInfo: {
|
||||||
endpoints: [],
|
userName: "",
|
||||||
},
|
password: "",
|
||||||
isSynapseLinkUpdating: false,
|
endpoints: [] as DataModels.SparkClusterEndpoint[],
|
||||||
memoryUsageInfo: undefined,
|
},
|
||||||
isShellEnabled: false,
|
isSynapseLinkUpdating: false,
|
||||||
notebookBasePath: Constants.Notebook.defaultBasePath,
|
memoryUsageInfo: undefined as DataModels.MemoryUsageInfo,
|
||||||
isInitializingNotebooks: false,
|
isShellEnabled: false,
|
||||||
myNotebooksContentRoot: undefined,
|
notebookBasePath: Constants.Notebook.defaultBasePath,
|
||||||
gitHubNotebooksContentRoot: undefined,
|
isInitializingNotebooks: false,
|
||||||
galleryContentRoot: undefined,
|
myNotebooksContentRoot: undefined as NotebookContentItem,
|
||||||
connectionInfo: {
|
gitHubNotebooksContentRoot: undefined as NotebookContentItem,
|
||||||
status: ConnectionStatusType.Connect,
|
galleryContentRoot: undefined as NotebookContentItem,
|
||||||
},
|
connectionInfo: {
|
||||||
notebookFolderName: undefined,
|
status: ConnectionStatusType.Connect,
|
||||||
isAllocating: false,
|
},
|
||||||
isRefreshed: false,
|
notebookFolderName: "",
|
||||||
containerStatus: {
|
isAllocating: false,
|
||||||
status: undefined,
|
isRefreshed: false,
|
||||||
durationLeftInMinutes: undefined,
|
containerStatus: {
|
||||||
phoenixServerInfo: undefined,
|
status: undefined,
|
||||||
},
|
durationLeftInMinutes: undefined,
|
||||||
isPhoenixNotebooks: undefined,
|
phoenixServerInfo: undefined,
|
||||||
isPhoenixFeatures: undefined,
|
} as ContainerInfo,
|
||||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
isPhoenixNotebooks: undefined as boolean,
|
||||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
isPhoenixFeatures: undefined as boolean,
|
||||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
||||||
set({ notebookServerInfo }),
|
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
||||||
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) =>
|
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
||||||
set({ sparkClusterConnectionInfo }),
|
set({ notebookServerInfo }),
|
||||||
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
|
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) =>
|
||||||
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
|
set({ sparkClusterConnectionInfo }),
|
||||||
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
|
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
|
||||||
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
|
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
|
||||||
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
|
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
|
||||||
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
|
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
|
||||||
await get().getPhoenixStatus();
|
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
|
||||||
const { databaseAccount, authType } = userContext;
|
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
|
||||||
if (
|
await get().getPhoenixStatus();
|
||||||
authType === AuthType.EncryptedToken ||
|
const { databaseAccount, authType } = userContext;
|
||||||
authType === AuthType.ResourceToken ||
|
if (
|
||||||
authType === AuthType.MasterKey
|
authType === AuthType.EncryptedToken ||
|
||||||
) {
|
authType === AuthType.ResourceToken ||
|
||||||
set({ isNotebooksEnabledForAccount: false });
|
authType === AuthType.MasterKey
|
||||||
return;
|
) {
|
||||||
}
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
const firstWriteLocation =
|
||||||
},
|
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
|
||||||
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => {
|
? databaseAccount?.location
|
||||||
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
|
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
||||||
const parentItem = get().findItem(root, parent);
|
const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`;
|
||||||
item.parent = parentItem;
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
if (parentItem.children) {
|
try {
|
||||||
parentItem.children.push(item);
|
const response = await fetch(disallowedLocationsUri, {
|
||||||
} else {
|
method: "POST",
|
||||||
parentItem.children = [item];
|
body: JSON.stringify({
|
||||||
}
|
resourceTypes: [Constants.ArmResourceTypes.notebookWorkspaces],
|
||||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
}),
|
||||||
},
|
headers: {
|
||||||
updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
|
[Constants.HttpHeaders.contentType]: "application/json",
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 });
|
if (updatedRoot?.children) {
|
||||||
}
|
// Count 1st generation children (tree is lazy-loaded)
|
||||||
},
|
const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
|
||||||
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
|
updatedRoot.children.forEach((notebookItem) => {
|
||||||
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
|
switch (notebookItem.type) {
|
||||||
resetContainerConnection: (connectionStatus: ContainerConnectionInfo): void => {
|
case NotebookContentItemType.File:
|
||||||
useTabs.getState().closeAllNotebookTabs(true);
|
nodeCounts.files++;
|
||||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
break;
|
||||||
useNotebook.getState().setNotebookServerInfo(undefined);
|
case NotebookContentItemType.Directory:
|
||||||
useNotebook.getState().setIsAllocating(false);
|
nodeCounts.directories++;
|
||||||
useNotebook.getState().setContainerStatus({
|
break;
|
||||||
status: undefined,
|
case NotebookContentItemType.Notebook:
|
||||||
durationLeftInMinutes: undefined,
|
nodeCounts.notebooks++;
|
||||||
phoenixServerInfo: undefined,
|
break;
|
||||||
});
|
default:
|
||||||
},
|
break;
|
||||||
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
|
}
|
||||||
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
|
});
|
||||||
getPhoenixStatus: async () => {
|
TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts });
|
||||||
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;
|
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]): void => {
|
||||||
}
|
const gitHubNotebooksContentRoot = cloneDeep(get().gitHubNotebooksContentRoot);
|
||||||
set({ isPhoenixNotebooks: isPhoenixNotebooks });
|
if (gitHubNotebooksContentRoot) {
|
||||||
set({ isPhoenixFeatures: isPhoenixFeatures });
|
gitHubNotebooksContentRoot.children = [];
|
||||||
}
|
pinnedRepos?.forEach((pinnedRepo) => {
|
||||||
},
|
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
|
||||||
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),
|
const repoTreeItem: NotebookContentItem = {
|
||||||
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }),
|
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]
|
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection]
|
||||||
) {
|
) {
|
||||||
explorer.onNewCollectionClicked();
|
explorer.onNewCollectionClicked();
|
||||||
|
} else if (
|
||||||
|
action.paneKind === ActionContracts.PaneKind.QuickStart ||
|
||||||
|
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.QuickStart]
|
||||||
|
) {
|
||||||
|
explorer.onNewCollectionClicked({ isQuickstart: true });
|
||||||
} else if (
|
} else if (
|
||||||
action.paneKind === ActionContracts.PaneKind.CassandraAddCollection ||
|
action.paneKind === ActionContracts.PaneKind.CassandraAddCollection ||
|
||||||
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection]
|
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection]
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import { getCollectionName } from "Utils/APITypeUtils";
|
|||||||
import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
|
||||||
import { getUpsellMessage } from "Utils/PricingUtils";
|
import { getUpsellMessage } from "Utils/PricingUtils";
|
||||||
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
|
||||||
|
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||||
import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
|
||||||
import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator";
|
import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator";
|
||||||
@@ -60,7 +61,6 @@ import { useDatabases } from "../../useDatabases";
|
|||||||
import { PanelFooterComponent } from "../PanelFooterComponent";
|
import { PanelFooterComponent } from "../PanelFooterComponent";
|
||||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
||||||
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
import { PanelLoadingScreen } from "../PanelLoadingScreen";
|
||||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
|
||||||
|
|
||||||
export interface AddCollectionPanelProps {
|
export interface AddCollectionPanelProps {
|
||||||
explorer: Explorer;
|
explorer: Explorer;
|
||||||
@@ -123,7 +123,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
isSharded: userContext.apiType !== "Tables",
|
isSharded: userContext.apiType !== "Tables",
|
||||||
partitionKey: getPartitionKey(props.isQuickstart),
|
partitionKey: getPartitionKey(props.isQuickstart),
|
||||||
subPartitionKeys: [],
|
subPartitionKeys: [],
|
||||||
enableDedicatedThroughput: false,
|
enableDedicatedThroughput: isFabricNative(), // Dedicated throughput is only enabled in Fabric Native by default
|
||||||
createMongoWildCardIndex:
|
createMongoWildCardIndex:
|
||||||
isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport"),
|
isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport"),
|
||||||
useHashV1: false,
|
useHashV1: false,
|
||||||
@@ -336,7 +336,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
size={40}
|
size={40}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
aria-label="New database id, Type a new database id"
|
aria-label="New database id, Type a new database id"
|
||||||
autoFocus
|
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
value={this.state.newDatabaseId}
|
value={this.state.newDatabaseId}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
@@ -407,9 +406,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
responsiveMode={999}
|
responsiveMode={999}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<Separator className="panelSeparator" style={{ marginTop: -4, marginBottom: -4 }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
<Separator className="panelSeparator" style={{ marginTop: -4, marginBottom: -4 }} />
|
|
||||||
|
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack horizontal style={{ marginTop: -5, marginBottom: 1 }}>
|
<Stack horizontal style={{ marginTop: -5, marginBottom: 1 }}>
|
||||||
@@ -449,8 +448,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
this.setState({ collectionId: event.target.value })
|
this.setState({ collectionId: event.target.value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Separator className="panelSeparator" style={{ marginTop: -5, marginBottom: -5 }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Separator className="panelSeparator" style={{ marginTop: -5, marginBottom: -5 }} />
|
|
||||||
{this.shouldShowIndexingOptionsForFreeTierAccount() && (
|
{this.shouldShowIndexingOptionsForFreeTierAccount() && (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
|
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
|
||||||
@@ -645,7 +645,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!isFabricNative() && userContext.apiType === "SQL" && (
|
{userContext.apiType === "SQL" && (
|
||||||
<Stack className="panelGroupSpacing">
|
<Stack className="panelGroupSpacing">
|
||||||
<DefaultButton
|
<DefaultButton
|
||||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||||
@@ -709,7 +709,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.shouldShowCollectionThroughputInput() && (
|
{this.shouldShowCollectionThroughputInput() && !isFabricNative() && (
|
||||||
<ThroughputInput
|
<ThroughputInput
|
||||||
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
|
||||||
isDatabase={false}
|
isDatabase={false}
|
||||||
@@ -742,7 +742,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
: "Comma separated paths e.g. /firstName,/address/zipCode"
|
: "Comma separated paths e.g. /firstName,/address/zipCode"
|
||||||
}
|
}
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
autoFocus
|
|
||||||
value={uniqueKey}
|
value={uniqueKey}
|
||||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const uniqueKeys = this.state.uniqueKeys.map((uniqueKey: string, j: number) => {
|
const uniqueKeys = this.state.uniqueKeys.map((uniqueKey: string, j: number) => {
|
||||||
@@ -777,7 +776,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Separator className="panelSeparator" style={{ marginTop: -15, marginBottom: -4 }} />
|
{!isFabricNative() && userContext.apiType === "SQL" && (
|
||||||
|
<Separator className="panelSeparator" style={{ marginTop: -15, marginBottom: -4 }} />
|
||||||
|
)}
|
||||||
|
|
||||||
{shouldShowAnalyticalStoreOptions() && (
|
{shouldShowAnalyticalStoreOptions() && (
|
||||||
<Stack className="panelGroupSpacing" style={{ marginTop: -4 }}>
|
<Stack className="panelGroupSpacing" style={{ marginTop: -4 }}>
|
||||||
@@ -1133,7 +1134,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
private shouldShowCollectionThroughputInput(): boolean {
|
private shouldShowCollectionThroughputInput(): boolean {
|
||||||
if (isFabricNative() || isServerlessAccount()) {
|
if (isServerlessAccount()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1354,8 +1355,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
|
|
||||||
// Throughput
|
// Throughput
|
||||||
if (isFabricNative()) {
|
if (isFabricNative()) {
|
||||||
// Fabric Native accounts are always autoscale and have a fixed throughput of 1K
|
// Fabric Native accounts are always autoscale and have a fixed throughput of 5K
|
||||||
autoPilotMaxThroughput = AutoPilotUtils.autoPilotThroughput1K;
|
autoPilotMaxThroughput = AutoPilotUtils.autoPilotThroughput5K;
|
||||||
offerThroughput = undefined;
|
offerThroughput = undefined;
|
||||||
} else if (databaseLevelThroughput) {
|
} else if (databaseLevelThroughput) {
|
||||||
if (this.state.createNewDatabase) {
|
if (this.state.createNewDatabase) {
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
aria-label="New database id, Type a new database id"
|
aria-label="New database id, Type a new database id"
|
||||||
aria-required={true}
|
aria-required={true}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoFocus={true}
|
|
||||||
className="panelTextField"
|
className="panelTextField"
|
||||||
id="newDatabaseId"
|
id="newDatabaseId"
|
||||||
name="newDatabaseId"
|
name="newDatabaseId"
|
||||||
@@ -143,16 +142,16 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
</StyledTooltipHostBase>
|
</StyledTooltipHostBase>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
<Separator
|
||||||
<Separator
|
className="panelSeparator"
|
||||||
className="panelSeparator"
|
style={
|
||||||
style={
|
{
|
||||||
{
|
"marginBottom": -4,
|
||||||
"marginBottom": -4,
|
"marginTop": -4,
|
||||||
"marginTop": -4,
|
}
|
||||||
}
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
@@ -203,16 +202,16 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
|||||||
type="text"
|
type="text"
|
||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
</Stack>
|
<Separator
|
||||||
<Separator
|
className="panelSeparator"
|
||||||
className="panelSeparator"
|
style={
|
||||||
style={
|
{
|
||||||
{
|
"marginBottom": -5,
|
||||||
"marginBottom": -5,
|
"marginTop": -5,
|
||||||
"marginTop": -5,
|
}
|
||||||
}
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Stack
|
<Stack
|
||||||
horizontal={true}
|
horizontal={true}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@
|
|||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
& > :not(.collapsibleSection) {
|
&> :not(.collapsibleSection) {
|
||||||
margin-bottom: @DefaultSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
|
|
||||||
& > :not(:last-child) {
|
&> :not(:last-child) {
|
||||||
margin-bottom: @DefaultSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,6 +56,14 @@
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.panelMainContent {
|
||||||
|
padding: 0 24px;
|
||||||
|
margin: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.panelHeader {
|
.panelHeader {
|
||||||
@@ -113,70 +121,87 @@
|
|||||||
.deleteCollectionFeedback {
|
.deleteCollectionFeedback {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addRemoveIcon {
|
.addRemoveIcon {
|
||||||
margin-left: 4px !important;
|
margin-left: 4px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addRemoveIconLabel {
|
.addRemoveIconLabel {
|
||||||
margin-top: 28px;
|
margin-top: 28px;
|
||||||
margin-left: 4px !important;
|
margin-left: 4px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addRemoveIcon [alt="editEntity"]:focus,
|
.addRemoveIcon [alt="editEntity"]:focus,
|
||||||
.addRemoveIconLabel [alt="editEntity"]:focus {
|
.addRemoveIconLabel [alt="editEntity"]:focus {
|
||||||
border: 1px dashed #605e5c;
|
border: 1px dashed #605e5c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addNewParamStyle {
|
.addNewParamStyle {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
margin-left: 5px !important;
|
margin-left: 5px !important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panelGroupSpacing > :not(:last-child) {
|
.panelGroupSpacing> :not(:last-child) {
|
||||||
margin-bottom: @DefaultSpace;
|
margin-bottom: @DefaultSpace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fileUpload {
|
.fileUpload {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customFileUpload {
|
.customFileUpload {
|
||||||
padding: 25px 0px 0px 10px;
|
padding: 25px 0px 0px 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fileIcon {
|
.fileIcon {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panelAddIconLabel {
|
.panelAddIconLabel {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
margin: 30px 0 0 10px;
|
margin: 30px 0 0 10px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panelAddIcon {
|
.panelAddIcon {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
margin: 30px 0 0 10px;
|
margin: 30px 0 0 10px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.removeIcon {
|
.removeIcon {
|
||||||
color: @InfoIconColor;
|
color: @InfoIconColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backImageIcon {
|
.backImageIcon {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[alt="back"]:focus {
|
[alt="back"]:focus {
|
||||||
border: 1px solid #605e5c;
|
border: 1px solid #605e5c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addEntityDatePicker {
|
.addEntityDatePicker {
|
||||||
max-width: 145px;
|
max-width: 145px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addEntityTextField {
|
.addEntityTextField {
|
||||||
width: 237px;
|
width: 237px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.addButtonEntiy {
|
.addButtonEntiy {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-select-view {
|
.column-select-view {
|
||||||
margin: 20px 0px 0px 0px;
|
margin: 20px 0px 0px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panelSeparator::before {
|
.panelSeparator::before {
|
||||||
background-color: #edebe9;
|
background-color: #edebe9;
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
|
|||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import React, { FunctionComponent, useState } from "react";
|
import React, { FunctionComponent, useState } from "react";
|
||||||
import create, { UseStore } from "zustand";
|
import { create } from "zustand";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
|
|
||||||
@@ -65,8 +65,6 @@ export interface DataPlaneRbacState {
|
|||||||
setAadDataPlaneUpdated: (aadTokenUpdated: boolean) => void;
|
setAadDataPlaneUpdated: (aadTokenUpdated: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataPlaneRbacStore = UseStore<Partial<DataPlaneRbacState>>;
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
bulletList: {
|
bulletList: {
|
||||||
listStyleType: "disc",
|
listStyleType: "disc",
|
||||||
@@ -100,7 +98,7 @@ const useStyles = makeStyles({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useDataPlaneRbac: DataPlaneRbacStore = create(() => ({
|
export const useDataPlaneRbac = create<Partial<DataPlaneRbacState>>(() => ({
|
||||||
dataPlaneRbacEnabled: false,
|
dataPlaneRbacEnabled: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -199,6 +197,12 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true",
|
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 styles = useStyles();
|
||||||
|
|
||||||
const explorerVersion = configContext.gitSha;
|
const explorerVersion = configContext.gitSha;
|
||||||
@@ -261,6 +265,8 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
useDatabases.getState().sampleDataResourceTokenCollection &&
|
useDatabases.getState().sampleDataResourceTokenCollection &&
|
||||||
!isEmulator;
|
!isEmulator;
|
||||||
|
|
||||||
|
const shouldShowMongoGuidRepresentationOption = userContext.apiType === "Mongo";
|
||||||
|
|
||||||
const handlerOnSubmit = async () => {
|
const handlerOnSubmit = async () => {
|
||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
|
|
||||||
@@ -412,6 +418,10 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldShowMongoGuidRepresentationOption) {
|
||||||
|
LocalStorageUtility.setEntryString(StorageKey.MongoGuidRepresentation, mongoGuidRepresentation);
|
||||||
|
}
|
||||||
|
|
||||||
setIsExecuting(false);
|
setIsExecuting(false);
|
||||||
logConsoleInfo(
|
logConsoleInfo(
|
||||||
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`,
|
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`,
|
||||||
@@ -433,9 +443,14 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logConsoleInfo(
|
if (shouldShowMongoGuidRepresentationOption) {
|
||||||
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
|
logConsoleInfo(
|
||||||
);
|
`Updated Mongo Guid Representation to ${LocalStorageUtility.getEntryString(
|
||||||
|
StorageKey.MongoGuidRepresentation,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
refreshExplorer && (await explorer.refreshExplorer());
|
refreshExplorer && (await explorer.refreshExplorer());
|
||||||
closeSidePanel();
|
closeSidePanel();
|
||||||
};
|
};
|
||||||
@@ -480,6 +495,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
|
{ 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 = (
|
const handleOnPriorityLevelOptionChange = (
|
||||||
ev: React.FormEvent<HTMLInputElement>,
|
ev: React.FormEvent<HTMLInputElement>,
|
||||||
option: IChoiceGroupOption,
|
option: IChoiceGroupOption,
|
||||||
@@ -562,6 +584,13 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
setRefreshExplorer(false);
|
setRefreshExplorer(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnMongoGuidRepresentationOptionChange = (
|
||||||
|
ev: React.FormEvent<HTMLInputElement>,
|
||||||
|
option: IDropdownOption,
|
||||||
|
): void => {
|
||||||
|
setMongoGuidRepresentation(option.key as Constants.MongoGuidRepresentation);
|
||||||
|
};
|
||||||
|
|
||||||
const choiceButtonStyles = {
|
const choiceButtonStyles = {
|
||||||
root: {
|
root: {
|
||||||
clear: "both",
|
clear: "both",
|
||||||
@@ -1068,15 +1097,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
<div className={styles.settingsSectionContainer}>
|
<div className={styles.settingsSectionContainer}>
|
||||||
<div className={styles.settingsSectionDescription}>
|
<div className={styles.settingsSectionDescription}>
|
||||||
This is a sample database and collection with synthetic product data you can use to explore using
|
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
|
NoSQL queries. This will appear as another database in the Data Explorer UI, and is created by,
|
||||||
is created by, and maintained by Microsoft at no cost to you.
|
and maintained by Microsoft at no cost to you.
|
||||||
</div>
|
</div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
styles={{
|
styles={{
|
||||||
label: { padding: 0 },
|
label: { padding: 0 },
|
||||||
}}
|
}}
|
||||||
className="padding"
|
className="padding"
|
||||||
ariaLabel="Enable sample db for Query Advisor"
|
ariaLabel="Enable sample db for query exploration"
|
||||||
checked={copilotSampleDBEnabled}
|
checked={copilotSampleDBEnabled}
|
||||||
onChange={handleSampleDatabaseChange}
|
onChange={handleSampleDatabaseChange}
|
||||||
label="Enable sample database"
|
label="Enable sample database"
|
||||||
@@ -1085,6 +1114,27 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
|
|||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</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>
|
</Accordion>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
||||||
import QueryError from "Common/QueryError";
|
import QueryError from "Common/QueryError";
|
||||||
|
import * as DataModels from "Contracts/DataModels";
|
||||||
import { QueryResults } from "Contracts/ViewModels";
|
import { QueryResults } from "Contracts/ViewModels";
|
||||||
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
import { guid } from "Explorer/Tables/Utilities";
|
import { guid } from "Explorer/Tables/Utilities";
|
||||||
import { QueryCopilotState } from "hooks/useQueryCopilot";
|
import { QueryCopilotState } from "hooks/useQueryCopilot";
|
||||||
|
|
||||||
import React, { createContext, useContext, useState } from "react";
|
import React, { createContext, useContext, useState } from "react";
|
||||||
import create from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
const context = createContext(null);
|
const context = createContext(null);
|
||||||
const useCopilotStore = (): Partial<QueryCopilotState> => useContext(context);
|
const useCopilotStore = (): Partial<QueryCopilotState> => useContext(context);
|
||||||
|
|
||||||
@@ -24,12 +26,12 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
|||||||
isGeneratingQuery: false,
|
isGeneratingQuery: false,
|
||||||
isGeneratingExplanation: false,
|
isGeneratingExplanation: false,
|
||||||
isExecuting: false,
|
isExecuting: false,
|
||||||
dislikeQuery: undefined,
|
dislikeQuery: undefined as boolean,
|
||||||
showCallout: false,
|
showCallout: false,
|
||||||
showSamplePrompts: false,
|
showSamplePrompts: false,
|
||||||
queryIterator: undefined,
|
queryIterator: undefined as MinimalQueryIterator,
|
||||||
queryResults: undefined,
|
queryResults: undefined as QueryResults,
|
||||||
errors: [],
|
errors: [] as QueryError[],
|
||||||
isSamplePromptsOpen: false,
|
isSamplePromptsOpen: false,
|
||||||
showPromptTeachingBubble: true,
|
showPromptTeachingBubble: true,
|
||||||
showDeletePopup: false,
|
showDeletePopup: false,
|
||||||
@@ -41,7 +43,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
|||||||
wasCopilotUsed: false,
|
wasCopilotUsed: false,
|
||||||
showWelcomeSidebar: true,
|
showWelcomeSidebar: true,
|
||||||
showCopilotSidebar: false,
|
showCopilotSidebar: false,
|
||||||
chatMessages: [],
|
chatMessages: [] as CopilotMessage[],
|
||||||
shouldIncludeInMessages: true,
|
shouldIncludeInMessages: true,
|
||||||
showExplanationBubble: false,
|
showExplanationBubble: false,
|
||||||
isAllocatingContainer: false,
|
isAllocatingContainer: false,
|
||||||
@@ -86,7 +88,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
|||||||
},
|
},
|
||||||
|
|
||||||
resetQueryCopilotStates: () => {
|
resetQueryCopilotStates: () => {
|
||||||
set((state) => ({
|
set((state: QueryCopilotState) => ({
|
||||||
...state,
|
...state,
|
||||||
generatedQuery: "",
|
generatedQuery: "",
|
||||||
likeQuery: false,
|
likeQuery: false,
|
||||||
@@ -99,11 +101,11 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
|||||||
isGeneratingQuery: false,
|
isGeneratingQuery: false,
|
||||||
isGeneratingExplanation: false,
|
isGeneratingExplanation: false,
|
||||||
isExecuting: false,
|
isExecuting: false,
|
||||||
dislikeQuery: undefined,
|
dislikeQuery: undefined as boolean,
|
||||||
showCallout: false,
|
showCallout: false,
|
||||||
showSamplePrompts: false,
|
showSamplePrompts: false,
|
||||||
queryIterator: undefined,
|
queryIterator: undefined as MinimalQueryIterator,
|
||||||
queryResults: undefined,
|
queryResults: undefined as QueryResults,
|
||||||
errorMessage: "",
|
errorMessage: "",
|
||||||
isSamplePromptsOpen: false,
|
isSamplePromptsOpen: false,
|
||||||
showPromptTeachingBubble: true,
|
showPromptTeachingBubble: true,
|
||||||
@@ -115,19 +117,19 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
|||||||
generatedQueryComments: "",
|
generatedQueryComments: "",
|
||||||
wasCopilotUsed: false,
|
wasCopilotUsed: false,
|
||||||
showCopilotSidebar: false,
|
showCopilotSidebar: false,
|
||||||
chatMessages: [],
|
chatMessages: [] as CopilotMessage[],
|
||||||
shouldIncludeInMessages: true,
|
shouldIncludeInMessages: true,
|
||||||
showExplanationBubble: false,
|
showExplanationBubble: false,
|
||||||
notebookServerInfo: {
|
notebookServerInfo: {
|
||||||
notebookServerEndpoint: undefined,
|
notebookServerEndpoint: undefined,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
forwardingId: undefined,
|
forwardingId: undefined,
|
||||||
},
|
} as DataModels.NotebookWorkspaceConnectionInfo,
|
||||||
containerStatus: {
|
containerStatus: {
|
||||||
status: undefined,
|
status: undefined,
|
||||||
durationLeftInMinutes: undefined,
|
durationLeftInMinutes: undefined,
|
||||||
phoenixServerInfo: undefined,
|
phoenixServerInfo: undefined,
|
||||||
},
|
} as DataModels.ContainerInfo,
|
||||||
isAllocatingContainer: false,
|
isAllocatingContainer: false,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
@@ -137,3 +139,4 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
|
|||||||
};
|
};
|
||||||
|
|
||||||
export { CopilotProvider, useCopilotStore };
|
export { CopilotProvider, useCopilotStore };
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import { Stack } from "@fluentui/react";
|
import { Stack } from "@fluentui/react";
|
||||||
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
|
||||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
|
||||||
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
|
||||||
@@ -13,7 +11,6 @@ import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotRe
|
|||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { ReactTabKind, TabsState, useTabs } from "hooks/useTabs";
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import SplitterLayout from "react-splitter-layout";
|
import SplitterLayout from "react-splitter-layout";
|
||||||
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
|
||||||
@@ -26,7 +23,8 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
const [copilotActive, setCopilotActive] = useState<boolean>(() =>
|
const [copilotActive, setCopilotActive] = useState<boolean>(() =>
|
||||||
readCopilotToggleStatus(userContext.databaseAccount),
|
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 getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
||||||
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
||||||
@@ -70,17 +68,18 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
useCommandBar.getState().setContextButtons(getCommandbarButtons());
|
useCommandBar.getState().setContextButtons(getCommandbarButtons());
|
||||||
}, [query, selectedQuery, copilotActive]);
|
}, [query, selectedQuery, copilotActive]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
//TODO: Uncomment this effect when query copilot is reinstated in DE
|
||||||
return () => {
|
// React.useEffect(() => {
|
||||||
useTabs.subscribe((state: TabsState) => {
|
// return () => {
|
||||||
if (state.activeReactTab === ReactTabKind.QueryCopilot) {
|
// useTabs.subscribe((state: TabsState) => {
|
||||||
setTabActive(true);
|
// if (state.activeReactTab === ReactTabKind.QueryCopilot) {
|
||||||
} else {
|
// setTabActive(true);
|
||||||
setTabActive(false);
|
// } else {
|
||||||
}
|
// setTabActive(false);
|
||||||
});
|
// }
|
||||||
};
|
// });
|
||||||
}, []);
|
// };
|
||||||
|
// }, []);
|
||||||
|
|
||||||
const toggleCopilot = (toggle: boolean) => {
|
const toggleCopilot = (toggle: boolean) => {
|
||||||
setCopilotActive(toggle);
|
setCopilotActive(toggle);
|
||||||
@@ -90,6 +89,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
return (
|
return (
|
||||||
<Stack className="tab-pane" style={{ width: "100%" }}>
|
<Stack className="tab-pane" style={{ width: "100%" }}>
|
||||||
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
|
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
|
||||||
|
{/*TODO: Uncomment this section when query copilot is reinstated in DE
|
||||||
{tabActive && copilotActive && (
|
{tabActive && copilotActive && (
|
||||||
<QueryCopilotPromptbar
|
<QueryCopilotPromptbar
|
||||||
explorer={explorer}
|
explorer={explorer}
|
||||||
@@ -97,7 +97,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
|
|||||||
databaseId={QueryCopilotSampleDatabaseId}
|
databaseId={QueryCopilotSampleDatabaseId}
|
||||||
containerId={QueryCopilotSampleContainerId}
|
containerId={QueryCopilotSampleContainerId}
|
||||||
></QueryCopilotPromptbar>
|
></QueryCopilotPromptbar>
|
||||||
)}
|
)} */}
|
||||||
<Stack className="tabPaneContentContainer">
|
<Stack className="tabPaneContentContainer">
|
||||||
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
|
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
|
||||||
<EditorReact
|
<EditorReact
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Accordion top class
|
* Accordion top class
|
||||||
*/
|
*/
|
||||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
import { Link, makeStyles, tokens } from "@fluentui/react-components";
|
||||||
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
|
import { DocumentAddRegular, LinkMultipleRegular, OpenRegular } from "@fluentui/react-icons";
|
||||||
import { SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
|
import { SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
|
||||||
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
|
||||||
import { isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
|
import { isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
|
||||||
@@ -119,7 +119,7 @@ const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className
|
|||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
return (
|
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 className={styles.buttonUpperPart}>{icon}</div>
|
||||||
<div aria-label={title} className={styles.buttonLowerPart}>
|
<div aria-label={title} className={styles.buttonLowerPart}>
|
||||||
<div>{title}</div>
|
<div>{title}</div>
|
||||||
@@ -147,7 +147,7 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
|||||||
{
|
{
|
||||||
title: "Sample data",
|
title: "Sample data",
|
||||||
description: "Automatically load sample data in your database",
|
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),
|
onClick: () => setOpenSampleDataImportDialog(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -181,16 +181,18 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
|
|||||||
explorer={props.explorer}
|
explorer={props.explorer}
|
||||||
databaseName={userContext.fabricContext?.databaseName}
|
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}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
{getSplashScreenButtons()}
|
{getSplashScreenButtons()}
|
||||||
{/* <div className={styles.footer}>
|
{
|
||||||
Need help?{" "}
|
<div className={styles.footer}>
|
||||||
<Link href="https://aka.ms/cosmosdbfabricdocs" target="_blank">
|
Need help?{" "}
|
||||||
Learn more <img src={LinkIcon} alt="Learn more" />
|
<Link href="https://learn.microsoft.com/fabric/database/cosmos-db/overview" target="_blank">
|
||||||
</Link>
|
Learn more <OpenRegular />
|
||||||
</div> */}
|
</Link>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</CosmosFluentProvider>
|
</CosmosFluentProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { ReactTabKind, useTabs } from "hooks/useTabs";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import ConnectIcon from "../../../images/Connect_color.svg";
|
import ConnectIcon from "../../../images/Connect_color.svg";
|
||||||
import ContainersIcon from "../../../images/Containers.svg";
|
import ContainersIcon from "../../../images/Containers.svg";
|
||||||
|
import CosmosDBIcon from "../../../images/CosmosDB-logo.svg";
|
||||||
import LinkIcon from "../../../images/Link_blue.svg";
|
import LinkIcon from "../../../images/Link_blue.svg";
|
||||||
import PowerShellIcon from "../../../images/PowerShell.svg";
|
import PowerShellIcon from "../../../images/PowerShell.svg";
|
||||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||||
@@ -76,39 +77,39 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
{
|
{
|
||||||
dispose: useNotebook.subscribe(
|
dispose: useNotebook.subscribe(
|
||||||
() => this.setState({}),
|
|
||||||
(state) => state.isNotebookEnabled,
|
(state) => state.isNotebookEnabled,
|
||||||
|
() => this.setState({}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ dispose: useSelectedNode.subscribe(() => this.setState({})) },
|
{ dispose: useSelectedNode.subscribe(() => this.setState({})) },
|
||||||
{
|
{
|
||||||
dispose: useCarousel.subscribe(
|
dispose: useCarousel.subscribe(
|
||||||
() => this.setState({}),
|
|
||||||
(state) => state.showCoachMark,
|
(state) => state.showCoachMark,
|
||||||
|
() => this.setState({}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dispose: usePostgres.subscribe(
|
dispose: usePostgres.subscribe(
|
||||||
() => this.setState({}),
|
|
||||||
(state) => state.showPostgreTeachingBubble,
|
(state) => state.showPostgreTeachingBubble,
|
||||||
|
() => this.setState({}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dispose: usePostgres.subscribe(
|
dispose: usePostgres.subscribe(
|
||||||
() => this.setState({}),
|
|
||||||
(state) => state.showResetPasswordBubble,
|
(state) => state.showResetPasswordBubble,
|
||||||
|
() => this.setState({}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dispose: useDatabases.subscribe(
|
dispose: useDatabases.subscribe(
|
||||||
() => this.setState({}),
|
|
||||||
(state) => state.sampleDataResourceTokenCollection,
|
(state) => state.sampleDataResourceTokenCollection,
|
||||||
|
() => this.setState({}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dispose: useQueryCopilot.subscribe(
|
dispose: useQueryCopilot.subscribe(
|
||||||
() => this.setState({}),
|
|
||||||
(state) => state.copilotEnabled,
|
(state) => state.copilotEnabled,
|
||||||
|
() => this.setState({}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -120,11 +121,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private getSplashScreenButtons = (): JSX.Element => {
|
private getSplashScreenButtons = (): JSX.Element => {
|
||||||
if (
|
if (userContext.apiType === "SQL") {
|
||||||
userContext.apiType === "SQL" &&
|
|
||||||
useQueryCopilot.getState().copilotEnabled &&
|
|
||||||
useDatabases.getState().sampleDataResourceTokenCollection
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
className="splashStackContainer"
|
className="splashStackContainer"
|
||||||
@@ -152,25 +149,18 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack className="splashStackRow" horizontal>
|
<Stack className="splashStackRow" horizontal>
|
||||||
{useQueryCopilot.getState().copilotEnabled && (
|
<SplashScreenButton
|
||||||
<SplashScreenButton
|
imgSrc={CosmosDBIcon}
|
||||||
imgSrc={CopilotIcon}
|
imgSize={35}
|
||||||
title={"Query faster with Query Advisor"}
|
title={"Azure Cosmos DB Samples Gallery"}
|
||||||
description={
|
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!"
|
"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={() => {
|
onClick={() => {
|
||||||
const copilotVersion = userContext.features.copilotVersion;
|
window.open("https://azurecosmosdb.github.io/gallery/?tags=example", "_blank");
|
||||||
if (copilotVersion === "v1.0") {
|
traceOpen(Action.LearningResourcesClicked, { apiType: userContext.apiType });
|
||||||
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 });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<SplashScreenButton
|
<SplashScreenButton
|
||||||
imgSrc={ConnectIcon}
|
imgSrc={ConnectIcon}
|
||||||
title={"Connect"}
|
title={"Connect"}
|
||||||
@@ -212,6 +202,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
sample data, query.
|
sample data, query.
|
||||||
</TeachingBubble>
|
</TeachingBubble>
|
||||||
)}
|
)}
|
||||||
|
{/*TODO: convert below to use SplashScreenButton */}
|
||||||
{mainItems.map((item) => (
|
{mainItems.map((item) => (
|
||||||
<Stack
|
<Stack
|
||||||
id={`mainButton-${item.id}`}
|
id={`mainButton-${item.id}`}
|
||||||
@@ -477,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) {
|
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
|
||||||
return {
|
return {
|
||||||
iconSrc: CollectionIcon,
|
iconSrc: CollectionIcon,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ interface SplashScreenButtonProps {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
imgSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
||||||
@@ -14,6 +15,7 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
onClick,
|
onClick,
|
||||||
|
imgSize,
|
||||||
}: SplashScreenButtonProps): JSX.Element => {
|
}: SplashScreenButtonProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
@@ -39,7 +41,7 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
|
|||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<img src={imgSrc} alt={title} aria-hidden="true" />
|
<img src={imgSrc} alt={title} aria-hidden="true" {...(imgSize ? { height: imgSize, width: imgSize } : {})} />
|
||||||
</div>
|
</div>
|
||||||
<Stack style={{ marginLeft: 16 }}>
|
<Stack style={{ marginLeft: 16 }}>
|
||||||
<Text style={{ fontSize: 18, fontWeight: 600 }}>{title}</Text>
|
<Text style={{ fontSize: 18, fontWeight: 600 }}>{title}</Text>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
|||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
import { getAuthorizationHeader, isDataplaneRbacEnabledForProxyApi } from "../../Utils/AuthorizationUtils";
|
||||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
@@ -551,6 +551,10 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
const authorizationHeaderMetadata: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
const authorizationHeaderMetadata: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
|
||||||
xhr.setRequestHeader(authorizationHeaderMetadata.header, authorizationHeaderMetadata.token);
|
xhr.setRequestHeader(authorizationHeaderMetadata.header, authorizationHeaderMetadata.token);
|
||||||
|
|
||||||
|
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
|
||||||
|
xhr.setRequestHeader(Constants.HttpHeaders.entraIdToken, userContext.aadToken);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FitAddon } from "@xterm/addon-fit";
|
import { FitAddon } from "@xterm/addon-fit";
|
||||||
import { Terminal } from "@xterm/xterm";
|
import { Terminal } from "@xterm/xterm";
|
||||||
|
import "@xterm/xterm/css/xterm.css";
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import "xterm/css/xterm.css";
|
|
||||||
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
import { DatabaseAccount } from "../../../Contracts/DataModels";
|
||||||
import { TerminalKind } from "../../../Contracts/ViewModels";
|
import { TerminalKind } from "../../../Contracts/ViewModels";
|
||||||
import { startCloudShellTerminal } from "./CloudShellTerminalCore";
|
import { startCloudShellTerminal } from "./CloudShellTerminalCore";
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { formatErrorMessage, formatInfoMessage, formatWarningMessage } from "./U
|
|||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const DEFAULT_CLOUDSHELL_REGION = "westus";
|
const DEFAULT_CLOUDSHELL_REGION = "westus";
|
||||||
|
const DEFAULT_FAIRFAX_CLOUDSHELL_REGION = "usgovvirginia";
|
||||||
const POLLING_INTERVAL_MS = 2000;
|
const POLLING_INTERVAL_MS = 2000;
|
||||||
const MAX_RETRY_COUNT = 10;
|
const MAX_RETRY_COUNT = 10;
|
||||||
const MAX_PING_COUNT = 120 * 60; // 120 minutes (60 seconds/minute)
|
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();
|
||||||
|
|
||||||
resolvedRegion = determineCloudShellRegion();
|
|
||||||
|
|
||||||
terminal.writeln(formatWarningMessage("⚠️ IMPORTANT: Azure Cloud Shell Region Notice ⚠️"));
|
terminal.writeln(formatWarningMessage("⚠️ IMPORTANT: Azure Cloud Shell Region Notice ⚠️"));
|
||||||
terminal.writeln(
|
terminal.writeln(
|
||||||
formatInfoMessage(
|
formatInfoMessage(
|
||||||
"The Cloud Shell environment will operate in a region that may differ from your database's region.",
|
"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("1. Performance Impact:"));
|
||||||
terminal.writeln(
|
terminal.writeln(
|
||||||
formatInfoMessage(" Commands may experience higher latency due to geographic distance between regions."),
|
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(
|
terminal.writeln(
|
||||||
formatInfoMessage(
|
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("");
|
||||||
|
|
||||||
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");
|
terminal.writeln("\x1b[94mhttps://learn.microsoft.com/en-us/azure/cosmos-db/data-residency\x1b[0m");
|
||||||
|
|
||||||
// Ask for user consent for region
|
// Ask for user consent for region
|
||||||
@@ -159,7 +154,9 @@ export const ensureCloudShellProviderRegistered = async (): Promise<void> => {
|
|||||||
* Determines the appropriate CloudShell region
|
* Determines the appropriate CloudShell region
|
||||||
*/
|
*/
|
||||||
export const determineCloudShellRegion = (): string => {
|
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
|
||||||
|
|
||||||
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:
|
Data residency requirements may not be fully satisfied when using CloudShell due to limited regional availability.
|
||||||
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
**Note:** For up-to-date supported regions, refer to the region configuration in:
|
**Note:** For up-to-date supported regions, refer to the region configuration in:
|
||||||
`src/Explorer/CloudShell/Configuration/RegionConfig.ts`
|
`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
|
// Mock implementation for testing
|
||||||
class MockShellHandler extends AbstractShellHandler {
|
class MockShellHandler extends AbstractShellHandler {
|
||||||
@@ -18,8 +18,8 @@ class MockShellHandler extends AbstractShellHandler {
|
|||||||
return "mock-endpoint";
|
return "mock-endpoint";
|
||||||
}
|
}
|
||||||
|
|
||||||
getTerminalSuppressedData(): string {
|
getTerminalSuppressedData(): string[] {
|
||||||
return "suppressed-data";
|
return ["suppressed-data"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ describe("AbstractShellHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return the terminal suppressed data", () => {
|
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.
|
* Command that displays an error message and exits the shell session.
|
||||||
* Used when shell initialization or connection fails.
|
* 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
|
* Abstract class that defines the interface for shell-specific handlers
|
||||||
@@ -31,7 +42,16 @@ export abstract class AbstractShellHandler {
|
|||||||
abstract getShellName(): string;
|
abstract getShellName(): string;
|
||||||
abstract getSetUpCommands(): string[];
|
abstract getSetUpCommands(): string[];
|
||||||
abstract getConnectionCommand(): 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.
|
* Constructs the complete initialization command sequence for the shell.
|
||||||
@@ -57,7 +77,7 @@ export abstract class AbstractShellHandler {
|
|||||||
START_MARKER,
|
START_MARKER,
|
||||||
DISABLE_HISTORY,
|
DISABLE_HISTORY,
|
||||||
...setupCommands,
|
...setupCommands,
|
||||||
`{ ${connectionCommand}; } || true;${EXIT_COMMAND}`,
|
`{ ${connectionCommand}; } || true;${this.getExitCommand()}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
return allCommands.join("\n").concat("\n");
|
return allCommands.join("\n").concat("\n");
|
||||||
@@ -77,7 +97,7 @@ export abstract class AbstractShellHandler {
|
|||||||
* is not already present in the environment.
|
* is not already present in the environment.
|
||||||
*/
|
*/
|
||||||
protected mongoShellSetupCommands(): string[] {
|
protected mongoShellSetupCommands(): string[] {
|
||||||
const PACKAGE_VERSION: string = "2.5.0";
|
const PACKAGE_VERSION: string = "2.5.6";
|
||||||
return [
|
return [
|
||||||
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
|
"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`,
|
`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 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 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",
|
"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", () => {
|
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", () => {
|
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`;
|
return `cqlsh ${getHostFromUrl(this._endpoint)} 10350 -u ${dbName} -p ${this._key} --ssl`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTerminalSuppressedData(): string {
|
public getTerminalSuppressedData(): string[] {
|
||||||
return "";
|
return [""];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ interface DatabaseAccount {
|
|||||||
|
|
||||||
interface UserContextType {
|
interface UserContextType {
|
||||||
databaseAccount: DatabaseAccount;
|
databaseAccount: DatabaseAccount;
|
||||||
|
features: {
|
||||||
|
enableAadDataPlane: boolean;
|
||||||
|
};
|
||||||
|
apiType: string;
|
||||||
|
dataPlaneRbacEnabled: boolean;
|
||||||
|
aadToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
@@ -29,10 +35,13 @@ jest.mock("../../../../UserContext", () => ({
|
|||||||
mongoEndpoint: "https://test-mongo.documents.azure.com:443/",
|
mongoEndpoint: "https://test-mongo.documents.azure.com:443/",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
features: { enableAadDataPlane: false },
|
||||||
|
apiType: "Mongo",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("../Utils/CommonUtils", () => ({
|
jest.mock("../Utils/CommonUtils", () => ({
|
||||||
|
...jest.requireActual("../Utils/CommonUtils"),
|
||||||
getHostFromUrl: jest.fn().mockReturnValue("test-mongo.documents.azure.com"),
|
getHostFromUrl: jest.fn().mockReturnValue("test-mongo.documents.azure.com"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -69,7 +78,7 @@ describe("MongoShellHandler", () => {
|
|||||||
|
|
||||||
expect(Array.isArray(commands)).toBe(true);
|
expect(Array.isArray(commands)).toBe(true);
|
||||||
expect(commands.length).toBe(7);
|
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",
|
kind: "test-kind",
|
||||||
properties: { mongoEndpoint: "https://test-mongo.documents.azure.com:443/" },
|
properties: { mongoEndpoint: "https://test-mongo.documents.azure.com:443/" },
|
||||||
};
|
};
|
||||||
|
(userContext as UserContextType).dataPlaneRbacEnabled = false;
|
||||||
|
|
||||||
const command = mongoShellHandler.getConnectionCommand();
|
const command = mongoShellHandler.getConnectionCommand();
|
||||||
|
|
||||||
expect(command).toBe(
|
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/");
|
expect(CommonUtils.getHostFromUrl).toHaveBeenCalledWith("https://test-mongo.documents.azure.com:443/");
|
||||||
|
|
||||||
@@ -114,17 +124,55 @@ describe("MongoShellHandler", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const command = mongoShellHandler.getConnectionCommand();
|
const command = mongoShellHandler.getConnectionCommand();
|
||||||
|
|
||||||
expect(command).toBe("echo 'Database name not found.'");
|
expect(command).toBe("echo 'Database name not found.'");
|
||||||
|
|
||||||
// Restore original
|
// Restore original
|
||||||
(userContext as UserContextType).databaseAccount = originalDatabaseAccount;
|
(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", () => {
|
describe("getTerminalSuppressedData", () => {
|
||||||
it("should return the correct warning message", () => {
|
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 { userContext } from "../../../../UserContext";
|
||||||
import { getHostFromUrl } from "../Utils/CommonUtils";
|
import { isDataplaneRbacEnabledForProxyApi } from "../../../../Utils/AuthorizationUtils";
|
||||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
import { filterAndCleanTerminalOutput, getHostFromUrl, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
|
||||||
|
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND, EXIT_COMMAND_MONGO } from "./AbstractShellHandler";
|
||||||
|
|
||||||
export class MongoShellHandler extends AbstractShellHandler {
|
export class MongoShellHandler extends AbstractShellHandler {
|
||||||
private _key: string;
|
private _key: string;
|
||||||
private _endpoint: string | undefined;
|
private _endpoint: string | undefined;
|
||||||
|
private _removeInfoText: string[] = getMongoShellRemoveInfoText();
|
||||||
|
private _isEntraIdEnabled: boolean = isDataplaneRbacEnabledForProxyApi(userContext);
|
||||||
constructor(private key: string) {
|
constructor(private key: string) {
|
||||||
super();
|
super();
|
||||||
this._key = key;
|
this._key = key;
|
||||||
this._endpoint = userContext?.databaseAccount?.properties?.mongoEndpoint;
|
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 {
|
public getShellName(): string {
|
||||||
return "MongoDB";
|
return "MongoDB";
|
||||||
}
|
}
|
||||||
@@ -28,20 +41,22 @@ export class MongoShellHandler extends AbstractShellHandler {
|
|||||||
if (!dbName) {
|
if (!dbName) {
|
||||||
return "echo 'Database name not found.'";
|
return "echo 'Database name not found.'";
|
||||||
}
|
}
|
||||||
return (
|
const connectionCommand = this._isEntraIdEnabled
|
||||||
"mongosh mongodb://" +
|
? this._getAadConnectionCommand(dbName)
|
||||||
getHostFromUrl(this._endpoint) +
|
: this._getKeyConnectionCommand(dbName);
|
||||||
":10255?appName=" +
|
const fullCommand = `${DISABLE_TELEMETRY_COMMAND}; ${connectionCommand}`;
|
||||||
this.APP_NAME +
|
return fullCommand;
|
||||||
" --username " +
|
|
||||||
dbName +
|
|
||||||
" --password " +
|
|
||||||
this._key +
|
|
||||||
" --tls --tlsAllowInvalidCertificates"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTerminalSuppressedData(): string {
|
public getTerminalSuppressedData(): string[] {
|
||||||
return "Warning: Non-Genuine MongoDB Detected";
|
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", () => {
|
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}`;
|
return `psql -h "${this._endpoint}" -p 5432 -d "citus" -U "${loginName}" --set=sslmode=require --set=application_name=${this.APP_NAME}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTerminalSuppressedData(): string {
|
public getTerminalSuppressedData(): string[] {
|
||||||
return "";
|
return [""];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,24 @@ import { PostgresShellHandler } from "./PostgresShellHandler";
|
|||||||
import { getHandler, getKey } from "./ShellTypeFactory";
|
import { getHandler, getKey } from "./ShellTypeFactory";
|
||||||
import { VCoreMongoShellHandler } from "./VCoreMongoShellHandler";
|
import { VCoreMongoShellHandler } from "./VCoreMongoShellHandler";
|
||||||
|
|
||||||
|
interface UserContextType {
|
||||||
|
databaseAccount: { name: string };
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroup: string;
|
||||||
|
features: { enableAadDataPlane: boolean };
|
||||||
|
dataPlaneRbacEnabled: boolean;
|
||||||
|
aadToken?: string;
|
||||||
|
apiType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
jest.mock("../../../../UserContext", () => ({
|
jest.mock("../../../../UserContext", () => ({
|
||||||
userContext: {
|
userContext: {
|
||||||
databaseAccount: { name: "testDbName" },
|
databaseAccount: { name: "testDbName" },
|
||||||
subscriptionId: "testSubId",
|
subscriptionId: "testSubId",
|
||||||
resourceGroup: "testResourceGroup",
|
resourceGroup: "testResourceGroup",
|
||||||
|
features: { enableAadDataPlane: false },
|
||||||
|
dataPlaneRbacEnabled: false,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -109,5 +121,33 @@ describe("ShellTypeHandlerFactory", () => {
|
|||||||
expect(key).toBe(mockKey);
|
expect(key).toBe(mockKey);
|
||||||
expect(listKeys).toHaveBeenCalledWith("testSubId", "testResourceGroup", "testDbName");
|
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 { TerminalKind } from "../../../../Contracts/ViewModels";
|
||||||
import { userContext } from "../../../../UserContext";
|
import { userContext } from "../../../../UserContext";
|
||||||
import { listKeys } from "../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { listKeys } from "../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
|
import { isDataplaneRbacEnabledForProxyApi } from "../../../../Utils/AuthorizationUtils";
|
||||||
import { AbstractShellHandler } from "./AbstractShellHandler";
|
import { AbstractShellHandler } from "./AbstractShellHandler";
|
||||||
import { CassandraShellHandler } from "./CassandraShellHandler";
|
import { CassandraShellHandler } from "./CassandraShellHandler";
|
||||||
import { MongoShellHandler } from "./MongoShellHandler";
|
import { MongoShellHandler } from "./MongoShellHandler";
|
||||||
@@ -30,6 +31,9 @@ export async function getKey(): Promise<string> {
|
|||||||
if (!dbName) {
|
if (!dbName) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
|
||||||
|
return userContext.aadToken || "";
|
||||||
|
}
|
||||||
|
|
||||||
const keys = await listKeys(userContext.subscriptionId, userContext.resourceGroup, dbName);
|
const keys = await listKeys(userContext.subscriptionId, userContext.resourceGroup, dbName);
|
||||||
return keys?.primaryMasterKey || "";
|
return keys?.primaryMasterKey || "";
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe("VCoreMongoShellHandler", () => {
|
|||||||
|
|
||||||
expect(Array.isArray(commands)).toBe(true);
|
expect(Array.isArray(commands)).toBe(true);
|
||||||
expect(commands.length).toBe(7);
|
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");
|
expect(commands[0]).toContain("mongosh not found");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,7 +57,10 @@ describe("VCoreMongoShellHandler", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return the correct terminal suppressed data", () => {
|
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 { 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 {
|
export class VCoreMongoShellHandler extends AbstractShellHandler {
|
||||||
private _endpoint: string | undefined;
|
private _endpoint: string | undefined;
|
||||||
|
private _removeInfoText: string[] = getMongoShellRemoveInfoText();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -23,10 +25,24 @@ export class VCoreMongoShellHandler extends AbstractShellHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userName = userContext.vcoreMongoConnectionParams.adminLogin;
|
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 {
|
public getTerminalSuppressedData(): string[] {
|
||||||
return "Warning: Non-Genuine MongoDB Detected";
|
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
|
* @param {Terminal} terminal - The XTerm terminal instance
|
||||||
*/
|
*/
|
||||||
public addMessageListener(terminal: Terminal): void {
|
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(
|
this._disposables.push(
|
||||||
addSocketListener(this._socket, "message", (ev) => {
|
addSocketListener(this._socket, "message", (ev) => {
|
||||||
let data: ArrayBuffer | string = ev.data;
|
let data: ArrayBuffer | string = ev.data;
|
||||||
@@ -103,51 +115,136 @@ export class AttachAddon implements ITerminalAddon {
|
|||||||
data = enc.decode(ev.data as ArrayBuffer);
|
data = enc.decode(ev.data as ArrayBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// for example of json object look in TerminalHelper in the socket.onMessage
|
// Handle status messages
|
||||||
if (data.includes(startStatusJson) && data.includes(endStatusJson)) {
|
let processedStatusData = data;
|
||||||
// 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 = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._allowTerminalWrite && data.includes(this._startMarker)) {
|
// Process status messages with delimiters
|
||||||
this._allowTerminalWrite = false;
|
// eslint-disable-next-line no-constant-condition
|
||||||
terminal.write(`Preparing ${this._shellHandler.getShellName()} environment...\r\n`);
|
while (true) {
|
||||||
}
|
const startIndex = processedStatusData.indexOf(startStatusJson);
|
||||||
|
if (startIndex === -1) {
|
||||||
if (this._allowTerminalWrite) {
|
break;
|
||||||
const suppressedData = this._shellHandler?.getTerminalSuppressedData();
|
|
||||||
const hasSuppressedData = suppressedData && suppressedData.length > 0;
|
|
||||||
|
|
||||||
if (!hasSuppressedData || !data.includes(suppressedData)) {
|
|
||||||
terminal.write(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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())) {
|
// Add to message buffer
|
||||||
this._allowTerminalWrite = true;
|
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 {
|
public dispose(): void {
|
||||||
|
|||||||
@@ -50,3 +50,34 @@ export const getShellNameForDisplay = (terminalKind: TerminalKind): string => {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get MongoDB shell information text that should be removed from terminal output
|
||||||
|
*/
|
||||||
|
export const getMongoShellRemoveInfoText = (): string[] => {
|
||||||
|
return [
|
||||||
|
"For mongosh info see: https://www.mongodb.com/docs/mongodb-shell/",
|
||||||
|
"disableTelemetry() command",
|
||||||
|
"https://www.mongodb.com/legal/privacy-policy",
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterAndCleanTerminalOutput = (data: string, removeInfoText: string[]): string => {
|
||||||
|
if (!data || removeInfoText.length === 0) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = data.split("\n");
|
||||||
|
const filteredLines: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const shouldRemove = removeInfoText.some((text) => line.includes(text));
|
||||||
|
|
||||||
|
if (!shouldRemove) {
|
||||||
|
filteredLines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredLines.join("\n").replace(/((\r\n)|\n|\r){2,}/g, "\r\n");
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ const validCloudShellRegions = new Set([
|
|||||||
"westeurope",
|
"westeurope",
|
||||||
"centralindia",
|
"centralindia",
|
||||||
"southeastasia",
|
"southeastasia",
|
||||||
"westcentralus",
|
"usgovvirginia",
|
||||||
|
"usgovarizona",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,7 +40,6 @@ export const getNormalizedRegion = (region: string, defaultCloudshellRegion: str
|
|||||||
}
|
}
|
||||||
|
|
||||||
const regionMap: Record<string, string> = {
|
const regionMap: Record<string, string> = {
|
||||||
centralus: "westcentralus",
|
|
||||||
eastus2: "eastus",
|
eastus2: "eastus",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -146,10 +146,16 @@ describe("Documents tab (Mongo API)", () => {
|
|||||||
updateConfigContext({ platform: Platform.Hosted });
|
updateConfigContext({ platform: Platform.Hosted });
|
||||||
|
|
||||||
const props: IDocumentsTabComponentProps = createMockProps();
|
const props: IDocumentsTabComponentProps = createMockProps();
|
||||||
|
|
||||||
wrapper = mount(<DocumentsTabComponent {...props} />);
|
wrapper = mount(<DocumentsTabComponent {...props} />);
|
||||||
wrapper = await waitForComponentToPaint(wrapper);
|
|
||||||
});
|
// Wait for all pending promises
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for any async operations to complete
|
||||||
|
wrapper = await waitForComponentToPaint(wrapper, 100);
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export interface IGraphConfig {
|
|||||||
|
|
||||||
interface GraphTabOptions extends ViewModels.TabOptions {
|
interface GraphTabOptions extends ViewModels.TabOptions {
|
||||||
account: DatabaseAccount;
|
account: DatabaseAccount;
|
||||||
masterKey: string;
|
password: string;
|
||||||
collectionId: string;
|
collectionId: string;
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
collectionPartitionKeyProperty: string;
|
collectionPartitionKeyProperty: string;
|
||||||
@@ -107,7 +107,7 @@ export default class GraphTab extends TabsBase {
|
|||||||
graphBackendEndpoint: GraphTab.getGremlinEndpoint(options.account),
|
graphBackendEndpoint: GraphTab.getGremlinEndpoint(options.account),
|
||||||
databaseId: options.databaseId,
|
databaseId: options.databaseId,
|
||||||
collectionId: options.collectionId,
|
collectionId: options.collectionId,
|
||||||
masterKey: options.masterKey,
|
password: options.password,
|
||||||
onLoadStartKey: options.onLoadStartKey,
|
onLoadStartKey: options.onLoadStartKey,
|
||||||
onLoadStartKeyChange: (onLoadStartKey: number): void => {
|
onLoadStartKeyChange: (onLoadStartKey: number): void => {
|
||||||
if (onLoadStartKey === undefined) {
|
if (onLoadStartKey === undefined) {
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
|||||||
this.container = options.container;
|
this.container = options.container;
|
||||||
this.notebookPath = ko.observable(options.notebookContentItem.path);
|
this.notebookPath = ko.observable(options.notebookContentItem.path);
|
||||||
useNotebook.subscribe(
|
useNotebook.subscribe(
|
||||||
() => logConsoleInfo("New notebook server info received."),
|
|
||||||
(state) => state.notebookServerInfo,
|
(state) => state.notebookServerInfo,
|
||||||
|
() => logConsoleInfo("New notebook server info received."),
|
||||||
);
|
);
|
||||||
this.notebookComponentAdapter = new NotebookComponentAdapter({
|
this.notebookComponentAdapter = new NotebookComponentAdapter({
|
||||||
contentItem: options.notebookContentItem,
|
contentItem: options.notebookContentItem,
|
||||||
@@ -165,7 +165,7 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
|||||||
{
|
{
|
||||||
iconSrc: null,
|
iconSrc: null,
|
||||||
iconAlt: kernelLabel,
|
iconAlt: kernelLabel,
|
||||||
onCommandClick: () => {},
|
onCommandClick: () => { },
|
||||||
commandButtonLabel: null,
|
commandButtonLabel: null,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: availableKernels.length < 1,
|
disabled: availableKernels.length < 1,
|
||||||
@@ -276,7 +276,7 @@ export default class NotebookTabV2 extends NotebookTabBase {
|
|||||||
{
|
{
|
||||||
iconSrc: null,
|
iconSrc: null,
|
||||||
iconAlt: null,
|
iconAlt: null,
|
||||||
onCommandClick: () => {},
|
onCommandClick: () => { },
|
||||||
commandButtonLabel: null,
|
commandButtonLabel: null,
|
||||||
ariaLabel: cellTypeLabel,
|
ariaLabel: cellTypeLabel,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
|
|||||||
@@ -106,6 +106,6 @@ describe("QueryTabComponent", () => {
|
|||||||
<QueryTabCopilotComponent {...propsMock} />
|
<QueryTabCopilotComponent {...propsMock} />
|
||||||
</CopilotProvider>,
|
</CopilotProvider>,
|
||||||
);
|
);
|
||||||
expect(container.find(QueryCopilotPromptbar).exists()).toBe(true);
|
expect(container.find(QueryCopilotPromptbar).exists()).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { useDialog } from "Explorer/Controls/Dialog";
|
|||||||
import { monaco } from "Explorer/LazyMonaco";
|
import { monaco } from "Explorer/LazyMonaco";
|
||||||
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
|
||||||
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
|
||||||
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
|
|
||||||
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
|
||||||
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
|
||||||
@@ -28,8 +27,9 @@ import { TabsState, useTabs } from "hooks/useTabs";
|
|||||||
import React, { Fragment, createRef } from "react";
|
import React, { Fragment, createRef } from "react";
|
||||||
import "react-splitter-layout/lib/index.css";
|
import "react-splitter-layout/lib/index.css";
|
||||||
import { format } from "react-string-format";
|
import { format } from "react-string-format";
|
||||||
import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
|
//TODO: Uncomment next two lines when query copilot is reinstated in DE
|
||||||
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
// import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
|
||||||
|
// import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
|
||||||
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
|
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
|
||||||
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
|
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
|
||||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||||
@@ -494,53 +494,55 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.launchCopilotButton.visible && this.isCopilotTabActive) {
|
//TODO: Uncomment next section when query copilot is reinstated in DE
|
||||||
const mainButtonLabel = "Launch Copilot";
|
// if (this.launchCopilotButton.visible && this.isCopilotTabActive) {
|
||||||
const chatPaneLabel = "Open Copilot in chat pane (ALT+C)";
|
// const mainButtonLabel = "Launch Copilot";
|
||||||
const copilotSettingLabel = "Copilot settings";
|
// const chatPaneLabel = "Open Copilot in chat pane (ALT+C)";
|
||||||
|
// const copilotSettingLabel = "Copilot settings";
|
||||||
|
|
||||||
const openCopilotChatButton: CommandButtonComponentProps = {
|
// const openCopilotChatButton: CommandButtonComponentProps = {
|
||||||
iconAlt: chatPaneLabel,
|
// iconAlt: chatPaneLabel,
|
||||||
onCommandClick: this.launchQueryCopilotChat,
|
// onCommandClick: this.launchQueryCopilotChat,
|
||||||
commandButtonLabel: chatPaneLabel,
|
// commandButtonLabel: chatPaneLabel,
|
||||||
ariaLabel: chatPaneLabel,
|
// ariaLabel: chatPaneLabel,
|
||||||
hasPopup: false,
|
// hasPopup: false,
|
||||||
};
|
// };
|
||||||
|
|
||||||
const copilotSettingsButton: CommandButtonComponentProps = {
|
// const copilotSettingsButton: CommandButtonComponentProps = {
|
||||||
iconAlt: copilotSettingLabel,
|
// iconAlt: copilotSettingLabel,
|
||||||
onCommandClick: () => undefined,
|
// onCommandClick: () => undefined,
|
||||||
commandButtonLabel: copilotSettingLabel,
|
// commandButtonLabel: copilotSettingLabel,
|
||||||
ariaLabel: copilotSettingLabel,
|
// ariaLabel: copilotSettingLabel,
|
||||||
hasPopup: false,
|
// hasPopup: false,
|
||||||
};
|
// };
|
||||||
|
|
||||||
const launchCopilotButton: CommandButtonComponentProps = {
|
// const launchCopilotButton: CommandButtonComponentProps = {
|
||||||
iconSrc: LaunchCopilot,
|
// iconSrc: LaunchCopilot,
|
||||||
iconAlt: mainButtonLabel,
|
// iconAlt: mainButtonLabel,
|
||||||
onCommandClick: this.launchQueryCopilotChat,
|
// onCommandClick: this.launchQueryCopilotChat,
|
||||||
commandButtonLabel: mainButtonLabel,
|
// commandButtonLabel: mainButtonLabel,
|
||||||
ariaLabel: mainButtonLabel,
|
// ariaLabel: mainButtonLabel,
|
||||||
hasPopup: false,
|
// hasPopup: false,
|
||||||
children: [openCopilotChatButton, copilotSettingsButton],
|
// children: [openCopilotChatButton, copilotSettingsButton],
|
||||||
};
|
// };
|
||||||
buttons.push(launchCopilotButton);
|
// buttons.push(launchCopilotButton);
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (this.props.copilotEnabled) {
|
//TODO: Uncomment next section when query copilot is reinstated in DE
|
||||||
const toggleCopilotButton: CommandButtonComponentProps = {
|
// if (this.props.copilotEnabled) {
|
||||||
iconSrc: QueryCommandIcon,
|
// const toggleCopilotButton: CommandButtonComponentProps = {
|
||||||
iconAlt: "Query Advisor",
|
// iconSrc: QueryCommandIcon,
|
||||||
keyboardAction: KeyboardAction.TOGGLE_COPILOT,
|
// iconAlt: "Query Advisor",
|
||||||
onCommandClick: () => {
|
// keyboardAction: KeyboardAction.TOGGLE_COPILOT,
|
||||||
this._toggleCopilot(!this.state.copilotActive);
|
// onCommandClick: () => {
|
||||||
},
|
// this._toggleCopilot(!this.state.copilotActive);
|
||||||
commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
|
// },
|
||||||
ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
|
// commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
|
||||||
hasPopup: false,
|
// ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
|
||||||
};
|
// hasPopup: false,
|
||||||
buttons.push(toggleCopilotButton);
|
// };
|
||||||
}
|
// buttons.push(toggleCopilotButton);
|
||||||
|
// }
|
||||||
|
|
||||||
if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) {
|
if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) {
|
||||||
const label = "Cancel query";
|
const label = "Cancel query";
|
||||||
@@ -725,6 +727,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<CosmosFluentProvider id={this.props.tabId} className={this.props.styles.queryTab} role="tabpanel">
|
<CosmosFluentProvider id={this.props.tabId} className={this.props.styles.queryTab} role="tabpanel">
|
||||||
|
{/*TODO: Uncomment this section when query copilot is reinstated in DE
|
||||||
{this.props.copilotEnabled && this.state.currentTabActive && this.state.copilotActive && (
|
{this.props.copilotEnabled && this.state.currentTabActive && this.state.copilotActive && (
|
||||||
<QueryCopilotPromptbar
|
<QueryCopilotPromptbar
|
||||||
explorer={this.props.collection.container}
|
explorer={this.props.collection.container}
|
||||||
@@ -732,7 +735,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
|
|||||||
databaseId={this.props.collection.databaseId}
|
databaseId={this.props.collection.databaseId}
|
||||||
containerId={this.props.collection.id()}
|
containerId={this.props.collection.id()}
|
||||||
></QueryCopilotPromptbar>
|
></QueryCopilotPromptbar>
|
||||||
)}
|
)} */}
|
||||||
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
|
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
|
||||||
<Allotment
|
<Allotment
|
||||||
key={vertical.toString()}
|
key={vertical.toString()}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFi
|
|||||||
import { getShellNameForDisplay } from "Explorer/Tabs/CloudShellTab/Utils/CommonUtils";
|
import { getShellNameForDisplay } from "Explorer/Tabs/CloudShellTab/Utils/CommonUtils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import FirewallRuleScreenshot from "../../../../images/firewallRule.png";
|
import FirewallRuleScreenshot from "../../../../images/firewallRule.png";
|
||||||
import VcoreFirewallRuleScreenshot from "../../../../images/vcoreMongoFirewallRule.png";
|
|
||||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
@@ -25,15 +24,15 @@ export abstract class BaseTerminalComponentAdapter implements ReactAdapter {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
public renderComponent(): JSX.Element {
|
||||||
|
if (this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo) {
|
||||||
|
return this.renderTerminalComponent();
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.isAllPublicIPAddressesEnabled()) {
|
if (!this.isAllPublicIPAddressesEnabled()) {
|
||||||
return (
|
return (
|
||||||
<QuickstartFirewallNotification
|
<QuickstartFirewallNotification
|
||||||
messageType={this.getMessageType()}
|
messageType={this.getMessageType()}
|
||||||
screenshot={
|
screenshot={FirewallRuleScreenshot}
|
||||||
this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo
|
|
||||||
? VcoreFirewallRuleScreenshot
|
|
||||||
: FirewallRuleScreenshot
|
|
||||||
}
|
|
||||||
shellName={getShellNameForDisplay(this.kind)}
|
shellName={getShellNameForDisplay(this.kind)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
|
||||||
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
|
||||||
|
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
@@ -479,9 +480,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
node: this,
|
node: this,
|
||||||
title: title,
|
title: title,
|
||||||
tabPath: "",
|
tabPath: "",
|
||||||
|
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
|
||||||
collection: this,
|
collection: this,
|
||||||
masterKey: userContext.masterKey || "",
|
|
||||||
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
|
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
|
||||||
collectionId: this.id(),
|
collectionId: this.id(),
|
||||||
databaseId: this.databaseId,
|
databaseId: this.databaseId,
|
||||||
@@ -737,7 +737,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
title: title,
|
title: title,
|
||||||
tabPath: "",
|
tabPath: "",
|
||||||
collection: this,
|
collection: this,
|
||||||
masterKey: userContext.masterKey || "",
|
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
|
||||||
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
|
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
|
||||||
collectionId: this.id(),
|
collectionId: this.id(),
|
||||||
databaseId: this.databaseId,
|
databaseId: this.databaseId,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { useQueryCopilot } from "hooks/useQueryCopilot";
|
|||||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import shallow from "zustand/shallow";
|
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
|
|
||||||
@@ -38,13 +37,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
|
|||||||
const [openItems, setOpenItems] = React.useState<TreeItemValue[]>([]);
|
const [openItems, setOpenItems] = React.useState<TreeItemValue[]>([]);
|
||||||
const treeStyles = useTreeStyles();
|
const treeStyles = useTreeStyles();
|
||||||
|
|
||||||
const { isNotebookEnabled } = useNotebook(
|
const isNotebookEnabled = useNotebook((state) => state.isNotebookEnabled)
|
||||||
(state) => ({
|
|
||||||
isNotebookEnabled: state.isNotebookEnabled,
|
|
||||||
}),
|
|
||||||
shallow,
|
|
||||||
);
|
|
||||||
|
|
||||||
// We intentionally avoid using a state selector here because we want to re-render the tree if the active tab changes.
|
// We intentionally avoid using a state selector here because we want to re-render the tree if the active tab changes.
|
||||||
const { refreshActiveTab } = useTabs();
|
const { refreshActiveTab } = useTabs();
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import PublishIcon from "../../../images/notebook/publish_content.svg";
|
|||||||
import RefreshIcon from "../../../images/refresh-cosmos.svg";
|
import RefreshIcon from "../../../images/refresh-cosmos.svg";
|
||||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||||
@@ -58,12 +57,12 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
|
|
||||||
useSelectedNode.subscribe(() => this.triggerRender());
|
useSelectedNode.subscribe(() => this.triggerRender());
|
||||||
useTabs.subscribe(
|
useTabs.subscribe(
|
||||||
() => this.triggerRender(),
|
|
||||||
(state) => state.activeTab,
|
(state) => state.activeTab,
|
||||||
|
() => this.triggerRender(),
|
||||||
);
|
);
|
||||||
useNotebook.subscribe(
|
useNotebook.subscribe(
|
||||||
() => this.triggerRender(),
|
|
||||||
(state) => state.isNotebookEnabled,
|
(state) => state.isNotebookEnabled,
|
||||||
|
() => this.triggerRender(),
|
||||||
);
|
);
|
||||||
|
|
||||||
useDatabases.subscribe(() => this.triggerRender());
|
useDatabases.subscribe(() => this.triggerRender());
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -23,6 +28,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -45,6 +55,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -65,6 +80,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -123,6 +143,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -138,6 +163,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -187,6 +217,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
"children": [
|
"children": [
|
||||||
{
|
{
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -257,6 +292,11 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
|
|||||||
],
|
],
|
||||||
"className": "collectionNode",
|
"className": "collectionNode",
|
||||||
"contextMenu": [
|
"contextMenu": [
|
||||||
|
{
|
||||||
|
"iconSrc": {},
|
||||||
|
"label": "Open Cassandra Shell",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "Delete Table",
|
"label": "Delete Table",
|
||||||
@@ -323,7 +363,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -354,7 +394,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -386,7 +426,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -422,7 +462,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -490,7 +530,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -521,7 +561,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -580,7 +620,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -666,7 +706,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"iconSrc": {},
|
"iconSrc": {},
|
||||||
"label": "New Shell",
|
"label": "Open Mongo Shell",
|
||||||
"onClick": [Function],
|
"onClick": [Function],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
import create, { UseStore } from "zustand";
|
import { create } from "zustand";
|
||||||
|
import { subscribeWithSelector } from "zustand/middleware";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
@@ -26,143 +27,147 @@ interface DatabasesState {
|
|||||||
validateCollectionId: (databaseId: string, collectionId: string) => Promise<boolean>;
|
validateCollectionId: (databaseId: string, collectionId: string) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
|
export const useDatabases = create<DatabasesState>()(
|
||||||
databases: [],
|
subscribeWithSelector(
|
||||||
resourceTokenCollection: undefined,
|
(set, get) => ({
|
||||||
sampleDataResourceTokenCollection: undefined,
|
databases: [] as ViewModels.Database[],
|
||||||
updateDatabase: (updatedDatabase: ViewModels.Database) =>
|
resourceTokenCollection: undefined as ViewModels.CollectionBase,
|
||||||
set((state) => {
|
sampleDataResourceTokenCollection: undefined as ViewModels.CollectionBase,
|
||||||
const updatedDatabases = state.databases.map((database: ViewModels.Database) => {
|
updateDatabase: (updatedDatabase: ViewModels.Database) =>
|
||||||
if (database?.id() === updatedDatabase?.id()) {
|
set((state) => {
|
||||||
return updatedDatabase;
|
const updatedDatabases = state.databases.map((database: ViewModels.Database) => {
|
||||||
|
if (database?.id() === updatedDatabase?.id()) {
|
||||||
|
return updatedDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
return database;
|
||||||
|
});
|
||||||
|
return { databases: updatedDatabases };
|
||||||
|
}),
|
||||||
|
addDatabases: (databases: ViewModels.Database[]) =>
|
||||||
|
set((state) => ({
|
||||||
|
databases: [...state.databases, ...databases].sort((db1, db2) => db1.id().localeCompare(db2.id())),
|
||||||
|
})),
|
||||||
|
deleteDatabase: (database: ViewModels.Database) =>
|
||||||
|
set((state) => ({ databases: state.databases.filter((db) => database.id() !== db.id()) })),
|
||||||
|
clearDatabases: () => set(() => ({ databases: [] })),
|
||||||
|
isSaveQueryEnabled: () => {
|
||||||
|
const savedQueriesDatabase: ViewModels.Database = _.find(
|
||||||
|
get().databases,
|
||||||
|
(database: ViewModels.Database) => database.id() === Constants.SavedQueries.DatabaseName,
|
||||||
|
);
|
||||||
|
if (!savedQueriesDatabase) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const savedQueriesCollection: ViewModels.Collection =
|
||||||
|
savedQueriesDatabase &&
|
||||||
|
_.find(
|
||||||
|
savedQueriesDatabase.collections(),
|
||||||
|
(collection: ViewModels.Collection) => collection.id() === Constants.SavedQueries.CollectionName,
|
||||||
|
);
|
||||||
|
if (!savedQueriesCollection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => {
|
||||||
|
return isSampleDatabase === undefined
|
||||||
|
? get().databases.find((db) => databaseId === db.id())
|
||||||
|
: get().databases.find((db) => databaseId === db.id() && db.isSampleDB === isSampleDatabase);
|
||||||
|
},
|
||||||
|
isLastNonEmptyDatabase: () => {
|
||||||
|
const databases = get().databases;
|
||||||
|
return databases.length === 1 && (databases[0].collections()?.length > 0 || !!databases[0].offer());
|
||||||
|
},
|
||||||
|
findCollection: (databaseId: string, collectionId: string) => {
|
||||||
|
const database = get().findDatabaseWithId(databaseId);
|
||||||
|
return database?.collections()?.find((collection) => collection.id() === collectionId);
|
||||||
|
},
|
||||||
|
isLastCollection: () => {
|
||||||
|
const databases = get().databases;
|
||||||
|
if (databases.length === 0) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return database;
|
let collectionCount = 0;
|
||||||
});
|
for (let i = 0; i < databases.length; i++) {
|
||||||
return { databases: updatedDatabases };
|
const database = databases[i];
|
||||||
}),
|
collectionCount += database.collections().length;
|
||||||
addDatabases: (databases: ViewModels.Database[]) =>
|
if (collectionCount > 1) {
|
||||||
set((state) => ({
|
return false;
|
||||||
databases: [...state.databases, ...databases].sort((db1, db2) => db1.id().localeCompare(db2.id())),
|
}
|
||||||
})),
|
}
|
||||||
deleteDatabase: (database: ViewModels.Database) =>
|
|
||||||
set((state) => ({ databases: state.databases.filter((db) => database.id() !== db.id()) })),
|
|
||||||
clearDatabases: () => set(() => ({ databases: [] })),
|
|
||||||
isSaveQueryEnabled: () => {
|
|
||||||
const savedQueriesDatabase: ViewModels.Database = _.find(
|
|
||||||
get().databases,
|
|
||||||
(database: ViewModels.Database) => database.id() === Constants.SavedQueries.DatabaseName,
|
|
||||||
);
|
|
||||||
if (!savedQueriesDatabase) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const savedQueriesCollection: ViewModels.Collection =
|
|
||||||
savedQueriesDatabase &&
|
|
||||||
_.find(
|
|
||||||
savedQueriesDatabase.collections(),
|
|
||||||
(collection: ViewModels.Collection) => collection.id() === Constants.SavedQueries.CollectionName,
|
|
||||||
);
|
|
||||||
if (!savedQueriesCollection) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
findDatabaseWithId: (databaseId: string, isSampleDatabase?: boolean) => {
|
|
||||||
return isSampleDatabase === undefined
|
|
||||||
? get().databases.find((db) => databaseId === db.id())
|
|
||||||
: get().databases.find((db) => databaseId === db.id() && db.isSampleDB === isSampleDatabase);
|
|
||||||
},
|
|
||||||
isLastNonEmptyDatabase: () => {
|
|
||||||
const databases = get().databases;
|
|
||||||
return databases.length === 1 && (databases[0].collections()?.length > 0 || !!databases[0].offer());
|
|
||||||
},
|
|
||||||
findCollection: (databaseId: string, collectionId: string) => {
|
|
||||||
const database = get().findDatabaseWithId(databaseId);
|
|
||||||
return database?.collections()?.find((collection) => collection.id() === collectionId);
|
|
||||||
},
|
|
||||||
isLastCollection: () => {
|
|
||||||
const databases = get().databases;
|
|
||||||
if (databases.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let collectionCount = 0;
|
return true;
|
||||||
for (let i = 0; i < databases.length; i++) {
|
},
|
||||||
const database = databases[i];
|
loadDatabaseOffers: async () => {
|
||||||
collectionCount += database.collections().length;
|
|
||||||
if (collectionCount > 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
loadDatabaseOffers: async () => {
|
|
||||||
await Promise.all(
|
|
||||||
get().databases?.map(async (database: ViewModels.Database) => {
|
|
||||||
await database.loadOffer();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
loadAllOffers: async () => {
|
|
||||||
await Promise.all(
|
|
||||||
get().databases?.map(async (database: ViewModels.Database) => {
|
|
||||||
await database.loadOffer();
|
|
||||||
await database.loadCollections();
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
(database.collections() || []).map(async (collection: ViewModels.Collection) => {
|
get().databases?.map(async (database: ViewModels.Database) => {
|
||||||
await collection.loadOffer();
|
await database.loadOffer();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}),
|
},
|
||||||
);
|
loadAllOffers: async () => {
|
||||||
},
|
await Promise.all(
|
||||||
isFirstResourceCreated: () => {
|
get().databases?.map(async (database: ViewModels.Database) => {
|
||||||
const databases = get().databases;
|
await database.loadOffer();
|
||||||
|
await database.loadCollections();
|
||||||
|
await Promise.all(
|
||||||
|
(database.collections() || []).map(async (collection: ViewModels.Collection) => {
|
||||||
|
await collection.loadOffer();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isFirstResourceCreated: () => {
|
||||||
|
const databases = get().databases;
|
||||||
|
|
||||||
if (!databases || databases.length === 0) {
|
if (!databases || databases.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return databases.some((database) => {
|
return databases.some((database) => {
|
||||||
// user has created at least one collection
|
// user has created at least one collection
|
||||||
if (database.collections()?.length > 0) {
|
if (database.collections()?.length > 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// user has created a database with shared throughput
|
// user has created a database with shared throughput
|
||||||
if (database.offer()) {
|
if (database.offer()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// use has created an empty database without shared throughput
|
// use has created an empty database without shared throughput
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
findSelectedDatabase: (): ViewModels.Database => {
|
findSelectedDatabase: (): ViewModels.Database => {
|
||||||
const selectedNode = useSelectedNode.getState().selectedNode;
|
const selectedNode = useSelectedNode.getState().selectedNode;
|
||||||
if (!selectedNode) {
|
if (!selectedNode) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (selectedNode.nodeKind === "Database") {
|
if (selectedNode.nodeKind === "Database") {
|
||||||
return _.find(get().databases, (database: ViewModels.Database) => database.id() === selectedNode.id());
|
return _.find(get().databases, (database: ViewModels.Database) => database.id() === selectedNode.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedNode.nodeKind === "Collection") {
|
if (selectedNode.nodeKind === "Collection") {
|
||||||
return selectedNode.database;
|
return selectedNode.database;
|
||||||
}
|
}
|
||||||
|
|
||||||
return selectedNode.collection?.database;
|
return selectedNode.collection?.database;
|
||||||
},
|
},
|
||||||
validateDatabaseId: (id: string): boolean => {
|
validateDatabaseId: (id: string): boolean => {
|
||||||
return !get().databases.some((database) => database.id() === id);
|
return !get().databases.some((database) => database.id() === id);
|
||||||
},
|
},
|
||||||
validateCollectionId: async (databaseId: string, collectionId: string): Promise<boolean> => {
|
validateCollectionId: async (databaseId: string, collectionId: string): Promise<boolean> => {
|
||||||
const database = get().databases.find((db) => db.id() === databaseId);
|
const database = get().databases.find((db) => db.id() === databaseId);
|
||||||
// For a new tables account, database is undefined when creating the first table
|
// For a new tables account, database is undefined when creating the first table
|
||||||
if (!database && userContext.apiType === "Tables") {
|
if (!database && userContext.apiType === "Tables") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await database.loadCollections();
|
await database.loadCollections();
|
||||||
return !database.collections().some((collection) => collection.id() === collectionId);
|
return !database.collections().some((collection) => collection.id() === collectionId);
|
||||||
},
|
},
|
||||||
}));
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ConnectionStatusType, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
import { ConnectionStatusType, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||||
import create, { UseStore } from "zustand";
|
import { create } from "zustand";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { useTabs } from "../hooks/useTabs";
|
import { useTabs } from "../hooks/useTabs";
|
||||||
export interface SelectedNodeState {
|
export interface SelectedNodeState {
|
||||||
@@ -17,7 +17,7 @@ export interface SelectedNodeState {
|
|||||||
isQueryCopilotCollectionSelected: () => boolean;
|
isQueryCopilotCollectionSelected: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) => ({
|
export const useSelectedNode = create<SelectedNodeState>((set, get) => ({
|
||||||
selectedNode: undefined,
|
selectedNode: undefined,
|
||||||
setSelectedNode: (node: ViewModels.TreeNode) => set({ selectedNode: node }),
|
setSelectedNode: (node: ViewModels.TreeNode) => set({ selectedNode: node }),
|
||||||
isDatabaseNodeOrNoneSelected: (): boolean => {
|
isDatabaseNodeOrNoneSelected: (): boolean => {
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ const App: React.FunctionComponent = () => {
|
|||||||
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
|
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant, authFailure } =
|
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant, authFailure } =
|
||||||
useAADAuth();
|
useAADAuth(config);
|
||||||
|
|
||||||
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
|
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
|
||||||
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
|
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
|
||||||
const [connectionString, setConnectionString] = React.useState<string>();
|
const [connectionString, setConnectionString] = React.useState<string>();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { PropsWithChildren, useEffect } from "react";
|
import { PropsWithChildren, useEffect } from "react";
|
||||||
import { KeyBindingMap, tinykeys } from "tinykeys";
|
import { KeyBindingMap, tinykeys } from "tinykeys";
|
||||||
import create, { UseStore } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a keyboard shortcut handler.
|
* Represents a keyboard shortcut handler.
|
||||||
@@ -126,7 +126,7 @@ export const clearKeyboardActionGroup = (group: KeyboardActionGroup) => {
|
|||||||
useKeyboardActionHandlers.getState().setHandlers(group, {});
|
useKeyboardActionHandlers.getState().setHandlers(group, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
const useKeyboardActionHandlers: UseStore<KeyboardShortcutState> = create((set, get) => ({
|
const useKeyboardActionHandlers = create<KeyboardShortcutState>((set, get) => ({
|
||||||
allHandlers: {},
|
allHandlers: {},
|
||||||
groups: {},
|
groups: {},
|
||||||
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => {
|
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => {
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ export class PhoenixClient {
|
|||||||
|
|
||||||
private getPhoenixControlPlanePathPrefix(): string {
|
private getPhoenixControlPlanePathPrefix(): string {
|
||||||
if (!this.armResourceId) {
|
if (!this.armResourceId) {
|
||||||
throw new Error("The Phoenix client was not initialized properly: missing ARM resourcce id");
|
throw new Error("The Phoenix client was not initialized properly: missing ARM resource id");
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolsEndpoint =
|
const toolsEndpoint =
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
|||||||
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
|
||||||
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
|
||||||
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
|
||||||
enableCloudShell: "true" === get("enablecloudshell"),
|
enableCloudShell: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
|
|||||||
host: configContext.CATALOG_ENDPOINT,
|
host: configContext.CATALOG_ENDPOINT,
|
||||||
path: getOfferingIdPathForRegion(),
|
path: getOfferingIdPathForRegion(),
|
||||||
method: "GET",
|
method: "GET",
|
||||||
apiVersion: "2023-05-01-preview",
|
apiVersion: configContext.CATALOG_API_VERSION,
|
||||||
queryParams: {
|
queryParams: {
|
||||||
filter:
|
filter:
|
||||||
"armRegionName eq '" +
|
"armRegionName eq '" +
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { MongoGuidRepresentation } from "Common/Constants";
|
||||||
import { SplitterDirection } from "Common/Splitter";
|
import { SplitterDirection } from "Common/Splitter";
|
||||||
import * as LocalStorageUtility from "./LocalStorageUtility";
|
import * as LocalStorageUtility from "./LocalStorageUtility";
|
||||||
import * as SessionStorageUtility from "./SessionStorageUtility";
|
import * as SessionStorageUtility from "./SessionStorageUtility";
|
||||||
@@ -33,6 +34,7 @@ export enum StorageKey {
|
|||||||
DocumentsTabPrefs,
|
DocumentsTabPrefs,
|
||||||
DefaultQueryResultsView,
|
DefaultQueryResultsView,
|
||||||
AppState,
|
AppState,
|
||||||
|
MongoGuidRepresentation,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hasRUThresholdBeenConfigured = (): boolean => {
|
export const hasRUThresholdBeenConfigured = (): boolean => {
|
||||||
@@ -65,4 +67,13 @@ export const getDefaultQueryResultsView = (): SplitterDirection => {
|
|||||||
return SplitterDirection.Horizontal;
|
return SplitterDirection.Horizontal;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMongoGuidRepresentation = (): MongoGuidRepresentation => {
|
||||||
|
const mongoGuidRepresentation: string | null = LocalStorageUtility.getEntryString(StorageKey.MongoGuidRepresentation);
|
||||||
|
if (mongoGuidRepresentation) {
|
||||||
|
return mongoGuidRepresentation as MongoGuidRepresentation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MongoGuidRepresentation.CSharpLegacy;
|
||||||
|
};
|
||||||
|
|
||||||
export const DefaultRUThreshold = 5000;
|
export const DefaultRUThreshold = 5000;
|
||||||
|
|||||||
@@ -91,5 +91,11 @@ export const getItemName = (): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isDataplaneRbacSupported = (apiType: string): boolean => {
|
export const isDataplaneRbacSupported = (apiType: string): boolean => {
|
||||||
return apiType === "SQL" || apiType === "Tables";
|
return (
|
||||||
|
apiType === "SQL" || apiType === "Tables" || apiType === "Gremlin" || apiType === "Mongo" || apiType === "Cassandra"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasProxyServer = (apiType: string): boolean => {
|
||||||
|
return apiType === "Mongo" || apiType === "Cassandra";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,51 @@
|
|||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import { updateUserContext } from "../UserContext";
|
import { ApiType, updateUserContext, userContext } from "../UserContext";
|
||||||
import * as AuthorizationUtils from "./AuthorizationUtils";
|
import * as AuthorizationUtils from "./AuthorizationUtils";
|
||||||
jest.mock("../Explorer/Explorer");
|
jest.mock("../Explorer/Explorer");
|
||||||
|
|
||||||
describe("AuthorizationUtils", () => {
|
describe("AuthorizationUtils", () => {
|
||||||
|
const setAadDataPlane = (enabled: boolean) => {
|
||||||
|
updateUserContext({
|
||||||
|
features: {
|
||||||
|
enableAadDataPlane: enabled,
|
||||||
|
canExceedMaximumValue: false,
|
||||||
|
cosmosdb: false,
|
||||||
|
enableChangeFeedPolicy: false,
|
||||||
|
enableFixedCollectionWithSharedThroughput: false,
|
||||||
|
enableKOPanel: false,
|
||||||
|
enableNotebooks: false,
|
||||||
|
enableReactPane: false,
|
||||||
|
enableRightPanelV2: false,
|
||||||
|
enableSchema: false,
|
||||||
|
enableSDKoperations: false,
|
||||||
|
enableSpark: false,
|
||||||
|
enableTtl: false,
|
||||||
|
executeSproc: false,
|
||||||
|
enableResourceGraph: false,
|
||||||
|
enableKoResourceTree: false,
|
||||||
|
enableThroughputBuckets: false,
|
||||||
|
hostedDataExplorer: false,
|
||||||
|
sandboxNotebookOutputs: false,
|
||||||
|
showMinRUSurvey: false,
|
||||||
|
ttl90Days: false,
|
||||||
|
enableThroughputCap: false,
|
||||||
|
enableHierarchicalKeys: false,
|
||||||
|
enableCopilot: false,
|
||||||
|
disableCopilotPhoenixGateaway: false,
|
||||||
|
enableCopilotFullSchema: false,
|
||||||
|
copilotChatFixedMonacoEditorHeight: false,
|
||||||
|
enablePriorityBasedExecution: false,
|
||||||
|
disableConnectionStringLogin: false,
|
||||||
|
enableCloudShell: false,
|
||||||
|
autoscaleDefault: false,
|
||||||
|
partitionKeyDefault: false,
|
||||||
|
partitionKeyDefault2: false,
|
||||||
|
notebooksDownBanner: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
describe("getAuthorizationHeader()", () => {
|
describe("getAuthorizationHeader()", () => {
|
||||||
it("should return authorization header if authentication type is AAD", () => {
|
it("should return authorization header if authentication type is AAD", () => {
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
@@ -54,4 +95,41 @@ describe("AuthorizationUtils", () => {
|
|||||||
).toBeDefined();
|
).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("useDataplaneRbacAuthorization()", () => {
|
||||||
|
it("should return true if enableAadDataPlane feature flag is set", () => {
|
||||||
|
setAadDataPlane(true);
|
||||||
|
expect(AuthorizationUtils.useDataplaneRbacAuthorization(userContext)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true if dataPlaneRbacEnabled is set to true and API supports RBAC", () => {
|
||||||
|
setAadDataPlane(false);
|
||||||
|
["SQL", "Tables", "Gremlin", "Mongo", "Cassandra"].forEach((type) => {
|
||||||
|
updateUserContext({
|
||||||
|
dataPlaneRbacEnabled: true,
|
||||||
|
apiType: type as ApiType,
|
||||||
|
});
|
||||||
|
expect(AuthorizationUtils.useDataplaneRbacAuthorization(userContext)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if dataPlaneRbacEnabled is set to true and API does not support RBAC", () => {
|
||||||
|
setAadDataPlane(false);
|
||||||
|
["Postgres", "VCoreMongo"].forEach((type) => {
|
||||||
|
updateUserContext({
|
||||||
|
dataPlaneRbacEnabled: true,
|
||||||
|
apiType: type as ApiType,
|
||||||
|
});
|
||||||
|
expect(AuthorizationUtils.useDataplaneRbacAuthorization(userContext)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if dataPlaneRbacEnabled is set to false", () => {
|
||||||
|
setAadDataPlane(false);
|
||||||
|
updateUserContext({
|
||||||
|
dataPlaneRbacEnabled: false,
|
||||||
|
});
|
||||||
|
expect(AuthorizationUtils.useDataplaneRbacAuthorization(userContext)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import * as msal from "@azure/msal-browser";
|
import * as msal from "@azure/msal-browser";
|
||||||
|
import { getEnvironmentScopeEndpoint } from "Common/EnvironmentUtility";
|
||||||
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { hasProxyServer, isDataplaneRbacSupported } from "Utils/APITypeUtils";
|
||||||
import { AuthType } from "../AuthType";
|
import { AuthType } from "../AuthType";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
@@ -7,7 +9,7 @@ import { configContext } from "../ConfigContext";
|
|||||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
import { trace, traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
|
import { trace, traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../UserContext";
|
import { UserContext, userContext } from "../UserContext";
|
||||||
|
|
||||||
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
|
||||||
if (userContext.authType === AuthType.EncryptedToken) {
|
if (userContext.authType === AuthType.EncryptedToken) {
|
||||||
@@ -73,10 +75,12 @@ export async function acquireMsalTokenForAccount(
|
|||||||
if (userContext.databaseAccount.properties?.documentEndpoint === undefined) {
|
if (userContext.databaseAccount.properties?.documentEndpoint === undefined) {
|
||||||
throw new Error("Database account has no document endpoint defined");
|
throw new Error("Database account has no document endpoint defined");
|
||||||
}
|
}
|
||||||
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
|
let hrefEndpoint = "";
|
||||||
/\/+$/,
|
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
|
||||||
"/.default",
|
hrefEndpoint = getEnvironmentScopeEndpoint();
|
||||||
);
|
} else {
|
||||||
|
hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(/\/+$/, "/.default");
|
||||||
|
}
|
||||||
const msalInstance = await getMsalInstance();
|
const msalInstance = await getMsalInstance();
|
||||||
const knownAccounts = msalInstance.getAllAccounts();
|
const knownAccounts = msalInstance.getAllAccounts();
|
||||||
// If user_hint is provided, we will try to use it to find the account.
|
// If user_hint is provided, we will try to use it to find the account.
|
||||||
@@ -179,3 +183,14 @@ export async function acquireTokenWithMsal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useDataplaneRbacAuthorization(userContext: UserContext): boolean {
|
||||||
|
return (
|
||||||
|
userContext.features?.enableAadDataPlane ||
|
||||||
|
(userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDataplaneRbacEnabledForProxyApi(userContext: UserContext): boolean {
|
||||||
|
return useDataplaneRbacAuthorization(userContext) && hasProxyServer(userContext.apiType);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const autoPilotThroughput1K = 1000;
|
export const autoPilotThroughput1K = 1000;
|
||||||
export const autoPilotIncrementStep = 1000;
|
export const autoPilotIncrementStep = 1000;
|
||||||
export const autoPilotThroughput4K = 4000;
|
export const autoPilotThroughput4K = 4000;
|
||||||
|
export const autoPilotThroughput5K = 5000;
|
||||||
export const autoPilotThroughput10K = 10000;
|
export const autoPilotThroughput10K = 10000;
|
||||||
|
|
||||||
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
export function isValidAutoPilotThroughput(maxThroughput: number): boolean {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isFabricNative } from "Platform/Fabric/FabricUtil";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import { userContext } from "../UserContext";
|
import { userContext } from "../UserContext";
|
||||||
|
|
||||||
@@ -18,5 +19,8 @@ export const isServerlessAccount = (): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isVectorSearchEnabled = (): boolean => {
|
export const isVectorSearchEnabled = (): boolean => {
|
||||||
return userContext.apiType === "SQL" && isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch);
|
return (
|
||||||
|
userContext.apiType === "SQL" &&
|
||||||
|
(isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch) || isFabricNative())
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user