Compare commits

..

2 Commits

Author SHA1 Message Date
Archie Agarwal
a96d679087 clean up the code 2025-06-16 15:16:47 +05:30
Archie Agarwal
dc8bb69359 Initial changes for index 2025-06-11 18:54:36 +05:30
145 changed files with 61745 additions and 17742 deletions

View File

@@ -23,6 +23,8 @@ src/Common/MongoUtility.ts
src/Common/NotificationsClientBase.ts
src/Common/QueriesClient.ts
src/Common/Splitter.ts
src/Controls/Heatmap/Heatmap.test.ts
src/Controls/Heatmap/Heatmap.ts
src/Definitions/datatables.d.ts
src/Definitions/gif.d.ts
src/Definitions/globals.d.ts

View File

@@ -177,27 +177,9 @@ jobs:
- name: "Az CLI login"
uses: Azure/login@v2
with:
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# We can't use MSAL within playwright so we acquire tokens prior to running the tests
- name: "Acquire RBAC tokens for test accounts"
uses: azure/cli@v2
with:
azcliversion: latest
inlineScript: |
NOSQL_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_TOKEN"
echo NOSQL_TESTACCOUNT_TOKEN=$NOSQL_TESTACCOUNT_TOKEN >> $GITHUB_ENV
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
- name: Upload blob report to GitHub Actions Artifacts

View File

@@ -27,7 +27,7 @@ jobs:
- name: "Az CLI login"
uses: azure/login@v1
with:
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

2
.npmrc
View File

@@ -1,4 +1,4 @@
save-exact=true
# Ignore peer dependency conflicts
force=true # TODO: Remove this when we update to React 17 or higher!
force=true # TODO: Remove this when we update to React 17 or higher!

20
.vscode/settings.json vendored
View File

@@ -1,6 +1,22 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
".vs": true,
".vscode/**": true,
"*.trx": true,
"**/.DS_Store": true,
"**/.git": true,
"**/.hg": true,
"**/.svn": true,
"built/**": true,
"coverage/**": true,
"libs/**": true,
"node_modules/**": true,
"package-lock.json": true,
"quickstart/**": true,
"test/out/**": true,
"workers/libs/**": true
},
"typescript.tsdk": "node_modules/typescript/lib",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
@@ -9,7 +25,7 @@
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[typescript]": {
"[typescriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
}

View File

@@ -1,4 +1,5 @@
{
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
"isTerminalEnabled": true,
"isPhoenixEnabled": true
}
}

View File

@@ -1,4 +1,5 @@
{
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
"isPhoenixEnabled": false
}
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
"isTerminalEnabled" : false,
"isPhoenixEnabled" : false
}

37907
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,270 +4,200 @@
"description": "Cosmos Explorer",
"main": "index.js",
"dependencies": {
"@azure/arm-cosmosdb": "16.3.0",
"@azure/cosmos": "4.5.0",
"@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.3.0",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "4.10.1",
"@azure/msal-browser": "4.24.0",
"@babel/plugin-transform-class-properties": "^7.24.7",
"@babel/plugin-proposal-decorators": "7.28.0",
"@fluentui/react": "8.123.6",
"@fluentui/react-components": "9.70.0",
"@jupyterlab/services": "7.4.9",
"@jupyterlab/terminal": "4.4.9",
"@microsoft/applicationinsights-web": "3.3.10",
"@azure/identity": "4.5.0",
"@azure/msal-browser": "2.14.2",
"@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12",
"@fluentui/react": "8.119.0",
"@fluentui/react-components": "9.54.2",
"@jupyterlab/services": "6.0.2",
"@jupyterlab/terminal": "3.0.3",
"@microsoft/applicationinsights-web": "2.6.1",
"@nteract/commutable": "7.5.1",
"@nteract/connected-components": "6.9.0",
"@nteract/connected-components": "6.8.2",
"@nteract/core": "15.1.9",
"@nteract/data-explorer": "8.2.12",
"@nteract/directory-listing": "2.1.0",
"@nteract/dropdown-menu": "1.1.9",
"@nteract/data-explorer": "8.0.3",
"@nteract/directory-listing": "2.0.6",
"@nteract/dropdown-menu": "1.0.1",
"@nteract/editor": "10.1.12",
"@nteract/fixtures": "2.3.19",
"@nteract/fixtures": "2.3.0",
"@nteract/iron-icons": "1.0.0",
"@nteract/jupyter-widgets": "4.1.19",
"@nteract/jupyter-widgets": "2.0.0",
"@nteract/logos": "1.0.0",
"@nteract/markdown": "4.6.2",
"@nteract/markdown": "4.6.0",
"@nteract/monaco-editor": "3.2.2",
"@nteract/octicons": "2.0.0",
"@nteract/outputs": "5.1.14",
"@nteract/presentational-components": "3.4.12",
"@nteract/stateful-components": "1.7.15",
"@nteract/styles": "2.2.11",
"@nteract/transform-geojson": "5.1.13",
"@nteract/outputs": "3.0.9",
"@nteract/presentational-components": "3.0.7",
"@nteract/stateful-components": "1.7.0",
"@nteract/styles": "2.0.2",
"@nteract/transform-geojson": "5.1.8",
"@nteract/transform-model-debug": "5.0.1",
"@nteract/transform-plotly": "7.0.1",
"@nteract/transform-vdom": "4.0.15",
"@nteract/transform-plotly": "6.1.6",
"@nteract/transform-vdom": "4.0.11",
"@nteract/transform-vega": "7.0.6",
"@octokit/rest": "21.1.1",
"@octokit/rest": "17.9.2",
"@phosphor/widgets": "1.9.3",
"@testing-library/jest-dom": "6.8.0",
"@types/lodash": "4.17.20",
"@types/mkdirp": "1.0.2",
"@types/node-fetch": "2.6.13",
"@xmldom/xmldom": "0.9.8",
"@testing-library/jest-dom": "6.4.6",
"@types/lodash": "4.14.171",
"@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7",
"@xmldom/xmldom": "0.7.13",
"@xterm/xterm": "5.5.0",
"@xterm/addon-fit": "0.10.0",
"allotment": "1.20.4",
"applicationinsights": "3.12.0",
"allotment": "1.20.2",
"applicationinsights": "1.8.0",
"bootstrap": "3.4.1",
"canvas": "3.2.0",
"canvas": "2.11.2",
"clean-webpack-plugin": "4.0.0",
"clipboard-copy": "4.0.1",
"copy-webpack-plugin": "13.0.1",
"copy-webpack-plugin": "11.0.0",
"crossroads": "0.12.2",
"css-element-queries": "1.2.3",
"d3": "7.9.0",
"datatables.net-colreorder-dt": "2.1.1",
"datatables.net-dt": "2.3.4",
"date-fns": "4.1.0",
"dayjs": "1.11.18",
"css-element-queries": "1.1.1",
"d3": "7.8.5",
"datatables.net-colreorder-dt": "1.7.0",
"datatables.net-dt": "1.13.8",
"date-fns": "1.29.0",
"dayjs": "1.8.19",
"dom-to-image": "2.6.0",
"dotenv": "17.2.3",
"eslint-plugin-jest": "28.14.0",
"eslint-plugin-react": "7.37.5",
"dotenv": "8.2.0",
"eslint-plugin-jest": "27.4.2",
"eslint-plugin-react": "7.33.2",
"hasher": "1.2.0",
"html2canvas": "1.4.1",
"i18next": "25.5.2",
"i18next-browser-languagedetector": "8.2.0",
"i18next-http-backend": "3.0.2",
"iframe-resizer-react": "5.1.5",
"immer": "10.1.3",
"html2canvas": "1.0.0-rc.5",
"i18next": "23.11.5",
"i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "1.0.23",
"iframe-resizer-react": "1.1.0",
"immer": "9.0.6",
"immutable": "4.0.0-rc.12",
"is-ci": "4.1.0",
"is-ci": "2.0.0",
"jquery": "3.7.1",
"jquery-typeahead": "2.11.1",
"jquery-ui-dist": "1.13.3",
"jquery-ui-dist": "1.13.2",
"knockout": "3.5.1",
"loader-utils": "3.3.1",
"mkdirp": "3.0.1",
"monaco-editor": "0.53.0",
"loader-utils": "2.0.3",
"mkdirp": "1.0.4",
"monaco-editor": "0.44.0",
"ms": "2.1.3",
"p-retry": "6.2.1",
"patch-package": "8.0.1",
"plotly.js-cartesian-dist-min": "3.1.1",
"patch-package": "8.0.0",
"plotly.js-cartesian-dist-min": "1.52.3",
"post-robot": "10.0.42",
"q": "2.0.3",
"react": "18.2.0",
"react-animate-height": "3.2.3",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.2.0",
"q": "1.5.1",
"react": "16.14.0",
"react-animate-height": "2.0.8",
"react-dnd": "14.0.2",
"react-dnd-html5-backend": "14.0.0",
"react-dom": "16.14.0",
"react-hotkeys": "2.0.0",
"react-i18next": "16.0.0",
"react-i18next": "14.1.2",
"react-notification-system": "0.2.17",
"react-redux": "7.2.9",
"react-redux": "7.1.3",
"react-splitter-layout": "4.0.0",
"react-string-format": "1.2.0",
"react-string-format": "1.0.1",
"react-window": "1.8.10",
"react-youtube": "10.1.0",
"reflect-metadata": "0.2.2",
"rx-jupyter": "5.5.21",
"sanitize-html": "2.17.0",
"shell-quote": "1.8.3",
"styled-components": "6.1.19",
"swr": "2.3.6",
"terser-webpack-plugin": "5.3.14",
"tinykeys": "3.0.0",
"underscore": "1.13.7",
"utility-types": "3.11.0",
"zustand": "5.0.8"
"react-youtube": "9.0.1",
"reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12",
"sanitize-html": "2.3.3",
"shell-quote": "1.7.3",
"styled-components": "5.0.1",
"swr": "0.4.0",
"terser-webpack-plugin": "5.3.9",
"tinykeys": "2.1.0",
"underscore": "1.12.1",
"utility-types": "3.10.0",
"zustand": "3.5.0"
},
"devDependencies": {
"@babel/core": "7.28.4",
"@babel/preset-env": "7.28.3",
"@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "7.27.1",
"@playwright/test": "1.55.1",
"@testing-library/react": "16.3.0",
"@types/applicationinsights-js": "1.0.9",
"@types/codemirror": "5.60.16",
"@types/crossroads": "0.0.33",
"@types/d3": "7.4.3",
"@babel/core": "7.24.7",
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "7.24.7",
"@babel/preset-typescript": "7.24.7",
"@playwright/test": "1.49.1",
"@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56",
"@types/crossroads": "0.0.30",
"@types/d3": "5.9.2",
"@types/datatables.net": "1.10.28",
"@types/datatables.net-colreorder": "1.4.5",
"@types/dom-to-image": "2.6.7",
"@types/enzyme": "3.10.19",
"@types/dom-to-image": "2.6.2",
"@types/enzyme": "3.10.12",
"@types/enzyme-adapter-react-16": "1.0.9",
"@types/hasher": "0.0.35",
"@types/jest": "30.0.0",
"@types/jquery": "3.5.33",
"@types/node": "24.6.0",
"@types/post-robot": "10.0.6",
"@types/q": "1.5.8",
"@types/react": "18.3.7",
"@types/react-dom": "18.3.7",
"@types/react-notification-system": "0.2.46",
"@types/react-redux": "7.1.34",
"@types/react-splitter-layout": "4.0.0",
"@types/hasher": "0.0.31",
"@types/jest": "29.5.12",
"@types/jquery": "3.5.29",
"@types/node": "12.11.1",
"@types/post-robot": "10.0.1",
"@types/q": "1.5.1",
"@types/react": "17.0.44",
"@types/react-dom": "17.0.15",
"@types/react-notification-system": "0.2.39",
"@types/react-redux": "7.1.7",
"@types/react-splitter-layout": "3.0.1",
"@types/react-window": "1.8.8",
"@types/sanitize-html": "2.16.0",
"@types/sinon": "17.0.4",
"@types/styled-components": "5.1.34",
"@types/underscore": "1.13.0",
"@types/youtube-player": "5.5.11",
"@typescript-eslint/eslint-plugin": "8.45.0",
"@typescript-eslint/parser": "8.45.0",
"@webpack-cli/serve": "3.0.1",
"babel-jest": "30.2.0",
"babel-loader": "10.0.0",
"buffer": "6.0.3",
"@types/sanitize-html": "1.27.2",
"@types/sinon": "2.3.3",
"@types/styled-components": "5.1.1",
"@types/underscore": "1.7.36",
"@types/youtube-player": "5.5.6",
"@typescript-eslint/eslint-plugin": "6.7.4",
"@typescript-eslint/parser": "6.7.4",
"@webpack-cli/serve": "2.0.5",
"babel-jest": "29.7.0",
"babel-loader": "8.1.0",
"buffer": "5.1.0",
"case-sensitive-paths-webpack-plugin": "2.4.0",
"create-file-webpack": "1.0.2",
"css-loader": "7.1.2",
"css-loader": "6.8.1",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.8",
"enzyme-to-json": "3.6.2",
"eslint": "9.36.0",
"eslint": "8.50.0",
"eslint-cli": "1.1.1",
"eslint-plugin-no-null": "1.0.2",
"eslint-plugin-prefer-arrow": "1.2.3",
"eslint-plugin-react-hooks": "5.2.0",
"fast-glob": "3.3.3",
"fs-extra": "11.3.2",
"eslint-plugin-react-hooks": "4.6.0",
"fast-glob": "3.2.5",
"fs-extra": "7.0.0",
"html-inline-css-webpack-plugin": "1.11.2",
"html-loader": "5.1.0",
"html-webpack-plugin": "5.6.4",
"jest": "30.2.0",
"html-loader": "5.0.0",
"html-webpack-plugin": "5.5.3",
"jest": "29.7.0",
"jest-canvas-mock": "2.5.2",
"jest-circus": "30.2.0",
"jest-environment-jsdom": "30.2.0",
"jest-circus": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-html-loader": "1.0.0",
"jest-react-hooks-shallow": "1.5.1",
"jest-trx-results-processor": "3.0.2",
"less": "4.4.1",
"less-loader": "12.3.0",
"less": "3.8.1",
"less-loader": "11.1.3",
"less-vars-loader": "1.1.0",
"mini-css-extract-plugin": "2.9.4",
"mini-css-extract-plugin": "2.1.0",
"monaco-editor-webpack-plugin": "7.1.0",
"node-fetch": "3.3.2",
"prettier": "3.6.2",
"node-fetch": "2.6.7",
"prettier": "3.0.3",
"process": "0.11.10",
"querystring-es3": "0.2.1",
"raw-loader": "4.0.2",
"raw-loader": "0.5.1",
"react-dev-utils": "12.0.1",
"rimraf": "5.0.10",
"sinon": "21.0.0",
"style-loader": "4.0.0",
"ts-loader": "9.5.4",
"typedoc": "0.28.13",
"typescript": "5.9.2",
"rimraf": "3.0.0",
"sinon": "3.2.1",
"style-loader": "0.23.0",
"ts-loader": "9.2.4",
"typedoc": "0.26.2",
"typescript": "4.9.5",
"url-loader": "4.1.1",
"wait-on": "8.0.5",
"webpack": "5.102.0",
"webpack-bundle-analyzer": "4.10.2",
"webpack-cli": "6.0.1",
"webpack-dev-server": "5.2.2"
},
"overrides": {
"@nteract/connected-components": {
"react": "18.2.0",
"react-dom": "18.2.0"
},
"@nteract/core": {
"react": "18.2.0",
"react-dom": "18.2.0"
},
"@nteract/data-explorer": {
"react": "18.2.0",
"react-dom": "18.2.0"
},
"@nteract/dropdown-menu": {
"react": "18.2.0",
"react-dom": "18.2.0"
},
"@nteract/editor": {
"react": "18.2.0",
"react-dom": "18.2.0"
},
"@nteract/iron-icons": {
"react": "18.2.0"
},
"@nteract/jupyter-widgets": {
"react": "18.2.0",
"react-dom": "18.2.0"
},
"@nteract/logos": {
"react": "18.2.0",
"react-dom": "18.2.0"
},
"@nteract/markdown": {
"react": "18.2.0",
"react-dom": "18.2.0"
},
"@nteract/monaco-editor": {
"react": "18.2.0",
"react-dom": "18.2.0"
},
"@nteract/octicons": {
"react": "18.2.0"
},
"@nteract/outputs": {
"react": "18.2.0",
"react-dom": "18.2.0"
},
"@nteract/presentational-components": {
"react": "18.2.0"
},
"@nteract/stateful-components": {
"react": "18.2.0",
"react-dom": "18.2.0"
},
"@nteract/transform-geojson": {
"react": "18.2.0"
},
"@nteract/transform-model-debug": {
"react": "18.2.0"
},
"@nteract/transform-plotly": {
"react": "18.2.0"
},
"@nteract/transform-vdom": {
"react": "18.2.0"
},
"@nteract/transform-vega": {
"react": "18.2.0"
}
"wait-on": "4.0.2",
"webpack": "5.88.2",
"webpack-bundle-analyzer": "4.9.1",
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.2"
},
"scripts": {
"postinstall": "patch-package",

View File

@@ -6,9 +6,9 @@ index e5dc283..1930c2b 100644
/// <reference types="jquery" />
-import DataTables, {Api, ColumnSelector} from 'datatables.net';
+import DataTables, { Api, ColumnSelector } from 'datatables.net';
-import DataTables, {Api} from 'datatables.net';
+import DataTables, { Api } from 'datatables.net';
export default DataTables;
@@ -40,6 +40,8 @@ declare module 'datatables.net' {
@@ -17,6 +17,6 @@ index e5dc283..1930c2b 100644
*/
+ // Ignore this error: error TS7013: Construct signature, which lacks return-type annotation, implicitly has an 'any' return type.
+ // @ts-ignore
new (dt: Api<any>, settings: boolean | ConfigColReorder): DataTablesStatic['ColReorder'];
new (dt: Api<any>, settings: boolean | ConfigColReorder);
/**

View File

@@ -1,6 +1,6 @@
[defaults]
group = dataexplorer-preview
sku = P1v2
sku = P1V2
appserviceplan = dataexplorer-preview
location = westus2
web = dataexplorer-preview

36281
preview/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,18 +4,16 @@
"description": "",
"main": "index.js",
"scripts": {
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:20-lts\" --sku P1V2",
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:18-lts\" --sku P1V2",
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Microsoft Corporation",
"dependencies": {
"body-parser": "^1.20.3",
"express": "^4.21.2",
"express": "^4.17.1",
"http-proxy-middleware": "^3.0.3",
"node": "^20.19.5",
"node-fetch": "^2.6.1",
"path-to-regexp": "^0.1.12"
"node": "^18.20.6",
"node-fetch": "^2.6.1"
}
}

View File

@@ -138,12 +138,13 @@ export enum MongoBackendEndpointType {
remote,
}
export class AadScopeEndpoints {
public static readonly Development: string = "https://cosmos.azure.com";
public static readonly MPAC: string = "https://cosmos.azure.com";
public static readonly Prod: string = "https://cosmos.azure.com";
public static readonly Fairfax: string = "https://cosmos.azure.us";
public static readonly Mooncake: string = "https://cosmos.azure.cn";
export class BackendApi {
public static readonly GenerateToken: string = "GenerateToken";
public static readonly PortalSettings: string = "PortalSettings";
public static readonly AccountRestrictions: string = "AccountRestrictions";
public static readonly RuntimeProxy: string = "RuntimeProxy";
public static readonly DisallowedLocations: string = "DisallowedLocations";
public static readonly SampleData: string = "SampleData";
}
export class PortalBackendEndpoints {
@@ -263,7 +264,6 @@ export class HttpHeaders {
public static activityId: string = "x-ms-activity-id";
public static apiType: string = "x-ms-cosmos-apitype";
public static authorization: string = "authorization";
public static entraIdToken: string = "x-ms-entraid-token";
public static collectionIndexTransformationProgress: string =
"x-ms-documentdb-collection-index-transformation-progress";
public static continuation: string = "x-ms-continuation";
@@ -774,10 +774,3 @@ export const ShortenedQueryCopilotSampleContainerSchema = {
userPrompt: "find all products",
};
export enum MongoGuidRepresentation {
Standard = "Standard",
CSharpLegacy = "CSharpLegacy",
JavaLegacy = "JavaLegacy",
PythonLegacy = "PythonLegacy",
}

View File

@@ -4,12 +4,12 @@ import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
import { AuthType } from "../AuthType";
import { PriorityLevel } from "../Common/Constants";
import * as Logger from "../Common/Logger";
import { Platform, configContext } from "../ConfigContext";
import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
@@ -20,7 +20,8 @@ const _global = typeof self === "undefined" ? window : self;
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
const { verb, resourceId, resourceType, headers } = requestInfo;
if (useDataplaneRbacAuthorization(userContext)) {
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
Logger.logInfo(
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
"Explorer/tokenProvider",

View File

@@ -28,39 +28,3 @@ describe("Environment Utility Test", () => {
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Development);
});
});
describe("normalizeArmEndpoint", () => {
it("should append '/' if not present", () => {
expect(EnvironmentUtility.normalizeArmEndpoint("https://example.com")).toBe("https://example.com/");
});
it("should return the same uri if '/' is present at the end", () => {
expect(EnvironmentUtility.normalizeArmEndpoint("https://example.com/")).toBe("https://example.com/");
});
it("should handle empty string", () => {
expect(EnvironmentUtility.normalizeArmEndpoint("")).toBe("");
});
});
describe("getEnvironment", () => {
it("should return Prod environment", () => {
updateConfigContext({
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
});
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Prod);
});
it("should return Fairfax environment", () => {
updateConfigContext({
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Fairfax,
});
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Fairfax);
});
it("should return Mooncake environment", () => {
updateConfigContext({
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mooncake,
});
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Mooncake);
});
});

View File

@@ -1,5 +1,4 @@
import { AadScopeEndpoints, PortalBackendEndpoints } from "Common/Constants";
import * as Logger from "Common/Logger";
import { PortalBackendEndpoints } from "Common/Constants";
import { configContext } from "ConfigContext";
export function normalizeArmEndpoint(uri: string): string {
@@ -28,17 +27,3 @@ export const getEnvironment = (): Environment => {
return environmentMap[configContext.PORTAL_BACKEND_ENDPOINT];
};
export const getEnvironmentScopeEndpoint = (): string => {
const environment = getEnvironment();
const endpoint = AadScopeEndpoints[environment];
if (!endpoint) {
throw new Error("Cannot determine AAD scope endpoint");
}
const hrefEndpoint = new URL(endpoint).href.replace(/\/+$/, "/.default");
Logger.logInfo(
`Using AAD scope endpoint: ${hrefEndpoint}, Environment: ${environment}`,
"EnvironmentUtility/getEnvironmentScopeEndpoint",
);
return hrefEndpoint;
};

View File

@@ -65,6 +65,7 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
@@ -83,6 +84,7 @@ describe("MongoProxyClient", () => {
it("builds the correct proxy URL in development", () => {
updateConfigContext({
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
queryDocuments(databaseId, collection, true, "{}");
expect(window.fetch).toHaveBeenCalledWith(
@@ -99,6 +101,7 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
@@ -117,6 +120,7 @@ describe("MongoProxyClient", () => {
it("builds the correct proxy URL in development", () => {
updateConfigContext({
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
readDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith(
@@ -133,6 +137,7 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
@@ -151,6 +156,7 @@ describe("MongoProxyClient", () => {
it("builds the correct proxy URL in development", () => {
updateConfigContext({
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
readDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith(
@@ -167,6 +173,7 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
@@ -190,6 +197,7 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
@@ -208,6 +216,7 @@ describe("MongoProxyClient", () => {
it("builds the correct proxy URL in development", () => {
updateConfigContext({
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
deleteDocuments(databaseId, collection, [documentId]);
expect(window.fetch).toHaveBeenCalledWith(
@@ -224,6 +233,7 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
});

View File

@@ -1,5 +1,4 @@
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import { getMongoGuidRepresentation } from "Shared/StorageUtility";
import { AuthType } from "../AuthType";
import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
@@ -7,7 +6,6 @@ import { MessageTypes } from "../Contracts/ExplorerContracts";
import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId";
import { userContext } from "../UserContext";
import { isDataplaneRbacEnabledForProxyApi } from "../Utils/AuthorizationUtils";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
import { MinimalQueryIterator } from "./IteratorUtilities";
@@ -23,13 +21,7 @@ function authHeaders() {
if (userContext.authType === AuthType.EncryptedToken) {
return { [HttpHeaders.guestAccessToken]: userContext.accessToken };
} else {
const headers: { [key: string]: string } = {
[HttpHeaders.authorization]: userContext.authorizationToken,
};
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
headers[HttpHeaders.entraIdToken] = userContext.aadToken;
}
return headers;
return { [HttpHeaders.authorization]: userContext.authorizationToken };
}
}
@@ -147,9 +139,6 @@ export function readDocument(
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
? documentId.partitionKeyProperties?.[0]
: "",
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
};
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
@@ -192,9 +181,6 @@ export function createDocument(
partitionKey:
collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
documentContent: JSON.stringify(documentContent),
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
};
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
@@ -242,9 +228,6 @@ export function updateDocument(
? documentId.partitionKeyProperties?.[0]
: "",
documentContent,
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
};
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
@@ -291,9 +274,6 @@ export function deleteDocuments(
subscriptionID: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
databaseAccountName: databaseAccount.name,
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
};
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);

View File

@@ -2,6 +2,7 @@
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
{
"disableNonStreamingOrderByQuery": true,
"enableQueryControl": false,
"enableScanInQuery": true,
"forceQueryPlan": true,
@@ -13,6 +14,7 @@ exports[`getCommonQueryOptions builds the correct default options objects 1`] =
exports[`getCommonQueryOptions reads from localStorage 1`] = `
{
"disableNonStreamingOrderByQuery": true,
"enableQueryControl": false,
"enableScanInQuery": true,
"forceQueryPlan": true,

View File

@@ -1,4 +1,3 @@
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType";
import { configContext } from "../../ConfigContext";
import { userContext } from "../../UserContext";
@@ -42,7 +41,7 @@ interface MetricsResponse {
}
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
if (userContext.authType !== AuthType.AAD || isFabricNative()) {
if (userContext.authType !== AuthType.AAD) {
return undefined;
}

View File

@@ -1,4 +1,5 @@
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Queries } from "../Constants";
import { client } from "../CosmosClient";
@@ -27,5 +28,6 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
Queries.itemsPerPage;
options.enableQueryControl = LocalStorageUtility.getEntryBoolean(StorageKey.QueryControlEnabled);
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
return options;
};

View File

@@ -126,5 +126,12 @@ async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Co
throw new Error(`Unsupported default experience type: ${apiType}`);
}
return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection);
// TO DO: Remove when we get RP API Spec with materializedViews
/* eslint-disable @typescript-eslint/no-explicit-any */
return rpResponse?.value?.map((collection: any) => {
const collectionDataModel: DataModels.Collection = collection.properties?.resource as DataModels.Collection;
collectionDataModel.materializedViews = collection.properties?.resource?.materializedViews;
collectionDataModel.materializedViewDefinition = collection.properties?.resource?.materializedViewDefinition;
return collectionDataModel;
});
}

View File

@@ -1,15 +1,21 @@
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
import {
BackendApi,
CassandraProxyEndpoints,
JunoEndpoints,
MongoProxyEndpoints,
PortalBackendEndpoints,
} from "Common/Constants";
import {
allowedAadEndpoints,
allowedArcadiaEndpoints,
allowedEmulatorEndpoints,
allowedGraphEndpoints,
allowedHostedExplorerEndpoints,
allowedJunoOrigins,
allowedMsalRedirectEndpoints,
defaultAllowedAadEndpoints,
defaultAllowedArmEndpoints,
defaultAllowedBackendEndpoints,
defaultAllowedCassandraProxyEndpoints,
defaultAllowedGraphEndpoints,
defaultAllowedMongoProxyEndpoints,
validateEndpoint,
} from "Utils/EndpointUtils";
@@ -23,8 +29,6 @@ export enum Platform {
export interface ConfigContext {
platform: Platform;
allowedAadEndpoints: ReadonlyArray<string>;
allowedGraphEndpoints: ReadonlyArray<string>;
allowedArmEndpoints: ReadonlyArray<string>;
allowedBackendEndpoints: ReadonlyArray<string>;
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
@@ -33,8 +37,10 @@ export interface ConfigContext {
gitSha?: string;
proxyPath?: string;
AAD_ENDPOINT: string;
ARM_AUTH_AREA: string;
ARM_ENDPOINT: string;
EMULATOR_ENDPOINT?: string;
ARM_API_VERSION: string;
GRAPH_ENDPOINT: string;
GRAPH_API_VERSION: string;
// This is the endpoint to get offering Ids to be used to fetch prices. Refer to this doc: https://learn.microsoft.com/en-us/rest/api/marketplacecatalog/dataplane/skus/list?view=rest-marketplacecatalog-dataplane-2023-05-01-preview&tabs=HTTP
@@ -44,24 +50,27 @@ export interface ConfigContext {
ARCADIA_ENDPOINT: string;
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
PORTAL_BACKEND_ENDPOINT: string;
NEW_BACKEND_APIS?: BackendApi[];
MONGO_PROXY_ENDPOINT: string;
CASSANDRA_PROXY_ENDPOINT: string;
NEW_CASSANDRA_APIS?: string[];
PROXY_PATH?: string;
JUNO_ENDPOINT: string;
GITHUB_CLIENT_ID: string;
GITHUB_TEST_ENV_CLIENT_ID: string;
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
isTerminalEnabled: boolean;
isPhoenixEnabled: boolean;
hostedExplorerURL: string;
armAPIVersion?: string;
msalRedirectURI?: string;
globallyEnabledCassandraAPIs?: string[];
globallyEnabledMongoAPIs?: string[];
}
// Default configuration
let configContext: Readonly<ConfigContext> = {
platform: Platform.Portal,
allowedAadEndpoints: defaultAllowedAadEndpoints,
allowedGraphEndpoints: defaultAllowedGraphEndpoints,
allowedArmEndpoints: defaultAllowedArmEndpoints,
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
@@ -76,12 +85,17 @@ let configContext: Readonly<ConfigContext> = {
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
`^https:\\/\\/.*\\.powerbi\\.com$`,
`^https:\\/\\/.*\\.analysis-df\\.net$`,
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
`^https:\\/\\/.*\\.azure-test\\.net$`,
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
], // Webpack injects this at build time
gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/",
AAD_ENDPOINT: "https://login.microsoftonline.com/",
ARM_AUTH_AREA: "https://management.azure.com/",
ARM_ENDPOINT: "https://management.azure.com/",
ARM_API_VERSION: "2016-06-01",
GRAPH_ENDPOINT: "https://graph.microsoft.com",
GRAPH_API_VERSION: "1.6",
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
@@ -95,7 +109,11 @@ let configContext: Readonly<ConfigContext> = {
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
isTerminalEnabled: false,
isPhoenixEnabled: false,
globallyEnabledCassandraAPIs: [],
globallyEnabledMongoAPIs: [],
};
export function resetConfigContext(): void {
@@ -110,21 +128,19 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
return;
}
if (!validateEndpoint(newContext.AAD_ENDPOINT, configContext.allowedAadEndpoints || defaultAllowedAadEndpoints)) {
delete newContext.AAD_ENDPOINT;
}
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
delete newContext.ARM_ENDPOINT;
}
if (!validateEndpoint(newContext.AAD_ENDPOINT, allowedAadEndpoints)) {
delete newContext.AAD_ENDPOINT;
}
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
delete newContext.EMULATOR_ENDPOINT;
}
if (
!validateEndpoint(newContext.GRAPH_ENDPOINT, configContext.allowedGraphEndpoints || defaultAllowedGraphEndpoints)
) {
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) {
delete newContext.GRAPH_ENDPOINT;
}
@@ -132,15 +148,6 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
delete newContext.ARCADIA_ENDPOINT;
}
if (
!validateEndpoint(
newContext.PORTAL_BACKEND_ENDPOINT,
configContext.allowedBackendEndpoints || defaultAllowedBackendEndpoints,
)
) {
delete newContext.PORTAL_BACKEND_ENDPOINT;
}
if (
!validateEndpoint(
newContext.MONGO_PROXY_ENDPOINT,

View File

@@ -23,7 +23,6 @@ export enum PaneKind {
GlobalSettings,
AdHocAccess,
SwitchDirectory,
QuickStart,
}
/**

View File

@@ -7,7 +7,6 @@ export interface ArmEntity {
type: string;
kind: string;
tags?: Tags;
resourceGroup?: string;
}
export interface DatabaseAccount extends ArmEntity {
@@ -389,7 +388,7 @@ export interface VectorEmbeddingPolicy {
}
export interface VectorEmbedding {
dataType: "float32" | "uint8" | "int8";
dataType: "float16" | "float32" | "uint8" | "int8";
dimensions: number;
distanceFunction: "euclidean" | "cosine" | "dotproduct";
path: string;

View File

@@ -443,7 +443,6 @@ export interface DataExplorerInputsFrame {
[key: string]: string;
};
feedbackPolicies?: any;
aadToken?: string;
}
export interface SelfServeFrameInputs {

View File

@@ -0,0 +1,11 @@
<!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>

View File

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

View File

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

View File

@@ -0,0 +1,272 @@
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();

View File

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

View File

@@ -16,7 +16,7 @@ import {
TextField,
} from "@fluentui/react";
import React, { FC, useEffect } from "react";
import { create } from "zustand";
import create, { UseStore } from "zustand";
export interface DialogState {
visible: boolean;
@@ -38,7 +38,7 @@ export interface DialogState {
showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps) => void;
}
export const useDialog = create<DialogState>((set, get) => ({
export const useDialog: UseStore<DialogState> = create((set, get) => ({
visible: false,
openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })),
closeDialog: () =>

View File

@@ -559,81 +559,26 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
private getThroughputTextField = (): JSX.Element => (
<>
{this.props.isAutoPilotSelected ? (
<Stack horizontal verticalAlign="end" tokens={{ childrenGap: 8 }}>
{/* Column 1: Minimum RU/s */}
<Stack tokens={{ childrenGap: 4 }}>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
Minimum RU/s
</Text>
<FontIcon iconName="Info" style={{ fontSize: 12, color: "#666" }} />
</Stack>
<Text
style={{
fontFamily: "Segoe UI",
width: 70,
height: 28,
border: "none",
fontSize: 14,
backgroundColor: "transparent",
fontWeight: 400,
display: "flex",
alignItems: "center",
justifyContent: "center",
boxSizing: "border-box",
}}
>
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)}
</Text>
</Stack>
{/* Column 2: "x 10 =" Text */}
<Text
style={{
fontFamily: "Segoe UI",
fontSize: 12,
fontWeight: 400,
paddingBottom: 6,
}}
>
x 10 =
</Text>
{/* Column 3: Maximum RU/s */}
<Stack tokens={{ childrenGap: 4 }}>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
Maximum RU/s
</Text>
<FontIcon iconName="Info" style={{ fontSize: 12, color: "#666" }} />
</Stack>
<TextField
required
type="number"
id="autopilotInput"
key="auto pilot throughput input"
styles={{
...getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline),
fieldGroup: { width: 100, height: 28 },
field: { fontSize: 14, fontWeight: 400 },
}}
disabled={this.overrideWithProvisionedThroughputSettings()}
step={AutoPilotUtils.autoPilotIncrementStep}
value={
this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()
}
onChange={this.onAutoPilotThroughputChange}
min={autoPilotThroughput1K}
onGetErrorMessage={(value: string) => {
const sanitizedValue = getSanitizedInputValue(value);
return sanitizedValue % 1000
? "Throughput value must be in increments of 1000"
: this.props.throughputError;
}}
validateOnLoad={false}
/>
</Stack>
</Stack>
<TextField
label="Maximum RU/s required by this resource"
required
type="number"
id="autopilotInput"
key="auto pilot throughput input"
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
disabled={this.overrideWithProvisionedThroughputSettings()}
step={AutoPilotUtils.autoPilotIncrementStep}
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
onChange={this.onAutoPilotThroughputChange}
min={autoPilotThroughput1K}
onGetErrorMessage={(value: string) => {
const sanitizedValue = getSanitizedInputValue(value);
return sanitizedValue % 1000
? "Throughput value must be in increments of 1000"
: this.props.throughputError;
}}
validateOnLoad={false}
/>
) : (
<TextField
required

View File

@@ -157,148 +157,35 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
}
}
>
<Stack
horizontal={true}
tokens={
<StyledTextFieldBase
disabled={true}
id="autopilotInput"
key="auto pilot throughput input"
label="Maximum RU/s required by this resource"
min={1000}
onChange={[Function]}
onGetErrorMessage={[Function]}
required={true}
step={1000}
styles={
{
"childrenGap": 8,
"fieldGroup": {
"borderColor": "",
"height": 25,
"selectors": {
":disabled": {
"backgroundColor": undefined,
"borderColor": undefined,
},
},
"width": 300,
},
}
}
verticalAlign="end"
>
<Stack
tokens={
{
"childrenGap": 4,
}
}
>
<Stack
horizontal={true}
tokens={
{
"childrenGap": 4,
}
}
verticalAlign="center"
>
<Text
style={
{
"fontWeight": 600,
"lineHeight": "20px",
}
}
variant="small"
>
Minimum RU/s
</Text>
<FontIcon
iconName="Info"
style={
{
"color": "#666",
"fontSize": 12,
}
}
/>
</Stack>
<Text
style={
{
"alignItems": "center",
"backgroundColor": "transparent",
"border": "none",
"boxSizing": "border-box",
"display": "flex",
"fontFamily": "Segoe UI",
"fontSize": 14,
"fontWeight": 400,
"height": 28,
"justifyContent": "center",
"width": 70,
}
}
>
400
</Text>
</Stack>
<Text
style={
{
"fontFamily": "Segoe UI",
"fontSize": 12,
"fontWeight": 400,
"paddingBottom": 6,
}
}
>
x 10 =
</Text>
<Stack
tokens={
{
"childrenGap": 4,
}
}
>
<Stack
horizontal={true}
tokens={
{
"childrenGap": 4,
}
}
verticalAlign="center"
>
<Text
style={
{
"fontWeight": 600,
"lineHeight": "20px",
}
}
variant="small"
>
Maximum RU/s
</Text>
<FontIcon
iconName="Info"
style={
{
"color": "#666",
"fontSize": 12,
}
}
/>
</Stack>
<StyledTextFieldBase
disabled={true}
id="autopilotInput"
key="auto pilot throughput input"
min={1000}
onChange={[Function]}
onGetErrorMessage={[Function]}
required={true}
step={1000}
styles={
{
"field": {
"fontSize": 14,
"fontWeight": 400,
},
"fieldGroup": {
"height": 28,
"width": 100,
},
}
}
type="number"
validateOnLoad={false}
value=""
/>
</Stack>
</Stack>
type="number"
validateOnLoad={false}
value=""
/>
<Stack>
<Stack>
<Stack

View File

@@ -5,13 +5,13 @@ import { useDatabases } from "Explorer/useDatabases";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
import * as SharedConstants from "../../../Shared/Constants";
import { userContext } from "../../../UserContext";
import { getCollectionName } from "../../../Utils/APITypeUtils";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../../Utils/PricingUtils";
import "./ThroughputInput.less";
import { isFabricNative } from "../../../Platform/Fabric/FabricUtil";
export interface ThroughputInputProps {
isDatabase: boolean;
@@ -41,12 +41,11 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
let defaultThroughput: number;
const workloadType: Constants.WorkloadType = getWorkloadType();
if (isFabricNative()) {
defaultThroughput = AutoPilotUtils.autoPilotThroughput5K;
} else if (
if (
isFreeTier ||
isQuickstart ||
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType)
[Constants.WorkloadType.Learning, Constants.WorkloadType.DevelopmentTesting].includes(workloadType) ||
isFabricNative()
) {
defaultThroughput = AutoPilotUtils.autoPilotThroughput1K;
} else if (workloadType === Constants.WorkloadType.Production) {

View File

@@ -1,4 +1,3 @@
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
import { createCollection } from "../../Common/dataAccess/createCollection";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { createDocument as createMongoDocument } from "../../Common/MongoProxyClient";
@@ -91,13 +90,12 @@ export class ContainerSampleGenerator {
}
const { databaseAccount: account } = userContext;
const databaseId = collection.databaseId;
const gremlinClient = new GremlinClient();
gremlinClient.initialize({
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
databaseId: databaseId,
collectionId: collection.id(),
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
masterKey: userContext.masterKey || "",
maxResultSize: 100,
});

View File

@@ -8,12 +8,7 @@ import { MessageTypes } from "Contracts/ExplorerContracts";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { IGalleryItem } from "Juno/JunoClient";
import {
isFabricMirrored,
isFabricMirroredKey,
isFabricNative,
scheduleRefreshFabricToken,
} from "Platform/Fabric/FabricUtil";
import { isFabricMirrored, isFabricMirroredKey, scheduleRefreshFabricToken } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
@@ -23,7 +18,7 @@ import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as ko from "knockout";
import React from "react";
import _ from "underscore";
import { shallow } from "zustand/shallow";
import shallow from "zustand/shallow";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants";
@@ -112,8 +107,8 @@ export default class Explorer {
this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
useNotebook.subscribe(
(state) => state.isNotebooksEnabledForAccount,
() => this.refreshCommandBarButtons(),
(state) => state.isNotebooksEnabledForAccount,
);
this.queriesClient = new QueriesClient(this);
@@ -136,13 +131,13 @@ export default class Explorer {
});
useTabs.subscribe(
(state) => state.openedTabs,
(openedTabs: TabsBase[]) => {
if (openedTabs.length === 0) {
useSelectedNode.getState().setSelectedNode(undefined);
useCommandBar.getState().setContextButtons([]);
}
}
},
(state) => state.openedTabs,
);
this.isTabsContentExpanded = ko.observable(false);
@@ -170,9 +165,9 @@ export default class Explorer {
);
useNotebook.subscribe(
(state) => [state.isNotebookEnabled, state.isRefreshed],
async () => this.initiateAndRefreshNotebookList(),
{ equalityFn: shallow },
(state) => [state.isNotebookEnabled, state.isRefreshed],
shallow,
);
this.resourceTree = new ResourceTreeAdapter(this);
@@ -289,40 +284,14 @@ export default class Explorer {
}
}
/**
* 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() {
public openInVsCode(): void {
const activeTab = useTabs.getState().activeTab;
const resourceId = encodeURIComponent(userContext.databaseAccount.id);
const database = encodeURIComponent(activeTab?.collection?.databaseId);
const container = encodeURIComponent(activeTab?.collection?.id());
const baseUrl = `vscode://ms-azuretools.vscode-cosmosdb?resourceId=${resourceId}`;
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 = {
linkProps: {
linkText: "Download Visual Studio Code",
@@ -1180,10 +1149,7 @@ export default class Explorer {
? this.refreshDatabaseForResourceToken()
: await this.refreshAllDatabases(); // await: we rely on the databases to be loaded before restoring the tabs further in the flow
}
if (!isFabricNative()) {
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
}
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
const isNotebookEnabled =
@@ -1205,7 +1171,7 @@ export default class Explorer {
await this.initNotebooks(userContext.databaseAccount);
}
if (userContext.authType === AuthType.AAD && userContext.apiType === "SQL" && !isFabricNative()) {
if (userContext.authType === AuthType.AAD && userContext.apiType === "SQL") {
const throughputBucketsEnabled = await featureRegistered(userContext.subscriptionId, "ThroughputBucketing");
updateUserContext({ throughputBucketsEnabled });
}

View File

@@ -163,7 +163,8 @@ describe("GraphExplorer", () => {
graphBackendEndpoint: "graphBackendEndpoint",
databaseId: "databaseId",
collectionId: "collectionId",
password: "password",
masterKey: "masterKey",
onLoadStartKey: 0,
onLoadStartKeyChange: (newKey: number): void => {},
resourceId: "resourceId",

View File

@@ -59,7 +59,7 @@ export interface GraphExplorerProps {
graphBackendEndpoint: string;
databaseId: string;
collectionId: string;
password: string;
masterKey: string;
onLoadStartKey: number;
onLoadStartKeyChange: (newKey: number) => void;
@@ -1300,7 +1300,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
endpoint: `wss://${this.props.graphBackendEndpoint}`,
databaseId: this.props.databaseId,
collectionId: this.props.collectionId,
password: this.props.password,
masterKey: this.props.masterKey,
maxResultSize: GraphExplorer.MAX_RESULT_SIZE,
});
}

View File

@@ -8,28 +8,28 @@ describe("Gremlin Client", () => {
endpoint: null,
collectionId: null,
databaseId: null,
masterKey: null,
maxResultSize: 10000,
password: null,
};
it("should use databaseId, collectionId and password to authenticate", () => {
it("should use databaseId, collectionId and masterKey to authenticate", () => {
const collectionId = "collectionId";
const databaseId = "databaseId";
const testPassword = "password";
const masterKey = "masterKey";
const gremlinClient = new GremlinClient();
gremlinClient.initialize({
endpoint: null,
collectionId,
databaseId,
masterKey,
maxResultSize: 0,
password: testPassword,
});
// User must includes these values
expect(gremlinClient.client.params.user.indexOf(collectionId)).not.toBe(-1);
expect(gremlinClient.client.params.user.indexOf(databaseId)).not.toBe(-1);
expect(gremlinClient.client.params.password).toEqual(testPassword);
expect(gremlinClient.client.params.password).toEqual(masterKey);
});
it("should aggregate RU charges across multiple responses", (done) => {

View File

@@ -11,8 +11,8 @@ export interface GremlinClientParameters {
endpoint: string;
databaseId: string;
collectionId: string;
masterKey: string;
maxResultSize: number;
password: string;
}
export interface GremlinRequestResult {
@@ -43,7 +43,7 @@ export class GremlinClient {
this.client = new GremlinSimpleClient({
endpoint: params.endpoint,
user: `/dbs/${params.databaseId}/colls/${params.collectionId}`,
password: params.password,
password: params.masterKey,
successCallback: (result: Result) => {
this.storePendingResult(result);
this.flushResult(result.requestId);

View File

@@ -5,11 +5,11 @@
import * as sinon from "sinon";
import {
GremlinRequestMessage,
GremlinResponseMessage,
GremlinSimpleClient,
GremlinSimpleClientParameters,
Result,
GremlinRequestMessage,
GremlinResponseMessage,
} from "./GremlinSimpleClient";
describe("Gremlin Simple Client", () => {

View File

@@ -95,10 +95,3 @@
white-space: nowrap;
}
}
@media (max-width: 768px) {
.newVertexComponent {
padding: 0;
width: 100%;
}
}

View File

@@ -5,12 +5,11 @@
*/
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext";
import * as React from "react";
import { create } from "zustand";
import create, { UseStore } from "zustand";
import { ConnectionStatusType, PoolIdType } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/StyleConstants";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
@@ -30,8 +29,8 @@ export interface CommandBarStore {
setIsHidden: (isHidden: boolean) => void;
}
export const useCommandBar = create<CommandBarStore>((set) => ({
contextButtons: [] as CommandButtonComponentProps[],
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
contextButtons: [],
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
isHidden: false,
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
@@ -44,15 +43,6 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const backgroundColor = StyleConstants.BaseLight;
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
// Subscribe to the store changes that affect button creation
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
// Memoize the expensive button creation
const staticButtons = React.useMemo(() => {
return CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
}, [container, selectedNodeState, dataPlaneRbacEnabled, aadTokenUpdated]);
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
const buttons =
userContext.apiType === "Postgres"
@@ -72,6 +62,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
);
}
const staticButtons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState);
const contextButtons = (buttons || []).concat(
CommandBarComponentButtonFactory.createContextCommandBarButtons(container, selectedNodeState),
);

View File

@@ -1,6 +1,7 @@
import { KeyboardAction } from "KeyboardShortcuts";
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
import * as React from "react";
import { useEffect, useState } from "react";
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
@@ -67,7 +68,15 @@ export function createStaticCommandBarButtons(
}
if (isDataplaneRbacSupported(userContext.apiType)) {
const loginButtonProps = createLoginForEntraIDButton(container);
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
useEffect(() => {
const buttonProps = createLoginForEntraIDButton(container);
setLoginButtonProps(buttonProps);
}, [dataPlaneRbacEnabled, aadTokenUpdated, container]);
if (loginButtonProps) {
addDivider();
buttons.push(loginButtonProps);

View File

@@ -1,5 +1,5 @@
import { AppState, ContentRef, selectors } from "@nteract/core";
import { formatDistanceToNow } from "date-fns";
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
import React from "react";
import { connect } from "react-redux";
import styled from "styled-components";
@@ -59,7 +59,7 @@ export class StatusBar extends React.Component<Props> {
<Bar data-test="notebookStatusBar">
<RightStatus>
{this.props.lastSaved ? (
<p data-test="saveStatus"> Last saved {formatDistanceToNow(this.props.lastSaved)} ago </p>
<p data-test="saveStatus"> Last saved {distanceInWordsToNow(this.props.lastSaved)} </p>
) : (
<p> Not saved yet </p>
)}

View File

@@ -1,8 +1,7 @@
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { PhoenixClient } from "Phoenix/PhoenixClient";
import { cloneDeep } from "lodash";
import { create } from "zustand";
import { subscribeWithSelector } from 'zustand/middleware';
import create, { UseStore } from "zustand";
import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants";
import { ConnectionStatusType, HttpStatusCodes } from "../../Common/Constants";
@@ -67,274 +66,270 @@ interface NotebookState {
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
}
export const useNotebook = create<NotebookState>()(
subscribeWithSelector(
(set, get) => ({
isNotebookEnabled: false,
isNotebooksEnabledForAccount: false,
notebookServerInfo: {
notebookServerEndpoint: "",
authToken: "",
forwardingId: "",
},
sparkClusterConnectionInfo: {
userName: "",
password: "",
endpoints: [] as DataModels.SparkClusterEndpoint[],
},
isSynapseLinkUpdating: false,
memoryUsageInfo: undefined as DataModels.MemoryUsageInfo,
isShellEnabled: false,
notebookBasePath: Constants.Notebook.defaultBasePath,
isInitializingNotebooks: false,
myNotebooksContentRoot: undefined as NotebookContentItem,
gitHubNotebooksContentRoot: undefined as NotebookContentItem,
galleryContentRoot: undefined as NotebookContentItem,
connectionInfo: {
status: ConnectionStatusType.Connect,
},
notebookFolderName: "",
isAllocating: false,
isRefreshed: false,
containerStatus: {
status: undefined,
durationLeftInMinutes: undefined,
phoenixServerInfo: undefined,
} as ContainerInfo,
isPhoenixNotebooks: undefined as boolean,
isPhoenixFeatures: undefined as boolean,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
set({ notebookServerInfo }),
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) =>
set({ sparkClusterConnectionInfo }),
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
await get().getPhoenixStatus();
const { databaseAccount, authType } = userContext;
if (
authType === AuthType.EncryptedToken ||
authType === AuthType.ResourceToken ||
authType === AuthType.MasterKey
) {
set({ isNotebooksEnabledForAccount: false });
return;
}
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
isNotebookEnabled: false,
isNotebooksEnabledForAccount: false,
notebookServerInfo: {
notebookServerEndpoint: undefined,
authToken: undefined,
forwardingId: undefined,
},
sparkClusterConnectionInfo: {
userName: undefined,
password: undefined,
endpoints: [],
},
isSynapseLinkUpdating: false,
memoryUsageInfo: undefined,
isShellEnabled: false,
notebookBasePath: Constants.Notebook.defaultBasePath,
isInitializingNotebooks: false,
myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined,
galleryContentRoot: undefined,
connectionInfo: {
status: ConnectionStatusType.Connect,
},
notebookFolderName: undefined,
isAllocating: false,
isRefreshed: false,
containerStatus: {
status: undefined,
durationLeftInMinutes: undefined,
phoenixServerInfo: undefined,
},
isPhoenixNotebooks: undefined,
isPhoenixFeatures: undefined,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
set({ notebookServerInfo }),
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) =>
set({ sparkClusterConnectionInfo }),
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
await get().getPhoenixStatus();
const { databaseAccount, authType } = userContext;
if (
authType === AuthType.EncryptedToken ||
authType === AuthType.ResourceToken ||
authType === AuthType.MasterKey
) {
set({ isNotebooksEnabledForAccount: false });
return;
}
const firstWriteLocation =
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
? databaseAccount?.location
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`;
const authorizationHeader = getAuthorizationHeader();
try {
const response = await fetch(disallowedLocationsUri, {
method: "POST",
body: JSON.stringify({
resourceTypes: [Constants.ArmResourceTypes.notebookWorkspaces],
}),
headers: {
[authorizationHeader.header]: authorizationHeader.token,
[Constants.HttpHeaders.contentType]: "application/json",
},
});
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");
}
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;
}
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;
// 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) {
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;
}
}
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,
return undefined;
},
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => {
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, parent);
item.parent = parentItem;
if (parentItem.children) {
parentItem.children.push(item);
} else {
parentItem.children = [item];
}
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, item.parent);
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
parentItem.children.push(item);
item.parent = parentItem;
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, item.parent);
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
const notebookFolderName = get().isPhoenixNotebooks ? "Temporary Notebooks" : "My Notebooks";
set({ notebookFolderName });
const myNotebooksContentRoot = {
name: get().notebookFolderName,
path: get().notebookBasePath,
type: NotebookContentItemType.Directory,
};
const galleryContentRoot = {
name: "Gallery",
path: "Gallery",
type: NotebookContentItemType.File,
};
const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn()
? {
name: "GitHub repos",
path: "PsuedoDir",
type: NotebookContentItemType.Directory,
}
: undefined;
set({
myNotebooksContentRoot,
galleryContentRoot,
gitHubNotebooksContentRoot,
});
if (get().notebookServerInfo?.notebookServerEndpoint) {
const updatedRoot = await notebookManager?.notebookContentClient?.updateItemChildren(myNotebooksContentRoot);
set({ myNotebooksContentRoot: updatedRoot });
if (updatedRoot?.children) {
// Count 1st generation children (tree is lazy-loaded)
const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
updatedRoot.children.forEach((notebookItem) => {
switch (notebookItem.type) {
case NotebookContentItemType.File:
nodeCounts.files++;
break;
case NotebookContentItemType.Directory:
nodeCounts.directories++;
break;
case NotebookContentItemType.Notebook:
nodeCounts.notebooks++;
break;
default:
break;
}
});
TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts });
}
}
},
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]): void => {
const gitHubNotebooksContentRoot = cloneDeep(get().gitHubNotebooksContentRoot);
if (gitHubNotebooksContentRoot) {
gitHubNotebooksContentRoot.children = [];
pinnedRepos?.forEach((pinnedRepo) => {
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
const repoTreeItem: NotebookContentItem = {
name: repoFullName,
path: "PsuedoDir",
type: NotebookContentItemType.Directory,
children: [],
parent: gitHubNotebooksContentRoot,
};
const galleryContentRoot = {
name: "Gallery",
path: "Gallery",
type: NotebookContentItemType.File,
};
const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn()
? {
name: "GitHub repos",
path: "PsuedoDir",
pinnedRepo.branches.forEach((branch) => {
repoTreeItem.children.push({
name: branch.name,
path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""),
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,
});
});
gitHubNotebooksContentRoot.children.push(repoTreeItem);
parent: 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();
gitHubNotebooksContentRoot.children.push(repoTreeItem);
});
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 });
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;
}
},
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }),
})
)
);
} else {
isPhoenixNotebooks = isPhoenixFeatures = false;
}
set({ isPhoenixNotebooks: isPhoenixNotebooks });
set({ isPhoenixFeatures: isPhoenixFeatures });
}
},
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }),
}));

View File

@@ -188,11 +188,6 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection]
) {
explorer.onNewCollectionClicked();
} else if (
action.paneKind === ActionContracts.PaneKind.QuickStart ||
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.QuickStart]
) {
explorer.onNewCollectionClicked({ isQuickstart: true });
} else if (
action.paneKind === ActionContracts.PaneKind.CassandraAddCollection ||
action.paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection]

View File

@@ -52,7 +52,6 @@ import { getCollectionName } from "Utils/APITypeUtils";
import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { getUpsellMessage } from "Utils/PricingUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import { CollapsibleSectionComponent } from "../../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import { ContainerSampleGenerator } from "../../DataSamples/ContainerSampleGenerator";
@@ -61,6 +60,7 @@ import { useDatabases } from "../../useDatabases";
import { PanelFooterComponent } from "../PanelFooterComponent";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
import { PanelLoadingScreen } from "../PanelLoadingScreen";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
export interface AddCollectionPanelProps {
explorer: Explorer;
@@ -123,7 +123,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
isSharded: userContext.apiType !== "Tables",
partitionKey: getPartitionKey(props.isQuickstart),
subPartitionKeys: [],
enableDedicatedThroughput: isFabricNative(), // Dedicated throughput is only enabled in Fabric Native by default
enableDedicatedThroughput: false,
createMongoWildCardIndex:
isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport"),
useHashV1: false,
@@ -336,6 +336,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
size={40}
className="panelTextField"
aria-label="New database id, Type a new database id"
autoFocus
tabIndex={0}
value={this.state.newDatabaseId}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
@@ -406,9 +407,9 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
responsiveMode={999}
/>
)}
<Separator className="panelSeparator" style={{ marginTop: -4, marginBottom: -4 }} />
</Stack>
)}
<Separator className="panelSeparator" style={{ marginTop: -4, marginBottom: -4 }} />
<Stack>
<Stack horizontal style={{ marginTop: -5, marginBottom: 1 }}>
@@ -448,9 +449,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
this.setState({ collectionId: event.target.value })
}
/>
<Separator className="panelSeparator" style={{ marginTop: -5, marginBottom: -5 }} />
</Stack>
<Separator className="panelSeparator" style={{ marginTop: -5, marginBottom: -5 }} />
{this.shouldShowIndexingOptionsForFreeTierAccount() && (
<Stack>
<Stack horizontal style={{ marginTop: -4, marginBottom: -5 }}>
@@ -645,7 +645,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack>
);
})}
{userContext.apiType === "SQL" && (
{!isFabricNative() && userContext.apiType === "SQL" && (
<Stack className="panelGroupSpacing">
<DefaultButton
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
@@ -709,7 +709,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack>
)}
{this.shouldShowCollectionThroughputInput() && !isFabricNative() && (
{this.shouldShowCollectionThroughputInput() && (
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount() && !isFirstResourceCreated}
isDatabase={false}
@@ -742,6 +742,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
: "Comma separated paths e.g. /firstName,/address/zipCode"
}
className="panelTextField"
autoFocus
value={uniqueKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const uniqueKeys = this.state.uniqueKeys.map((uniqueKey: string, j: number) => {
@@ -776,9 +777,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack>
)}
{!isFabricNative() && userContext.apiType === "SQL" && (
<Separator className="panelSeparator" style={{ marginTop: -15, marginBottom: -4 }} />
)}
<Separator className="panelSeparator" style={{ marginTop: -15, marginBottom: -4 }} />
{shouldShowAnalyticalStoreOptions() && (
<Stack className="panelGroupSpacing" style={{ marginTop: -4 }}>
@@ -1134,7 +1133,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
// }
private shouldShowCollectionThroughputInput(): boolean {
if (isServerlessAccount()) {
if (isFabricNative() || isServerlessAccount()) {
return false;
}
@@ -1355,8 +1354,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
// Throughput
if (isFabricNative()) {
// Fabric Native accounts are always autoscale and have a fixed throughput of 5K
autoPilotMaxThroughput = AutoPilotUtils.autoPilotThroughput5K;
// Fabric Native accounts are always autoscale and have a fixed throughput of 1K
autoPilotMaxThroughput = AutoPilotUtils.autoPilotThroughput1K;
offerThroughput = undefined;
} else if (databaseLevelThroughput) {
if (this.state.createNewDatabase) {

View File

@@ -93,6 +93,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
aria-label="New database id, Type a new database id"
aria-required={true}
autoComplete="off"
autoFocus={true}
className="panelTextField"
id="newDatabaseId"
name="newDatabaseId"
@@ -142,16 +143,16 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
</StyledTooltipHostBase>
</Stack>
</Stack>
<Separator
className="panelSeparator"
style={
{
"marginBottom": -4,
"marginTop": -4,
}
}
/>
</Stack>
<Separator
className="panelSeparator"
style={
{
"marginBottom": -4,
"marginTop": -4,
}
}
/>
<Stack>
<Stack
horizontal={true}
@@ -202,16 +203,16 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
type="text"
value=""
/>
<Separator
className="panelSeparator"
style={
{
"marginBottom": -5,
"marginTop": -5,
}
}
/>
</Stack>
<Separator
className="panelSeparator"
style={
{
"marginBottom": -5,
"marginTop": -5,
}
}
/>
<Stack>
<Stack
horizontal={true}

View File

@@ -11,10 +11,10 @@
margin: 20px 0;
overflow-x: hidden;
&> :not(.collapsibleSection) {
& > :not(.collapsibleSection) {
margin-bottom: @DefaultSpace;
&> :not(:last-child) {
& > :not(:last-child) {
margin-bottom: @DefaultSpace;
}
}
@@ -56,14 +56,6 @@
transform: translate(-50%, -50%);
}
}
@media (max-width: 768px) {
.panelMainContent {
padding: 0 24px;
margin: 0;
overflow-x: auto;
}
}
}
.panelHeader {
@@ -121,87 +113,70 @@
.deleteCollectionFeedback {
margin-top: 12px;
}
.addRemoveIcon {
margin-left: 4px !important;
}
.addRemoveIconLabel {
margin-top: 28px;
margin-left: 4px !important;
}
.addRemoveIcon [alt="editEntity"]:focus,
.addRemoveIconLabel [alt="editEntity"]:focus {
border: 1px dashed #605e5c;
}
.addNewParamStyle {
margin-top: 5px;
margin-left: 5px !important;
cursor: pointer;
}
.panelGroupSpacing> :not(:last-child) {
.panelGroupSpacing > :not(:last-child) {
margin-bottom: @DefaultSpace;
}
.fileUpload {
display: none !important;
}
.customFileUpload {
padding: 25px 0px 0px 10px;
cursor: pointer;
display: flex;
}
.fileIcon {
align-self: center;
}
.panelAddIconLabel {
font-size: 20px;
width: 20px;
margin: 30px 0 0 10px;
cursor: default;
}
.panelAddIcon {
font-size: 20px;
width: 20px;
margin: 30px 0 0 10px;
cursor: default;
}
.removeIcon {
color: @InfoIconColor;
}
.backImageIcon {
margin-top: 8px;
}
[alt="back"]:focus {
border: 1px solid #605e5c;
}
.addEntityDatePicker {
max-width: 145px;
}
.addEntityTextField {
width: 237px;
}
.addButtonEntiy {
width: 25%;
}
.column-select-view {
margin: 20px 0px 0px 0px;
}
.panelSeparator::before {
background-color: #edebe9;
}
}

View File

@@ -51,7 +51,7 @@ import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react";
import { create } from "zustand";
import create, { UseStore } from "zustand";
import Explorer from "../../Explorer";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
@@ -65,6 +65,8 @@ export interface DataPlaneRbacState {
setAadDataPlaneUpdated: (aadTokenUpdated: boolean) => void;
}
type DataPlaneRbacStore = UseStore<Partial<DataPlaneRbacState>>;
const useStyles = makeStyles({
bulletList: {
listStyleType: "disc",
@@ -98,7 +100,7 @@ const useStyles = makeStyles({
},
});
export const useDataPlaneRbac = create<Partial<DataPlaneRbacState>>(() => ({
export const useDataPlaneRbac: DataPlaneRbacStore = create(() => ({
dataPlaneRbacEnabled: false,
}));
@@ -197,12 +199,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
LocalStorageUtility.getEntryString(StorageKey.CopilotSampleDBEnabled) === "true",
);
const [mongoGuidRepresentation, setMongoGuidRepresentation] = useState<Constants.MongoGuidRepresentation>(
LocalStorageUtility.hasItem(StorageKey.MongoGuidRepresentation)
? (LocalStorageUtility.getEntryString(StorageKey.MongoGuidRepresentation) as Constants.MongoGuidRepresentation)
: Constants.MongoGuidRepresentation.CSharpLegacy,
);
const styles = useStyles();
const explorerVersion = configContext.gitSha;
@@ -265,8 +261,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
useDatabases.getState().sampleDataResourceTokenCollection &&
!isEmulator;
const shouldShowMongoGuidRepresentationOption = userContext.apiType === "Mongo";
const handlerOnSubmit = async () => {
setIsExecuting(true);
@@ -418,10 +412,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
);
}
if (shouldShowMongoGuidRepresentationOption) {
LocalStorageUtility.setEntryString(StorageKey.MongoGuidRepresentation, mongoGuidRepresentation);
}
setIsExecuting(false);
logConsoleInfo(
`Updated items per page setting to ${LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage)}`,
@@ -443,14 +433,9 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
);
}
if (shouldShowMongoGuidRepresentationOption) {
logConsoleInfo(
`Updated Mongo Guid Representation to ${LocalStorageUtility.getEntryString(
StorageKey.MongoGuidRepresentation,
)}`,
);
}
logConsoleInfo(
`Updated query setting to ${LocalStorageUtility.getEntryString(StorageKey.SetPartitionKeyUndefined)}`,
);
refreshExplorer && (await explorer.refreshExplorer());
closeSidePanel();
};
@@ -495,13 +480,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
{ key: SplitterDirection.Horizontal, text: "Horizontal" },
];
const mongoGuidRepresentationDropdownOptions: IDropdownOption[] = [
{ key: Constants.MongoGuidRepresentation.CSharpLegacy, text: Constants.MongoGuidRepresentation.CSharpLegacy },
{ key: Constants.MongoGuidRepresentation.JavaLegacy, text: Constants.MongoGuidRepresentation.JavaLegacy },
{ key: Constants.MongoGuidRepresentation.PythonLegacy, text: Constants.MongoGuidRepresentation.PythonLegacy },
{ key: Constants.MongoGuidRepresentation.Standard, text: Constants.MongoGuidRepresentation.Standard },
];
const handleOnPriorityLevelOptionChange = (
ev: React.FormEvent<HTMLInputElement>,
option: IChoiceGroupOption,
@@ -584,13 +562,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
setRefreshExplorer(false);
};
const handleOnMongoGuidRepresentationOptionChange = (
ev: React.FormEvent<HTMLInputElement>,
option: IDropdownOption,
): void => {
setMongoGuidRepresentation(option.key as Constants.MongoGuidRepresentation);
};
const choiceButtonStyles = {
root: {
clear: "both",
@@ -1097,15 +1068,15 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
This is a sample database and collection with synthetic product data you can use to explore using
NoSQL queries. This will appear as another database in the Data Explorer UI, and is created by,
and maintained by Microsoft at no cost to you.
NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and
is created by, and maintained by Microsoft at no cost to you.
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
ariaLabel="Enable sample db for query exploration"
ariaLabel="Enable sample db for Query Advisor"
checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange}
label="Enable sample database"
@@ -1114,27 +1085,6 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
</AccordionPanel>
</AccordionItem>
)}
{shouldShowMongoGuidRepresentationOption && (
<AccordionItem value="14">
<AccordionHeader>
<div className={styles.header}>Guid Representation</div>
</AccordionHeader>
<AccordionPanel>
<div className={styles.settingsSectionContainer}>
<div className={styles.settingsSectionDescription}>
GuidRepresentation in MongoDB refers to how Globally Unique Identifiers (GUIDs) are serialized and
deserialized when stored in BSON documents. This will apply to all document operations.
</div>
<Dropdown
aria-labelledby="mongoGuidRepresentation"
selectedKey={mongoGuidRepresentation}
options={mongoGuidRepresentationDropdownOptions}
onChange={handleOnMongoGuidRepresentationOptionChange}
/>
</div>
</AccordionPanel>
</AccordionItem>
)}
</Accordion>
)}

View File

@@ -1,14 +1,12 @@
import { MinimalQueryIterator } from "Common/IteratorUtilities";
import QueryError from "Common/QueryError";
import * as DataModels from "Contracts/DataModels";
import { QueryResults } from "Contracts/ViewModels";
import { CopilotMessage } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { guid } from "Explorer/Tables/Utilities";
import { QueryCopilotState } from "hooks/useQueryCopilot";
import React, { createContext, useContext, useState } from "react";
import { create } from "zustand";
import create from "zustand";
const context = createContext(null);
const useCopilotStore = (): Partial<QueryCopilotState> => useContext(context);
@@ -26,12 +24,12 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
isGeneratingQuery: false,
isGeneratingExplanation: false,
isExecuting: false,
dislikeQuery: undefined as boolean,
dislikeQuery: undefined,
showCallout: false,
showSamplePrompts: false,
queryIterator: undefined as MinimalQueryIterator,
queryResults: undefined as QueryResults,
errors: [] as QueryError[],
queryIterator: undefined,
queryResults: undefined,
errors: [],
isSamplePromptsOpen: false,
showPromptTeachingBubble: true,
showDeletePopup: false,
@@ -43,7 +41,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
wasCopilotUsed: false,
showWelcomeSidebar: true,
showCopilotSidebar: false,
chatMessages: [] as CopilotMessage[],
chatMessages: [],
shouldIncludeInMessages: true,
showExplanationBubble: false,
isAllocatingContainer: false,
@@ -88,7 +86,7 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
},
resetQueryCopilotStates: () => {
set((state: QueryCopilotState) => ({
set((state) => ({
...state,
generatedQuery: "",
likeQuery: false,
@@ -101,11 +99,11 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
isGeneratingQuery: false,
isGeneratingExplanation: false,
isExecuting: false,
dislikeQuery: undefined as boolean,
dislikeQuery: undefined,
showCallout: false,
showSamplePrompts: false,
queryIterator: undefined as MinimalQueryIterator,
queryResults: undefined as QueryResults,
queryIterator: undefined,
queryResults: undefined,
errorMessage: "",
isSamplePromptsOpen: false,
showPromptTeachingBubble: true,
@@ -117,19 +115,19 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
generatedQueryComments: "",
wasCopilotUsed: false,
showCopilotSidebar: false,
chatMessages: [] as CopilotMessage[],
chatMessages: [],
shouldIncludeInMessages: true,
showExplanationBubble: false,
notebookServerInfo: {
notebookServerEndpoint: undefined,
authToken: undefined,
forwardingId: undefined,
} as DataModels.NotebookWorkspaceConnectionInfo,
},
containerStatus: {
status: undefined,
durationLeftInMinutes: undefined,
phoenixServerInfo: undefined,
} as DataModels.ContainerInfo,
},
isAllocatingContainer: false,
}));
},
@@ -139,4 +137,3 @@ const CopilotProvider = ({ children }: { children: React.ReactNode }): JSX.Eleme
};
export { CopilotProvider, useCopilotStore };

View File

@@ -1,9 +1,11 @@
/* eslint-disable no-console */
import { Stack } from "@fluentui/react";
import { QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotProps } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
@@ -11,6 +13,7 @@ import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotRe
import { userContext } from "UserContext";
import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel";
import { ReactTabKind, TabsState, useTabs } from "hooks/useTabs";
import React, { useState } from "react";
import SplitterLayout from "react-splitter-layout";
import QueryCommandIcon from "../../../images/CopilotCommand.svg";
@@ -23,8 +26,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
const [copilotActive, setCopilotActive] = useState<boolean>(() =>
readCopilotToggleStatus(userContext.databaseAccount),
);
//TODO: Uncomment this useState when query copilot is reinstated in DE
// const [tabActive, setTabActive] = useState<boolean>(true);
const [tabActive, setTabActive] = useState<boolean>(true);
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
@@ -68,18 +70,17 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
useCommandBar.getState().setContextButtons(getCommandbarButtons());
}, [query, selectedQuery, copilotActive]);
//TODO: Uncomment this effect when query copilot is reinstated in DE
// React.useEffect(() => {
// return () => {
// useTabs.subscribe((state: TabsState) => {
// if (state.activeReactTab === ReactTabKind.QueryCopilot) {
// setTabActive(true);
// } else {
// setTabActive(false);
// }
// });
// };
// }, []);
React.useEffect(() => {
return () => {
useTabs.subscribe((state: TabsState) => {
if (state.activeReactTab === ReactTabKind.QueryCopilot) {
setTabActive(true);
} else {
setTabActive(false);
}
});
};
}, []);
const toggleCopilot = (toggle: boolean) => {
setCopilotActive(toggle);
@@ -89,7 +90,6 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
return (
<Stack className="tab-pane" style={{ width: "100%" }}>
<div style={isGeneratingQuery ? { height: "100%" } : { overflowY: "auto", height: "100%" }}>
{/*TODO: Uncomment this section when query copilot is reinstated in DE
{tabActive && copilotActive && (
<QueryCopilotPromptbar
explorer={explorer}
@@ -97,7 +97,7 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
databaseId={QueryCopilotSampleDatabaseId}
containerId={QueryCopilotSampleContainerId}
></QueryCopilotPromptbar>
)} */}
)}
<Stack className="tabPaneContentContainer">
<SplitterLayout percentage={true} vertical={true} primaryIndex={0} primaryMinSize={30} secondaryMinSize={70}>
<EditorReact

View File

@@ -1,8 +1,8 @@
/**
* Accordion top class
*/
import { Link, makeStyles, tokens } from "@fluentui/react-components";
import { DocumentAddRegular, LinkMultipleRegular, OpenRegular } from "@fluentui/react-icons";
import { makeStyles, tokens } from "@fluentui/react-components";
import { DocumentAddRegular, LinkMultipleRegular } from "@fluentui/react-icons";
import { SampleDataImportDialog } from "Explorer/SplashScreen/SampleDataImportDialog";
import { CosmosFluentProvider } from "Explorer/Theme/ThemeUtil";
import { isFabricNative, isFabricNativeReadOnly } from "Platform/Fabric/FabricUtil";
@@ -119,7 +119,7 @@ const FabricHomeScreenButton: React.FC<FabricHomeScreenButtonProps & { className
}) => {
const styles = useStyles();
return (
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick} tabIndex={0}>
<div role="button" className={`${styles.buttonContainer} ${className}`} onClick={onClick}>
<div className={styles.buttonUpperPart}>{icon}</div>
<div aria-label={title} className={styles.buttonLowerPart}>
<div>{title}</div>
@@ -147,7 +147,7 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
{
title: "Sample data",
description: "Automatically load sample data in your database",
icon: <img src={CosmosDbBlackIcon} alt={"Azure Cosmos DB icon"} aria-hidden="true" />,
icon: <img src={CosmosDbBlackIcon} />,
onClick: () => setOpenSampleDataImportDialog(true),
},
{
@@ -181,18 +181,16 @@ export const FabricHomeScreen: React.FC<SplashScreenProps> = (props: SplashScree
explorer={props.explorer}
databaseName={userContext.fabricContext?.databaseName}
/>
<div className={styles.title} role="heading" aria-label={title} aria-level={1}>
<div className={styles.title} role="heading" aria-label={title}>
{title}
</div>
{getSplashScreenButtons()}
{
<div className={styles.footer}>
Need help?{" "}
<Link href="https://learn.microsoft.com/fabric/database/cosmos-db/overview" target="_blank">
Learn more <OpenRegular />
</Link>
</div>
}
{/* <div className={styles.footer}>
Need help?{" "}
<Link href="https://aka.ms/cosmosdbfabricdocs" target="_blank">
Learn more <img src={LinkIcon} alt="Learn more" />
</Link>
</div> */}
</CosmosFluentProvider>
</>
);

View File

@@ -24,7 +24,6 @@ import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react";
import ConnectIcon from "../../../images/Connect_color.svg";
import ContainersIcon from "../../../images/Containers.svg";
import CosmosDBIcon from "../../../images/CosmosDB-logo.svg";
import LinkIcon from "../../../images/Link_blue.svg";
import PowerShellIcon from "../../../images/PowerShell.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
@@ -77,39 +76,39 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
this.subscriptions.push(
{
dispose: useNotebook.subscribe(
(state) => state.isNotebookEnabled,
() => this.setState({}),
(state) => state.isNotebookEnabled,
),
},
{ dispose: useSelectedNode.subscribe(() => this.setState({})) },
{
dispose: useCarousel.subscribe(
() => this.setState({}),
(state) => state.showCoachMark,
() => this.setState({}),
),
},
{
dispose: usePostgres.subscribe(
() => this.setState({}),
(state) => state.showPostgreTeachingBubble,
() => this.setState({}),
),
},
{
dispose: usePostgres.subscribe(
(state) => state.showResetPasswordBubble,
() => this.setState({}),
(state) => state.showResetPasswordBubble,
),
},
{
dispose: useDatabases.subscribe(
(state) => state.sampleDataResourceTokenCollection,
() => this.setState({}),
(state) => state.sampleDataResourceTokenCollection,
),
},
{
dispose: useQueryCopilot.subscribe(
(state) => state.copilotEnabled,
() => this.setState({}),
(state) => state.copilotEnabled,
),
},
);
@@ -121,7 +120,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
};
private getSplashScreenButtons = (): JSX.Element => {
if (userContext.apiType === "SQL") {
if (
userContext.apiType === "SQL" &&
useQueryCopilot.getState().copilotEnabled &&
useDatabases.getState().sampleDataResourceTokenCollection
) {
return (
<Stack
className="splashStackContainer"
@@ -149,18 +152,25 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
/>
</Stack>
<Stack className="splashStackRow" horizontal>
<SplashScreenButton
imgSrc={CosmosDBIcon}
imgSize={35}
title={"Azure Cosmos DB Samples Gallery"}
description={
"Discover samples that showcase scalable, intelligent app patterns. Try one now to see how fast you can go from concept to code with Cosmos DB"
}
onClick={() => {
window.open("https://azurecosmosdb.github.io/gallery/?tags=example", "_blank");
traceOpen(Action.LearningResourcesClicked, { apiType: userContext.apiType });
}}
/>
{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 });
}}
/>
)}
<SplashScreenButton
imgSrc={ConnectIcon}
title={"Connect"}
@@ -202,7 +212,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
sample data, query.
</TeachingBubble>
)}
{/*TODO: convert below to use SplashScreenButton */}
{mainItems.map((item) => (
<Stack
id={`mainButton-${item.id}`}
@@ -468,34 +477,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
};
}
//TODO: Re-enable lint rule when query copilot is reinstated in DE
/* eslint-disable-next-line no-unused-vars */
private getQueryCopilotCard = (): JSX.Element => {
return (
<>
{useQueryCopilot.getState().copilotEnabled && (
<SplashScreenButton
imgSrc={CopilotIcon}
title={"Query faster with Query Advisor"}
description={
"Query Advisor is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
}
onClick={() => {
const copilotVersion = userContext.features.copilotVersion;
if (copilotVersion === "v1.0") {
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
} else if (copilotVersion === "v2.0") {
const sampleCollection = useDatabases.getState().sampleDataResourceTokenCollection;
sampleCollection.onNewQueryClick(sampleCollection, undefined);
}
traceOpen(Action.OpenQueryCopilotFromSplashScreen, { apiType: userContext.apiType });
}}
/>
)}
</>
);
};
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
return {
iconSrc: CollectionIcon,

View File

@@ -7,7 +7,6 @@ interface SplashScreenButtonProps {
title: string;
description: string;
onClick: () => void;
imgSize?: number;
}
export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
@@ -15,7 +14,6 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
title,
description,
onClick,
imgSize,
}: SplashScreenButtonProps): JSX.Element => {
return (
<Stack
@@ -41,7 +39,7 @@ export const SplashScreenButton: React.FC<SplashScreenButtonProps> = ({
role="button"
>
<div>
<img src={imgSrc} alt={title} aria-hidden="true" {...(imgSize ? { height: imgSize, width: imgSize } : {})} />
<img src={imgSrc} alt={title} aria-hidden="true" />
</div>
<Stack style={{ marginLeft: 16 }}>
<Text style={{ fontSize: 18, fontWeight: 600 }}>{title}</Text>

View File

@@ -13,7 +13,7 @@ import { updateDocument } from "../../Common/dataAccess/updateDocument";
import { configContext } from "../../ConfigContext";
import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext";
import { getAuthorizationHeader, isDataplaneRbacEnabledForProxyApi } from "../../Utils/AuthorizationUtils";
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
@@ -551,10 +551,6 @@ export class CassandraAPIDataClient extends TableDataClient {
const authorizationHeaderMetadata: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
xhr.setRequestHeader(authorizationHeaderMetadata.header, authorizationHeaderMetadata.token);
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
xhr.setRequestHeader(Constants.HttpHeaders.entraIdToken, userContext.aadToken);
}
return true;
};

View File

@@ -1,7 +1,7 @@
import { FitAddon } from "@xterm/addon-fit";
import { Terminal } from "@xterm/xterm";
import "@xterm/xterm/css/xterm.css";
import React, { useEffect, useRef } from "react";
import "xterm/css/xterm.css";
import { DatabaseAccount } from "../../../Contracts/DataModels";
import { TerminalKind } from "../../../Contracts/ViewModels";
import { startCloudShellTerminal } from "./CloudShellTerminalCore";

View File

@@ -22,7 +22,6 @@ import { formatErrorMessage, formatInfoMessage, formatWarningMessage } from "./U
// Constants
const DEFAULT_CLOUDSHELL_REGION = "westus";
const DEFAULT_FAIRFAX_CLOUDSHELL_REGION = "usgovvirginia";
const POLLING_INTERVAL_MS = 2000;
const MAX_RETRY_COUNT = 10;
const MAX_PING_COUNT = 120 * 60; // 120 minutes (60 seconds/minute)
@@ -45,26 +44,32 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter
resolvedRegion = determineCloudShellRegion();
resolvedRegion = determineCloudShellRegion();
terminal.writeln(formatWarningMessage("⚠️ IMPORTANT: Azure Cloud Shell Region Notice ⚠️"));
terminal.writeln(
formatInfoMessage(
"The Cloud Shell environment will operate in a region that may differ from your database's region.",
),
);
terminal.writeln(formatInfoMessage("By using this feature, you acknowledge and agree to the following"));
terminal.writeln(formatInfoMessage("This has two potential implications:"));
terminal.writeln(formatInfoMessage("1. Performance Impact:"));
terminal.writeln(
formatInfoMessage(" Commands may experience higher latency due to geographic distance between regions."),
);
terminal.writeln(formatInfoMessage("2. Data Transfers:"));
terminal.writeln(formatInfoMessage("2. Data Compliance Considerations:"));
terminal.writeln(
formatInfoMessage(
" Data processed through this Cloud Shell service can be processed outside of your tenant's geographical region, compliance boundary or national cloud instance.",
" Data processed through this shell could temporarily reside in a different geographic region,",
),
);
terminal.writeln(
formatInfoMessage(" which may affect compliance with data residency requirements or regulations specific"),
);
terminal.writeln(formatInfoMessage(" to your organization."));
terminal.writeln("");
terminal.writeln("\x1b[94mFor more information on Azure Cosmos DB data residency, please visit:");
terminal.writeln("\x1b[94mFor more information on Azure Cosmos DB data governance and compliance, please visit:");
terminal.writeln("\x1b[94mhttps://learn.microsoft.com/en-us/azure/cosmos-db/data-residency\x1b[0m");
// Ask for user consent for region
@@ -154,9 +159,7 @@ export const ensureCloudShellProviderRegistered = async (): Promise<void> => {
* Determines the appropriate CloudShell region
*/
export const determineCloudShellRegion = (): string => {
const defaultRegion =
userContext.portalEnv === "fairfax" ? DEFAULT_FAIRFAX_CLOUDSHELL_REGION : DEFAULT_CLOUDSHELL_REGION;
return getNormalizedRegion(userContext.databaseAccount?.location, defaultRegion);
return getNormalizedRegion(userContext.databaseAccount?.location, DEFAULT_CLOUDSHELL_REGION);
};
/**

View File

@@ -258,7 +258,14 @@ Key limitations:
### Data Residency
Data residency requirements may not be fully satisfied when using CloudShell due to limited regional availability.
Data residency requirements may not be fully satisfied when using CloudShell due to limited regional availability. CloudShell services are currently available in the following regions:
| Geography | Regions |
|-----------|---------|
| Americas | East US, West US 2, South Central US, West Central US |
| Europe | West Europe, North Europe |
| Asia Pacific | Southeast Asia, Japan East, Australia East |
| Middle East | UAE North |
**Note:** For up-to-date supported regions, refer to the region configuration in:
`src/Explorer/CloudShell/Configuration/RegionConfig.ts`

View File

@@ -1,4 +1,4 @@
import { AbstractShellHandler, DISABLE_HISTORY, EXIT_COMMAND, START_MARKER } from "./AbstractShellHandler";
import { AbstractShellHandler, DISABLE_HISTORY, START_MARKER, EXIT_COMMAND } from "./AbstractShellHandler";
// Mock implementation for testing
class MockShellHandler extends AbstractShellHandler {
@@ -18,8 +18,8 @@ class MockShellHandler extends AbstractShellHandler {
return "mock-endpoint";
}
getTerminalSuppressedData(): string[] {
return ["suppressed-data"];
getTerminalSuppressedData(): string {
return "suppressed-data";
}
}
@@ -90,7 +90,7 @@ describe("AbstractShellHandler", () => {
});
it("should return the terminal suppressed data", () => {
expect(shellHandler.getTerminalSuppressedData()).toEqual(["suppressed-data"]);
expect(shellHandler.getTerminalSuppressedData()).toBe("suppressed-data");
});
});
});

View File

@@ -13,18 +13,7 @@ export const DISABLE_HISTORY = `set +o history`;
* Command that displays an error message and exits the shell session.
* Used when shell initialization or connection fails.
*/
export const EXIT_COMMAND = ` printf "\\033[1;31mSession ended. Please close this tab and initiate a new shell session if needed.\\033[0m\\n" && 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()'`;
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`;
/**
* Abstract class that defines the interface for shell-specific handlers
@@ -42,16 +31,7 @@ export abstract class AbstractShellHandler {
abstract getShellName(): string;
abstract getSetUpCommands(): string[];
abstract getConnectionCommand(): 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;
}
abstract getTerminalSuppressedData(): string;
/**
* Constructs the complete initialization command sequence for the shell.
@@ -77,7 +57,7 @@ export abstract class AbstractShellHandler {
START_MARKER,
DISABLE_HISTORY,
...setupCommands,
`{ ${connectionCommand}; } || true;${this.getExitCommand()}`,
`{ ${connectionCommand}; } || true;${EXIT_COMMAND}`,
];
return allCommands.join("\n").concat("\n");
@@ -97,7 +77,7 @@ export abstract class AbstractShellHandler {
* is not already present in the environment.
*/
protected mongoShellSetupCommands(): string[] {
const PACKAGE_VERSION: string = "2.5.6";
const PACKAGE_VERSION: string = "2.5.0";
return [
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
@@ -105,7 +85,7 @@ export abstract class AbstractShellHandler {
`if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh/bin && mv mongosh-${PACKAGE_VERSION}-linux-x64/bin/mongosh ~/mongosh/bin/ && chmod +x ~/mongosh/bin/mongosh; fi`,
`if ! command -v mongosh &> /dev/null; then rm -rf mongosh-${PACKAGE_VERSION}-linux-x64 mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
"if ! command -v mongosh &> /dev/null; then source ~/.bashrc; fi",
"source ~/.bashrc",
];
}
}

View File

@@ -94,7 +94,7 @@ describe("CassandraShellHandler", () => {
});
test("should return the correct terminal suppressed data", () => {
expect(handler.getTerminalSuppressedData()).toEqual([""]);
expect(handler.getTerminalSuppressedData()).toBe("");
});
test("should include the correct package version in setup commands", () => {

View File

@@ -41,7 +41,7 @@ export class CassandraShellHandler extends AbstractShellHandler {
return `cqlsh ${getHostFromUrl(this._endpoint)} 10350 -u ${dbName} -p ${this._key} --ssl`;
}
public getTerminalSuppressedData(): string[] {
return [""];
public getTerminalSuppressedData(): string {
return "";
}
}

View File

@@ -18,12 +18,6 @@ interface DatabaseAccount {
interface UserContextType {
databaseAccount: DatabaseAccount;
features: {
enableAadDataPlane: boolean;
};
apiType: string;
dataPlaneRbacEnabled: boolean;
aadToken?: string;
}
// Mock dependencies
@@ -35,13 +29,10 @@ jest.mock("../../../../UserContext", () => ({
mongoEndpoint: "https://test-mongo.documents.azure.com:443/",
},
},
features: { enableAadDataPlane: false },
apiType: "Mongo",
},
}));
jest.mock("../Utils/CommonUtils", () => ({
...jest.requireActual("../Utils/CommonUtils"),
getHostFromUrl: jest.fn().mockReturnValue("test-mongo.documents.azure.com"),
}));
@@ -78,7 +69,7 @@ describe("MongoShellHandler", () => {
expect(Array.isArray(commands)).toBe(true);
expect(commands.length).toBe(7);
expect(commands[1]).toContain("mongosh-2.5.6-linux-x64.tgz");
expect(commands[1]).toContain("mongosh-2.5.0-linux-x64.tgz");
});
});
@@ -96,12 +87,11 @@ describe("MongoShellHandler", () => {
kind: "test-kind",
properties: { mongoEndpoint: "https://test-mongo.documents.azure.com:443/" },
};
(userContext as UserContextType).dataPlaneRbacEnabled = false;
const command = mongoShellHandler.getConnectionCommand();
expect(command).toBe(
"mongosh --nodb --quiet --eval 'disableTelemetry()'; mongosh mongodb://test-mongo.documents.azure.com:10255?appName=CosmosExplorerTerminal --username test-account --password test-key --tls --tlsAllowInvalidCertificates",
"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/");
@@ -124,55 +114,17 @@ describe("MongoShellHandler", () => {
};
const command = mongoShellHandler.getConnectionCommand();
expect(command).toBe("echo 'Database name not found.'");
// Restore original
(userContext as UserContextType).databaseAccount = originalDatabaseAccount;
});
it("should return echo if endpoint is missing", () => {
const testKey = "test-key";
(userContext as UserContextType).databaseAccount = {
id: "test-id",
name: "", // Empty name to simulate missing name
location: "test-location",
type: "test-type",
kind: "test-kind",
properties: { mongoEndpoint: "" },
};
const mongoShellHandler = new MongoShellHandler(testKey);
const command = mongoShellHandler.getConnectionCommand();
expect(command).toBe("echo 'MongoDB endpoint not found.'");
});
it("should use _getAadConnectionCommand when _isEntraIdEnabled is true", () => {
const testKey = "aad-key";
(userContext as UserContextType).databaseAccount = {
id: "test-id",
name: "test-account",
location: "test-location",
type: "test-type",
kind: "test-kind",
properties: { mongoEndpoint: "https://test-mongo.documents.azure.com:443/" },
};
(userContext as UserContextType).dataPlaneRbacEnabled = true;
const mongoShellHandler = new MongoShellHandler(testKey);
const command = mongoShellHandler.getConnectionCommand();
expect(command).toContain(
"mongosh 'mongodb://test-account:aad-key@test-account.mongo.cosmos.azure.com:10255/?ssl=true&replicaSet=globaldb&authMechanism=PLAIN&retryWrites=false' --tls --tlsAllowInvalidCertificates",
);
expect(command.startsWith("mongosh --nodb")).toBeTruthy();
});
});
describe("getTerminalSuppressedData", () => {
it("should return the correct warning message", () => {
expect(mongoShellHandler.getTerminalSuppressedData()).toEqual([
"Warning: Non-Genuine MongoDB Detected",
"Telemetry is now disabled.",
]);
expect(mongoShellHandler.getTerminalSuppressedData()).toBe("Warning: Non-Genuine MongoDB Detected");
});
});
});

View File

@@ -1,29 +1,16 @@
import { userContext } from "../../../../UserContext";
import { isDataplaneRbacEnabledForProxyApi } from "../../../../Utils/AuthorizationUtils";
import { filterAndCleanTerminalOutput, getHostFromUrl, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND, EXIT_COMMAND_MONGO } from "./AbstractShellHandler";
import { getHostFromUrl } from "../Utils/CommonUtils";
import { AbstractShellHandler } from "./AbstractShellHandler";
export class MongoShellHandler extends AbstractShellHandler {
private _key: string;
private _endpoint: string | undefined;
private _removeInfoText: string[] = getMongoShellRemoveInfoText();
private _isEntraIdEnabled: boolean = isDataplaneRbacEnabledForProxyApi(userContext);
constructor(private key: string) {
super();
this._key = key;
this._endpoint = userContext?.databaseAccount?.properties?.mongoEndpoint;
}
private _getKeyConnectionCommand(dbName: string): string {
return `mongosh mongodb://${getHostFromUrl(this._endpoint)}:10255?appName=${
this.APP_NAME
} --username ${dbName} --password ${this._key} --tls --tlsAllowInvalidCertificates`;
}
private _getAadConnectionCommand(dbName: string): string {
return `mongosh 'mongodb://${dbName}:${this._key}@${dbName}.mongo.cosmos.azure.com:10255/?ssl=true&replicaSet=globaldb&authMechanism=PLAIN&retryWrites=false' --tls --tlsAllowInvalidCertificates`;
}
public getShellName(): string {
return "MongoDB";
}
@@ -41,22 +28,20 @@ export class MongoShellHandler extends AbstractShellHandler {
if (!dbName) {
return "echo 'Database name not found.'";
}
const connectionCommand = this._isEntraIdEnabled
? this._getAadConnectionCommand(dbName)
: this._getKeyConnectionCommand(dbName);
const fullCommand = `${DISABLE_TELEMETRY_COMMAND}; ${connectionCommand}`;
return fullCommand;
return (
"mongosh mongodb://" +
getHostFromUrl(this._endpoint) +
":10255?appName=" +
this.APP_NAME +
" --username " +
dbName +
" --password " +
this._key +
" --tls --tlsAllowInvalidCertificates"
);
}
public getTerminalSuppressedData(): string[] {
return ["Warning: Non-Genuine MongoDB Detected", "Telemetry is now disabled."];
}
protected getExitCommand(): string {
return EXIT_COMMAND_MONGO;
}
updateTerminalData(data: string): string {
return filterAndCleanTerminalOutput(data, this._removeInfoText);
public getTerminalSuppressedData(): string {
return "Warning: Non-Genuine MongoDB Detected";
}
}

View File

@@ -58,7 +58,7 @@ describe("PostgresShellHandler", () => {
});
it("should return empty string for terminal suppressed data", () => {
expect(postgresShellHandler.getTerminalSuppressedData()).toEqual([""]);
expect(postgresShellHandler.getTerminalSuppressedData()).toBe("");
});
});
});

View File

@@ -57,7 +57,7 @@ export class PostgresShellHandler extends AbstractShellHandler {
return `psql -h "${this._endpoint}" -p 5432 -d "citus" -U "${loginName}" --set=sslmode=require --set=application_name=${this.APP_NAME}`;
}
public getTerminalSuppressedData(): string[] {
return [""];
public getTerminalSuppressedData(): string {
return "";
}
}

View File

@@ -7,24 +7,12 @@ import { PostgresShellHandler } from "./PostgresShellHandler";
import { getHandler, getKey } from "./ShellTypeFactory";
import { VCoreMongoShellHandler } from "./VCoreMongoShellHandler";
interface UserContextType {
databaseAccount: { name: string };
subscriptionId: string;
resourceGroup: string;
features: { enableAadDataPlane: boolean };
dataPlaneRbacEnabled: boolean;
aadToken?: string;
apiType?: string;
}
// Mock dependencies
jest.mock("../../../../UserContext", () => ({
userContext: {
databaseAccount: { name: "testDbName" },
subscriptionId: "testSubId",
resourceGroup: "testResourceGroup",
features: { enableAadDataPlane: false },
dataPlaneRbacEnabled: false,
},
}));
@@ -121,33 +109,5 @@ describe("ShellTypeHandlerFactory", () => {
expect(key).toBe(mockKey);
expect(listKeys).toHaveBeenCalledWith("testSubId", "testResourceGroup", "testDbName");
});
it("should return MongoShellHandler with primaryMasterKey for TerminalKind.Mongo when RBAC is disabled", async () => {
(listKeys as jest.Mock).mockResolvedValue({ primaryMasterKey: "primaryKey123" });
(userContext as UserContextType).features.enableAadDataPlane = false;
(userContext as UserContextType).dataPlaneRbacEnabled = false;
const handler = await getHandler(TerminalKind.Mongo);
expect(handler).toBeInstanceOf(MongoShellHandler);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(handler.key).toBe("primaryKey123");
});
it("should return MongoShellHandler with aadToken for TerminalKind.Mongo when RBAC is enabled", async () => {
(userContext as UserContextType).aadToken = "aadToken123";
(userContext as UserContextType).features.enableAadDataPlane = true;
(userContext as UserContextType).dataPlaneRbacEnabled = true;
(userContext as UserContextType).apiType = "Mongo";
const handler = await getHandler(TerminalKind.Mongo);
expect(handler).toBeInstanceOf(MongoShellHandler);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(handler.key).toBe("aadToken123");
});
it("should throw error for unsupported shell type", async () => {
await expect(getHandler("UnknownShell" as unknown as TerminalKind)).rejects.toThrow(
"Unsupported shell type: UnknownShell",
);
});
});
});

View File

@@ -1,7 +1,6 @@
import { TerminalKind } from "../../../../Contracts/ViewModels";
import { userContext } from "../../../../UserContext";
import { listKeys } from "../../../../Utils/arm/generatedClients/cosmos/databaseAccounts";
import { isDataplaneRbacEnabledForProxyApi } from "../../../../Utils/AuthorizationUtils";
import { AbstractShellHandler } from "./AbstractShellHandler";
import { CassandraShellHandler } from "./CassandraShellHandler";
import { MongoShellHandler } from "./MongoShellHandler";
@@ -31,9 +30,6 @@ export async function getKey(): Promise<string> {
if (!dbName) {
return "";
}
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
return userContext.aadToken || "";
}
const keys = await listKeys(userContext.subscriptionId, userContext.resourceGroup, dbName);
return keys?.primaryMasterKey || "";

View File

@@ -45,7 +45,7 @@ describe("VCoreMongoShellHandler", () => {
expect(Array.isArray(commands)).toBe(true);
expect(commands.length).toBe(7);
expect(commands[1]).toContain("mongosh-2.5.6-linux-x64.tgz");
expect(commands[1]).toContain("mongosh-2.5.0-linux-x64.tgz");
expect(commands[0]).toContain("mongosh not found");
});
@@ -57,10 +57,7 @@ describe("VCoreMongoShellHandler", () => {
});
it("should return the correct terminal suppressed data", () => {
expect(vcoreMongoShellHandler.getTerminalSuppressedData()).toEqual([
"Warning: Non-Genuine MongoDB Detected",
"Telemetry is now disabled.",
]);
expect(vcoreMongoShellHandler.getTerminalSuppressedData()).toBe("Warning: Non-Genuine MongoDB Detected");
});
});
});

View File

@@ -1,10 +1,8 @@
import { userContext } from "../../../../UserContext";
import { filterAndCleanTerminalOutput, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND, EXIT_COMMAND_MONGO } from "./AbstractShellHandler";
import { AbstractShellHandler } from "./AbstractShellHandler";
export class VCoreMongoShellHandler extends AbstractShellHandler {
private _endpoint: string | undefined;
private _removeInfoText: string[] = getMongoShellRemoveInfoText();
constructor() {
super();
@@ -25,24 +23,10 @@ export class VCoreMongoShellHandler extends AbstractShellHandler {
}
const userName = userContext.vcoreMongoConnectionParams.adminLogin;
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}"`;
return `mongosh "mongodb+srv://${userName}:@${this._endpoint}/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000&appName=${this.APP_NAME}"`;
}
public getTerminalSuppressedData(): string[] {
return ["Warning: Non-Genuine MongoDB Detected", "Telemetry is now disabled."];
}
/**
* Override getExitCommand to include MongoDB networking guidance
*/
protected getExitCommand(): string {
return EXIT_COMMAND_MONGO;
}
updateTerminalData(data: string): string {
return filterAndCleanTerminalOutput(data, this._removeInfoText);
public getTerminalSuppressedData(): string {
return "Warning: Non-Genuine MongoDB Detected";
}
}

View File

@@ -92,18 +92,6 @@ export class AttachAddon implements ITerminalAddon {
* @param {Terminal} terminal - The XTerm terminal instance
*/
public addMessageListener(terminal: Terminal): void {
let messageBuffer = "";
let bufferTimeout: NodeJS.Timeout | null = null;
const BUFFER_TIMEOUT = 50; // ms - short timeout for prompt detection
const processBuffer = () => {
if (messageBuffer.length > 0) {
this.handleCompleteTerminalData(terminal, messageBuffer);
messageBuffer = "";
}
bufferTimeout = null;
};
this._disposables.push(
addSocketListener(this._socket, "message", (ev) => {
let data: ArrayBuffer | string = ev.data;
@@ -115,136 +103,51 @@ export class AttachAddon implements ITerminalAddon {
data = enc.decode(ev.data as ArrayBuffer);
}
// Handle status messages
let processedStatusData = data;
// Process status messages with delimiters
// eslint-disable-next-line no-constant-condition
while (true) {
const startIndex = processedStatusData.indexOf(startStatusJson);
if (startIndex === -1) {
break;
}
const afterStart = processedStatusData.substring(startIndex + startStatusJson.length);
const endIndex = afterStart.indexOf(endStatusJson);
if (endIndex === -1) {
// Incomplete status message
this._socketData += processedStatusData.substring(startIndex);
processedStatusData = processedStatusData.substring(0, startIndex);
break;
}
// Remove processed status message
processedStatusData =
processedStatusData.substring(0, startIndex) + afterStart.substring(endIndex + endStatusJson.length);
// for example of json object look in TerminalHelper in the socket.onMessage
if (data.includes(startStatusJson) && data.includes(endStatusJson)) {
// process as one line
const statusData = data.split(startStatusJson)[1].split(endStatusJson)[0];
data = data.replace(statusData, "");
data = data.replace(startStatusJson, "");
data = data.replace(endStatusJson, "");
} else if (data.includes(startStatusJson)) {
// check for start
const partialStatusData = data.split(startStatusJson)[1];
this._socketData += partialStatusData;
data = data.replace(partialStatusData, "");
data = data.replace(startStatusJson, "");
} else if (data.includes(endStatusJson)) {
// check for end and process the command
const partialStatusData = data.split(endStatusJson)[0];
this._socketData += partialStatusData;
data = data.replace(partialStatusData, "");
data = data.replace(endStatusJson, "");
this._socketData = "";
} else if (this._socketData.length > 0) {
// check if the line is all data then just concatenate
this._socketData += data;
data = "";
}
// Add to message buffer
messageBuffer += processedStatusData;
// Clear existing timeout
if (bufferTimeout) {
clearTimeout(bufferTimeout);
bufferTimeout = null;
if (this._allowTerminalWrite && data.includes(this._startMarker)) {
this._allowTerminalWrite = false;
terminal.write(`Preparing ${this._shellHandler.getShellName()} environment...\r\n`);
}
// Check if this looks like a complete message/command
const isComplete = this.isMessageComplete(messageBuffer, processedStatusData);
if (this._allowTerminalWrite) {
const suppressedData = this._shellHandler?.getTerminalSuppressedData();
const hasSuppressedData = suppressedData && suppressedData.length > 0;
if (isComplete) {
// Message marked as complete, processing immediately
processBuffer();
} else {
// Set timeout to process buffer after delay
bufferTimeout = setTimeout(processBuffer, BUFFER_TIMEOUT);
if (!hasSuppressedData || !data.includes(suppressedData)) {
terminal.write(data);
}
}
if (data.includes(this._shellHandler.getConnectionCommand())) {
this._allowTerminalWrite = true;
}
}),
);
// 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 {

View File

@@ -50,34 +50,3 @@ export const getShellNameForDisplay = (terminalKind: TerminalKind): string => {
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");
};

View File

@@ -7,8 +7,7 @@ const validCloudShellRegions = new Set([
"westeurope",
"centralindia",
"southeastasia",
"usgovvirginia",
"usgovarizona",
"westcentralus",
]);
/**
@@ -40,6 +39,7 @@ export const getNormalizedRegion = (region: string, defaultCloudshellRegion: str
}
const regionMap: Record<string, string> = {
centralus: "westcentralus",
eastus2: "eastus",
};

View File

@@ -146,16 +146,10 @@ describe("Documents tab (Mongo API)", () => {
updateConfigContext({ platform: Platform.Hosted });
const props: IDocumentsTabComponentProps = createMockProps();
wrapper = mount(<DocumentsTabComponent {...props} />);
// 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);
wrapper = await waitForComponentToPaint(wrapper);
});
afterEach(() => {
wrapper.unmount();

View File

@@ -45,7 +45,7 @@ export interface IGraphConfig {
interface GraphTabOptions extends ViewModels.TabOptions {
account: DatabaseAccount;
password: string;
masterKey: string;
collectionId: string;
databaseId: string;
collectionPartitionKeyProperty: string;
@@ -107,7 +107,7 @@ export default class GraphTab extends TabsBase {
graphBackendEndpoint: GraphTab.getGremlinEndpoint(options.account),
databaseId: options.databaseId,
collectionId: options.collectionId,
password: options.password,
masterKey: options.masterKey,
onLoadStartKey: options.onLoadStartKey,
onLoadStartKeyChange: (onLoadStartKey: number): void => {
if (onLoadStartKey === undefined) {

View File

@@ -44,8 +44,8 @@ export default class NotebookTabV2 extends NotebookTabBase {
this.container = options.container;
this.notebookPath = ko.observable(options.notebookContentItem.path);
useNotebook.subscribe(
(state) => state.notebookServerInfo,
() => logConsoleInfo("New notebook server info received."),
(state) => state.notebookServerInfo,
);
this.notebookComponentAdapter = new NotebookComponentAdapter({
contentItem: options.notebookContentItem,
@@ -165,7 +165,7 @@ export default class NotebookTabV2 extends NotebookTabBase {
{
iconSrc: null,
iconAlt: kernelLabel,
onCommandClick: () => { },
onCommandClick: () => {},
commandButtonLabel: null,
hasPopup: false,
disabled: availableKernels.length < 1,
@@ -276,7 +276,7 @@ export default class NotebookTabV2 extends NotebookTabBase {
{
iconSrc: null,
iconAlt: null,
onCommandClick: () => { },
onCommandClick: () => {},
commandButtonLabel: null,
ariaLabel: cellTypeLabel,
hasPopup: false,

View File

@@ -106,6 +106,6 @@ describe("QueryTabComponent", () => {
<QueryTabCopilotComponent {...propsMock} />
</CopilotProvider>,
);
expect(container.find(QueryCopilotPromptbar).exists()).toBe(false);
expect(container.find(QueryCopilotPromptbar).exists()).toBe(true);
});
});

View File

@@ -9,6 +9,7 @@ import { useDialog } from "Explorer/Controls/Dialog";
import { monaco } from "Explorer/LazyMonaco";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
@@ -27,9 +28,8 @@ import { TabsState, useTabs } from "hooks/useTabs";
import React, { Fragment, createRef } from "react";
import "react-splitter-layout/lib/index.css";
import { format } from "react-string-format";
//TODO: Uncomment next two lines when query copilot is reinstated in DE
// import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
// 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 CancelQueryIcon from "../../../../images/Entity_cancel.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
@@ -55,6 +55,8 @@ import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPa
import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
import TabsBase from "../TabsBase";
import "./QueryTabComponent.less";
import { useQueryMetadataStore } from "./useQueryMetadataStore"; // adjust path if needed
enum ToggleState {
Result,
@@ -196,7 +198,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
visible: true,
};
// const query=this.state.sqlQueryEditorContent;
// const db = this.props.collection.databaseId;
// const container = this.props.collection.id();
const isSaveQueryBtnEnabled = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
this.saveQueryButton = {
enabled: isSaveQueryBtnEnabled,
@@ -260,6 +265,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
}
public onExecuteQueryClick = async (): Promise<void> => {
const query1=this.state.sqlQueryEditorContent;
const db = this.props.collection.databaseId;
const container = this.props.collection.id();
useQueryMetadataStore.getState().setMetadata(query1, db, container);
this._iterator = undefined;
setTimeout(async () => {
@@ -494,55 +503,53 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
});
}
//TODO: Uncomment next section when query copilot is reinstated in DE
// if (this.launchCopilotButton.visible && this.isCopilotTabActive) {
// const mainButtonLabel = "Launch Copilot";
// const chatPaneLabel = "Open Copilot in chat pane (ALT+C)";
// const copilotSettingLabel = "Copilot settings";
if (this.launchCopilotButton.visible && this.isCopilotTabActive) {
const mainButtonLabel = "Launch Copilot";
const chatPaneLabel = "Open Copilot in chat pane (ALT+C)";
const copilotSettingLabel = "Copilot settings";
// const openCopilotChatButton: CommandButtonComponentProps = {
// iconAlt: chatPaneLabel,
// onCommandClick: this.launchQueryCopilotChat,
// commandButtonLabel: chatPaneLabel,
// ariaLabel: chatPaneLabel,
// hasPopup: false,
// };
const openCopilotChatButton: CommandButtonComponentProps = {
iconAlt: chatPaneLabel,
onCommandClick: this.launchQueryCopilotChat,
commandButtonLabel: chatPaneLabel,
ariaLabel: chatPaneLabel,
hasPopup: false,
};
// const copilotSettingsButton: CommandButtonComponentProps = {
// iconAlt: copilotSettingLabel,
// onCommandClick: () => undefined,
// commandButtonLabel: copilotSettingLabel,
// ariaLabel: copilotSettingLabel,
// hasPopup: false,
// };
const copilotSettingsButton: CommandButtonComponentProps = {
iconAlt: copilotSettingLabel,
onCommandClick: () => undefined,
commandButtonLabel: copilotSettingLabel,
ariaLabel: copilotSettingLabel,
hasPopup: false,
};
// const launchCopilotButton: CommandButtonComponentProps = {
// iconSrc: LaunchCopilot,
// iconAlt: mainButtonLabel,
// onCommandClick: this.launchQueryCopilotChat,
// commandButtonLabel: mainButtonLabel,
// ariaLabel: mainButtonLabel,
// hasPopup: false,
// children: [openCopilotChatButton, copilotSettingsButton],
// };
// buttons.push(launchCopilotButton);
// }
const launchCopilotButton: CommandButtonComponentProps = {
iconSrc: LaunchCopilot,
iconAlt: mainButtonLabel,
onCommandClick: this.launchQueryCopilotChat,
commandButtonLabel: mainButtonLabel,
ariaLabel: mainButtonLabel,
hasPopup: false,
children: [openCopilotChatButton, copilotSettingsButton],
};
buttons.push(launchCopilotButton);
}
//TODO: Uncomment next section when query copilot is reinstated in DE
// if (this.props.copilotEnabled) {
// const toggleCopilotButton: CommandButtonComponentProps = {
// iconSrc: QueryCommandIcon,
// iconAlt: "Query Advisor",
// keyboardAction: KeyboardAction.TOGGLE_COPILOT,
// 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",
// hasPopup: false,
// };
// buttons.push(toggleCopilotButton);
// }
if (this.props.copilotEnabled) {
const toggleCopilotButton: CommandButtonComponentProps = {
iconSrc: QueryCommandIcon,
iconAlt: "Query Advisor",
keyboardAction: KeyboardAction.TOGGLE_COPILOT,
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",
hasPopup: false,
};
buttons.push(toggleCopilotButton);
}
if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) {
const label = "Cancel query";
@@ -727,7 +734,6 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
return (
<Fragment>
<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 && (
<QueryCopilotPromptbar
explorer={this.props.collection.container}
@@ -735,7 +741,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
databaseId={this.props.collection.databaseId}
containerId={this.props.collection.id()}
></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 */}
<Allotment
key={vertical.toString()}

View File

@@ -1,5 +1,7 @@
import { FontIcon } from "@fluentui/react";
import {
Button,
Checkbox,
DataGrid,
DataGridBody,
DataGridCell,
@@ -8,26 +10,36 @@ import {
DataGridRow,
SelectTabData,
SelectTabEvent,
Spinner,
Tab,
TabList,
Table,
TableBody,
TableCell,
TableColumnDefinition,
createTableColumn,
TableHeader,
TableRow,
createTableColumn
} from "@fluentui/react-components";
import { ArrowDownloadRegular, CopyRegular } from "@fluentui/react-icons";
import { ArrowDownloadRegular, ChevronDown20Regular, ChevronRight20Regular, CircleFilled, CopyRegular } from "@fluentui/react-icons";
import copy from "clipboard-copy";
import { HttpHeaders } from "Common/Constants";
import MongoUtility from "Common/MongoUtility";
import { QueryMetrics } from "Contracts/DataModels";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent";
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
import { useQueryMetadataStore } from "Explorer/Tabs/QueryTab/useQueryMetadataStore";
import React, { useCallback, useEffect, useState } from "react";
import { userContext } from "UserContext";
import copy from "clipboard-copy";
import React, { useCallback, useState } from "react";
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
import { client } from "../../../Common/CosmosClient";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { ResultsViewProps } from "./QueryResultSection";
enum ResultsTabs {
Results = "results",
QueryStats = "queryStats",
IndexAdvisor = "indexadv",
}
const ResultsTab: React.FC<ResultsViewProps> = ({ queryResults, isMongoDB, executeQueryDocumentsPage }) => {
@@ -380,9 +392,8 @@ const QueryStatsTab: React.FC<Pick<ResultsViewProps, "queryResults">> = ({ query
},
{
metric: "User defined function execution time",
value: `${
aggregatedQueryMetrics.runtimeExecutionTimes?.userDefinedFunctionExecutionTime?.toString() || 0
} ms`,
value: `${aggregatedQueryMetrics.runtimeExecutionTimes?.userDefinedFunctionExecutionTime?.toString() || 0
} ms`,
toolTip: "Total time spent executing user-defined functions",
},
{
@@ -523,6 +534,394 @@ const QueryStatsTab: React.FC<Pick<ResultsViewProps, "queryResults">> = ({ query
);
};
interface IIndexMetric {
index: string;
impact: string;
section: "Included" | "Not Included" | "Header";
}
const IndexAdvisorTab: React.FC = () => {
const { userQuery, databaseId, containerId } = useQueryMetadataStore();
const [loading, setLoading] = useState(true);
const [indexMetrics, setIndexMetrics] = useState<any>(null);
const [showIncluded, setShowIncluded] = useState(true);
const [showNotIncluded, setShowNotIncluded] = useState(true);
const [selectedIndexes, setSelectedIndexes] = useState<any[]>([]);
const [selectAll, setSelectAll] = useState(false);
const [updateMessageShown, setUpdateMessageShown] = useState(false);
const [included, setIncludedIndexes] = useState<IIndexMetric[]>([]);
const [notIncluded, setNotIncludedIndexes] = useState<IIndexMetric[]>([]);
useEffect(() => {
async function fetchIndexMetrics() {
const clearMessage = logConsoleProgress(`Querying items with IndexMetrics in container ${containerId}`);
try {
const querySpec = {
query: userQuery || "SELECT TOP 10 c.id FROM c WHERE c.Item = 'value1234' ",
};
const sdkResponse = await client()
.database(databaseId)
.container(containerId)
.items.query(querySpec, {
populateIndexMetrics: true,
})
.fetchAll();
setIndexMetrics(sdkResponse.indexMetrics);
} catch (error) {
handleError(error, "queryItemsWithIndexMetrics", `Error querying items from ${containerId}`);
} finally {
clearMessage();
setLoading(false);
}
}
if (userQuery && databaseId && containerId) {
fetchIndexMetrics();
}
}, [userQuery, databaseId, containerId]);
useEffect(() => {
if (!indexMetrics) return;
const included: any[] = [];
const notIncluded: any[] = [];
const lines = indexMetrics.split("\n").map((line: string) => line.trim()).filter(Boolean);
let currentSection = "";
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith("Utilized Single Indexes") || line.startsWith("Utilized Composite Indexes")) {
currentSection = "included";
} else if (line.startsWith("Potential Single Indexes") || line.startsWith("Potential Composite Indexes")) {
currentSection = "notIncluded";
} else if (line.startsWith("Index Spec:")) {
const index = line.replace("Index Spec:", "").trim();
const impactLine = lines[i + 1];
const impact = impactLine?.includes("Index Impact Score:") ? impactLine.split(":")[1].trim() : "Unknown";
const isComposite = index.includes(",");
const indexObj: any = { index, impact };
if (isComposite) {
indexObj.composite = index.split(",").map((part: string) => {
const [path, order] = part.trim().split(/\s+/);
return {
path: path.trim(),
order: order?.toLowerCase() === "desc" ? "descending" : "ascending",
};
});
} else {
let path = "/unknown/*";
const pathRegex = /\/[^\/\s*?]+(?:\/[^\/\s*?]+)*(\/\*|\?)/;
const match = index.match(pathRegex);
if (match) {
path = match[0];
} else {
const simplePathRegex = /\/[^\/\s]+/;
const simpleMatch = index.match(simplePathRegex);
if (simpleMatch) {
path = simpleMatch[0] + "/*";
}
}
indexObj.path = path;
}
if (currentSection === "included") {
included.push(indexObj);
} else if (currentSection === "notIncluded") {
notIncluded.push(indexObj);
}
}
}
setIncludedIndexes(included);
setNotIncludedIndexes(notIncluded);
}, [indexMetrics]);
useEffect(() => {
const allSelected = notIncluded.length > 0 && notIncluded.every((item) => selectedIndexes.some((s) => s.index === item.index));
setSelectAll(allSelected);
}, [selectedIndexes, notIncluded]);
const handleCheckboxChange = (indexObj: any, checked: boolean) => {
if (checked) {
setSelectedIndexes((prev) => [...prev, indexObj]);
} else {
setSelectedIndexes((prev) =>
prev.filter((item) => item.index !== indexObj.index)
);
}
};
const handleSelectAll = (checked: boolean) => {
setSelectAll(checked);
setSelectedIndexes(checked ? notIncluded : []);
};
const handleUpdatePolicy = async () => {
if (selectedIndexes.length === 0) {
console.log("No indexes selected for update");
return;
}
try {
const { resource: containerDef } = await client()
.database(databaseId)
.container(containerId)
.read();
const newIncludedPaths = selectedIndexes
.filter(index => !index.composite)
.map(index => {
return {
path: index.path,
};
});
const newCompositeIndexes = selectedIndexes
.filter(index => index.composite)
.map(index => index.composite);
const updatedPolicy = {
...containerDef.indexingPolicy,
includedPaths: [
...(containerDef.indexingPolicy?.includedPaths || []),
...newIncludedPaths,
],
compositeIndexes: [
...(containerDef.indexingPolicy?.compositeIndexes || []),
...newCompositeIndexes,
],
};
await client()
.database(databaseId)
.container(containerId)
.replace({
id: containerId,
partitionKey: containerDef.partitionKey,
indexingPolicy: updatedPolicy,
});
const newIncluded = [...included, ...notIncluded.filter(item =>
selectedIndexes.find(s => s.index === item.index)
)];
const newNotIncluded = notIncluded.filter(item =>
!selectedIndexes.find(s => s.index === item.index)
);
setSelectedIndexes([]);
setSelectAll(false);
setIndexMetricsFromParsed(newIncluded, newNotIncluded);
setUpdateMessageShown(true);
} catch (err) {
console.error("Failed to update indexing policy:", err);
}
};
const setIndexMetricsFromParsed = (included: { index: string; impact: string }[], notIncluded: { index: string; impact: string }[]) => {
const serialize = (sectionTitle: string, items: { index: string; impact: string }[], isUtilized: boolean) =>
items.length
? `${sectionTitle}\n` +
items
.map((item) => `Index Spec: ${item.index}\nIndex Impact Score: ${item.impact}`)
.join("\n") + "\n"
: "";
const composedMetrics =
serialize("Utilized Single Indexes", included, true) +
serialize("Potential Single Indexes", notIncluded, false);
setIndexMetrics(composedMetrics.trim());
};
const renderImpactDots = (impact: string) => {
let count = 0;
if (impact === "High") count = 3;
else if (impact === "Medium") count = 2;
else if (impact === "Low") count = 1;
return (
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
{Array.from({ length: count }).map((_, i) => (
<CircleFilled
key={i}
style={{
color: "#0078D4",
fontSize: "12px",
display: "inline-flex",
}}
/>
))}
</div>
);
};
const renderRow = (item: IIndexMetric, index: number) => {
const isHeader = item.section === "Header";
const isNotIncluded = item.section === "Not Included";
const isIncluded = item.section === "Included";
return (
<TableRow key={index}>
<TableCell colSpan={2}>
<div
style={{
display: "grid",
gridTemplateColumns: "30px 30px 1fr 50px 120px",
alignItems: "center",
gap: "8px",
}}>
{isNotIncluded ? (
<Checkbox
checked={selectedIndexes.some((selected) => selected.index === item.index)}
onChange={(_, data) => handleCheckboxChange(item, data.checked === true)}
/>
) : isHeader && item.index === "Not Included in Current Policy" && notIncluded.length > 0 ? (
<Checkbox
checked={selectAll}
onChange={(_, data) => handleSelectAll(data.checked === true)}
/>
) : (
<div style={{ width: "18px", height: "18px" }}></div>
)}
{isHeader ? (
<span
style={{ cursor: "pointer" }}
onClick={() => {
if (item.index === "Included in Current Policy") {
setShowIncluded(!showIncluded);
} else if (item.index === "Not Included in Current Policy") {
setShowNotIncluded(!showNotIncluded);
}
}}
>
{item.index === "Included in Current Policy"
? showIncluded
? <ChevronDown20Regular />
: <ChevronRight20Regular />
: showNotIncluded
? <ChevronDown20Regular />
: <ChevronRight20Regular />
}
</span>
) : (
<div style={{ width: "24px" }}></div>
)}
<div style={{ fontWeight: isHeader ? "bold" : "normal" }}>
{item.index}
</div>
<div style={{ fontSize: isHeader ? 0 : undefined }}>
{isHeader ? null : item.impact}
</div>
<div>
{isHeader ? null : renderImpactDots(item.impact)}
</div>
</div>
</TableCell>
</TableRow>
);
};
const generateIndexMetricItems = (
included: { index: string; impact: string }[],
notIncluded: { index: string; impact: string }[]
): IIndexMetric[] => {
const items: IIndexMetric[] = [];
items.push({ index: "Not Included in Current Policy", impact: "", section: "Header" });
if (showNotIncluded) {
notIncluded.forEach((item) =>
items.push({ ...item, section: "Not Included" })
);
}
items.push({ index: "Included in Current Policy", impact: "", section: "Header" });
if (showIncluded) {
included.forEach((item) =>
items.push({ ...item, section: "Included" })
);
}
return items;
};
if (loading) {
return <div>
<Spinner
size="small"
style={{
'--spinner-size': '16px',
'--spinner-thickness': '2px',
'--spinner-color': '#0078D4',
} as React.CSSProperties} />
</div>;
}
return (
<div>
<div style={{ padding: "1rem", fontSize: "1.2rem", display: "flex", alignItems: "center", gap: "0.5rem" }}>
{updateMessageShown ? (
<>
<span
style={{
width: 18,
height: 18,
borderRadius: "50%",
backgroundColor: "#107C10",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}>
<FontIcon iconName="CheckMark" style={{ color: "white", fontSize: 12 }} />
</span>
<span>
Your indexing policy has been updated with the new included paths. You may review the changes in Scale & Settings.
</span>
</>
) : (
"Here is an analysis on the indexes utilized for executing the query. Based on the analysis, Cosmos DB recommends adding the selected indexes to your indexing policy to optimize the performance of this particular query."
)}
</div>
<div style={{ padding: "1rem", fontSize: "1.3rem", fontWeight: "bold" }}>Indexes analysis</div>
<Table style={{ display: "block", alignItems: "center", marginBottom: "7rem" }}>
<TableHeader>
<TableRow >
<TableCell colSpan={2}>
<div
style={{
display: "grid",
gridTemplateColumns: "30px 30px 1fr 50px 120px",
alignItems: "center",
gap: "8px",
fontWeight: "bold",
}}
>
<div style={{ width: "18px", height: "18px" }}></div>
<div style={{ width: "24px" }}></div>
<div>Index</div>
<div><span style={{ whiteSpace: "nowrap" }}>Estimated Impact</span></div>
</div>
</TableCell>
</TableRow>
</TableHeader>
<TableBody>
{generateIndexMetricItems(included, notIncluded).map(renderRow)}
</TableBody>
</Table>
{selectedIndexes.length > 0 && (
<div style={{ padding: "1rem", marginTop: "-7rem", flexWrap: "wrap" }}>
<button
onClick={handleUpdatePolicy}
style={{
backgroundColor: "#0078D4",
color: "white",
padding: "8px 16px",
border: "none",
borderRadius: "4px",
cursor: "pointer",
marginTop: "1rem",
}}
>
Update Indexing Policy with selected index(es)
</button>
</div>
)}
</div>
);
};
export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResults, executeQueryDocumentsPage }) => {
const styles = useQueryTabStyles();
const [activeTab, setActiveTab] = useState<ResultsTabs>(ResultsTabs.Results);
@@ -548,6 +947,13 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
>
Query Stats
</Tab>
<Tab
data-test="QueryTab/ResultsPane/ResultsView/IndexAdvisorTab"
id={ResultsTabs.IndexAdvisor}
value={ResultsTabs.IndexAdvisor}
>
Index Advisor
</Tab>
</TabList>
<div className={styles.queryResultsTabContentContainer}>
{activeTab === ResultsTabs.Results && (
@@ -558,6 +964,7 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
/>
)}
{activeTab === ResultsTabs.QueryStats && <QueryStatsTab queryResults={queryResults} />}
{activeTab === ResultsTabs.IndexAdvisor && <IndexAdvisorTab />}
</div>
</div>
);

View File

@@ -0,0 +1,16 @@
import create from "zustand";
interface QueryMetadataStore {
userQuery: string;
databaseId: string;
containerId: string;
setMetadata: (query1: string, db: string, container: string) => void;
}
export const useQueryMetadataStore = create<QueryMetadataStore>((set) => ({
userQuery: "",
databaseId: "",
containerId: "",
setMetadata: (query1, db, container) =>
set({ userQuery: query1, databaseId: db, containerId: container }),
}));

View File

@@ -4,6 +4,7 @@ import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFi
import { getShellNameForDisplay } from "Explorer/Tabs/CloudShellTab/Utils/CommonUtils";
import * as React from "react";
import FirewallRuleScreenshot from "../../../../images/firewallRule.png";
import VcoreFirewallRuleScreenshot from "../../../../images/vcoreMongoFirewallRule.png";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
@@ -24,15 +25,15 @@ export abstract class BaseTerminalComponentAdapter implements ReactAdapter {
) {}
public renderComponent(): JSX.Element {
if (this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo) {
return this.renderTerminalComponent();
}
if (!this.isAllPublicIPAddressesEnabled()) {
return (
<QuickstartFirewallNotification
messageType={this.getMessageType()}
screenshot={FirewallRuleScreenshot}
screenshot={
this.kind === ViewModels.TerminalKind.Mongo || this.kind === ViewModels.TerminalKind.VCoreMongo
? VcoreFirewallRuleScreenshot
: FirewallRuleScreenshot
}
shellName={getShellNameForDisplay(this.kind)}
/>
);

View File

@@ -8,7 +8,6 @@ import {
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
import * as ko from "knockout";
import * as _ from "underscore";
import * as Constants from "../../Common/Constants";
@@ -480,8 +479,9 @@ export default class Collection implements ViewModels.Collection {
node: this,
title: title,
tabPath: "",
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
collection: this,
masterKey: userContext.masterKey || "",
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
collectionId: this.id(),
databaseId: this.databaseId,
@@ -737,7 +737,7 @@ export default class Collection implements ViewModels.Collection {
title: title,
tabPath: "",
collection: this,
password: useDataplaneRbacAuthorization(userContext) ? userContext.aadToken : userContext.masterKey || "",
masterKey: userContext.masterKey || "",
collectionPartitionKeyProperty: this.partitionKeyProperties?.[0],
collectionId: this.id(),
databaseId: this.databaseId,

View File

@@ -16,6 +16,7 @@ import { useQueryCopilot } from "hooks/useQueryCopilot";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react";
import { useEffect, useMemo } from "react";
import shallow from "zustand/shallow";
import Explorer from "../Explorer";
import { useNotebook } from "../Notebook/useNotebook";
@@ -37,8 +38,13 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
const [openItems, setOpenItems] = React.useState<TreeItemValue[]>([]);
const treeStyles = useTreeStyles();
const isNotebookEnabled = useNotebook((state) => state.isNotebookEnabled)
const { isNotebookEnabled } = useNotebook(
(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.
const { refreshActiveTab } = useTabs();

View File

@@ -14,6 +14,7 @@ import PublishIcon from "../../../images/notebook/publish_content.svg";
import RefreshIcon from "../../../images/refresh-cosmos.svg";
import CollectionIcon from "../../../images/tree-collection.svg";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { IPinnedRepo } from "../../Juno/JunoClient";
@@ -57,12 +58,12 @@ export class ResourceTreeAdapter implements ReactAdapter {
useSelectedNode.subscribe(() => this.triggerRender());
useTabs.subscribe(
(state) => state.activeTab,
() => this.triggerRender(),
(state) => state.activeTab,
);
useNotebook.subscribe(
(state) => state.isNotebookEnabled,
() => this.triggerRender(),
(state) => state.isNotebookEnabled,
);
useDatabases.subscribe(() => this.triggerRender());

View File

@@ -8,11 +8,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
"children": [
{
"contextMenu": [
{
"iconSrc": {},
"label": "Open Cassandra Shell",
"onClick": [Function],
},
{
"iconSrc": {},
"label": "Delete Table",
@@ -28,11 +23,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
],
"className": "collectionNode",
"contextMenu": [
{
"iconSrc": {},
"label": "Open Cassandra Shell",
"onClick": [Function],
},
{
"iconSrc": {},
"label": "Delete Table",
@@ -55,11 +45,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
"children": [
{
"contextMenu": [
{
"iconSrc": {},
"label": "Open Cassandra Shell",
"onClick": [Function],
},
{
"iconSrc": {},
"label": "Delete Table",
@@ -80,11 +65,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
],
"className": "collectionNode",
"contextMenu": [
{
"iconSrc": {},
"label": "Open Cassandra Shell",
"onClick": [Function],
},
{
"iconSrc": {},
"label": "Delete Table",
@@ -143,11 +123,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
"children": [
{
"contextMenu": [
{
"iconSrc": {},
"label": "Open Cassandra Shell",
"onClick": [Function],
},
{
"iconSrc": {},
"label": "Delete Table",
@@ -163,11 +138,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
],
"className": "collectionNode",
"contextMenu": [
{
"iconSrc": {},
"label": "Open Cassandra Shell",
"onClick": [Function],
},
{
"iconSrc": {},
"label": "Delete Table",
@@ -217,11 +187,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
"children": [
{
"contextMenu": [
{
"iconSrc": {},
"label": "Open Cassandra Shell",
"onClick": [Function],
},
{
"iconSrc": {},
"label": "Delete Table",
@@ -292,11 +257,6 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
],
"className": "collectionNode",
"contextMenu": [
{
"iconSrc": {},
"label": "Open Cassandra Shell",
"onClick": [Function],
},
{
"iconSrc": {},
"label": "Delete Table",
@@ -363,7 +323,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
},
{
"iconSrc": {},
"label": "Open Mongo Shell",
"label": "New Shell",
"onClick": [Function],
},
{
@@ -394,7 +354,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
},
{
"iconSrc": {},
"label": "Open Mongo Shell",
"label": "New Shell",
"onClick": [Function],
},
{
@@ -426,7 +386,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
},
{
"iconSrc": {},
"label": "Open Mongo Shell",
"label": "New Shell",
"onClick": [Function],
},
{
@@ -462,7 +422,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
},
{
"iconSrc": {},
"label": "Open Mongo Shell",
"label": "New Shell",
"onClick": [Function],
},
{
@@ -530,7 +490,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
},
{
"iconSrc": {},
"label": "Open Mongo Shell",
"label": "New Shell",
"onClick": [Function],
},
{
@@ -561,7 +521,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
},
{
"iconSrc": {},
"label": "Open Mongo Shell",
"label": "New Shell",
"onClick": [Function],
},
{
@@ -620,7 +580,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
},
{
"iconSrc": {},
"label": "Open Mongo Shell",
"label": "New Shell",
"onClick": [Function],
},
{
@@ -706,7 +666,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Mo
},
{
"iconSrc": {},
"label": "Open Mongo Shell",
"label": "New Shell",
"onClick": [Function],
},
{

View File

@@ -1,6 +1,5 @@
import _ from "underscore";
import { create } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
import create, { UseStore } from "zustand";
import * as Constants from "../Common/Constants";
import * as ViewModels from "../Contracts/ViewModels";
import { userContext } from "../UserContext";
@@ -27,147 +26,143 @@ interface DatabasesState {
validateCollectionId: (databaseId: string, collectionId: string) => Promise<boolean>;
}
export const useDatabases = create<DatabasesState>()(
subscribeWithSelector(
(set, get) => ({
databases: [] as ViewModels.Database[],
resourceTokenCollection: undefined as ViewModels.CollectionBase,
sampleDataResourceTokenCollection: undefined as ViewModels.CollectionBase,
updateDatabase: (updatedDatabase: ViewModels.Database) =>
set((state) => {
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;
export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
databases: [],
resourceTokenCollection: undefined,
sampleDataResourceTokenCollection: undefined,
updateDatabase: (updatedDatabase: ViewModels.Database) =>
set((state) => {
const updatedDatabases = state.databases.map((database: ViewModels.Database) => {
if (database?.id() === updatedDatabase?.id()) {
return updatedDatabase;
}
let collectionCount = 0;
for (let i = 0; i < databases.length; i++) {
const database = databases[i];
collectionCount += database.collections().length;
if (collectionCount > 1) {
return false;
}
}
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 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(
(database.collections() || []).map(async (collection: ViewModels.Collection) => {
await collection.loadOffer();
}),
);
}),
);
},
isFirstResourceCreated: () => {
const databases = get().databases;
if (!databases || databases.length === 0) {
return false;
}
return databases.some((database) => {
// user has created at least one collection
if (database.collections()?.length > 0) {
return true;
}
// user has created a database with shared throughput
if (database.offer()) {
return true;
}
// use has created an empty database without shared throughput
return false;
});
},
findSelectedDatabase: (): ViewModels.Database => {
const selectedNode = useSelectedNode.getState().selectedNode;
if (!selectedNode) {
return undefined;
}
if (selectedNode.nodeKind === "Database") {
return _.find(get().databases, (database: ViewModels.Database) => database.id() === selectedNode.id());
}
if (selectedNode.nodeKind === "Collection") {
return selectedNode.database;
}
return selectedNode.collection?.database;
},
validateDatabaseId: (id: string): boolean => {
return !get().databases.some((database) => database.id() === id);
},
validateCollectionId: async (databaseId: string, collectionId: string): Promise<boolean> => {
const database = get().databases.find((db) => db.id() === databaseId);
// For a new tables account, database is undefined when creating the first table
if (!database && userContext.apiType === "Tables") {
return true;
}
let collectionCount = 0;
for (let i = 0; i < databases.length; i++) {
const database = databases[i];
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();
return !database.collections().some((collection) => collection.id() === collectionId);
},
})
)
);
await Promise.all(
(database.collections() || []).map(async (collection: ViewModels.Collection) => {
await collection.loadOffer();
}),
);
}),
);
},
isFirstResourceCreated: () => {
const databases = get().databases;
if (!databases || databases.length === 0) {
return false;
}
return databases.some((database) => {
// user has created at least one collection
if (database.collections()?.length > 0) {
return true;
}
// user has created a database with shared throughput
if (database.offer()) {
return true;
}
// use has created an empty database without shared throughput
return false;
});
},
findSelectedDatabase: (): ViewModels.Database => {
const selectedNode = useSelectedNode.getState().selectedNode;
if (!selectedNode) {
return undefined;
}
if (selectedNode.nodeKind === "Database") {
return _.find(get().databases, (database: ViewModels.Database) => database.id() === selectedNode.id());
}
if (selectedNode.nodeKind === "Collection") {
return selectedNode.database;
}
return selectedNode.collection?.database;
},
validateDatabaseId: (id: string): boolean => {
return !get().databases.some((database) => database.id() === id);
},
validateCollectionId: async (databaseId: string, collectionId: string): Promise<boolean> => {
const database = get().databases.find((db) => db.id() === databaseId);
// For a new tables account, database is undefined when creating the first table
if (!database && userContext.apiType === "Tables") {
return true;
}
await database.loadCollections();
return !database.collections().some((collection) => collection.id() === collectionId);
},
}));

View File

@@ -1,6 +1,6 @@
import { ConnectionStatusType, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { create } from "zustand";
import create, { UseStore } from "zustand";
import * as ViewModels from "../Contracts/ViewModels";
import { useTabs } from "../hooks/useTabs";
export interface SelectedNodeState {
@@ -17,7 +17,7 @@ export interface SelectedNodeState {
isQueryCopilotCollectionSelected: () => boolean;
}
export const useSelectedNode = create<SelectedNodeState>((set, get) => ({
export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) => ({
selectedNode: undefined,
setSelectedNode: (node: ViewModels.TreeNode) => set({ selectedNode: node }),
isDatabaseNodeOrNoneSelected: (): boolean => {

View File

@@ -34,8 +34,7 @@ const App: React.FunctionComponent = () => {
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
const config = useConfig();
const { isLoggedIn, armToken, graphToken, account, tenantId, logout, login, switchTenant, authFailure } =
useAADAuth(config);
useAADAuth();
const [databaseAccount, setDatabaseAccount] = React.useState<DatabaseAccount>();
const [authType, setAuthType] = React.useState<AuthType>(encryptedToken ? AuthType.EncryptedToken : undefined);
const [connectionString, setConnectionString] = React.useState<string>();

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import { PropsWithChildren, useEffect } from "react";
import { KeyBindingMap, tinykeys } from "tinykeys";
import { create } from "zustand";
import create, { UseStore } from "zustand";
/**
* Represents a keyboard shortcut handler.
@@ -126,7 +126,7 @@ export const clearKeyboardActionGroup = (group: KeyboardActionGroup) => {
useKeyboardActionHandlers.getState().setHandlers(group, {});
};
const useKeyboardActionHandlers = create<KeyboardShortcutState>((set, get) => ({
const useKeyboardActionHandlers: UseStore<KeyboardShortcutState> = create((set, get) => ({
allHandlers: {},
groups: {},
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => {

View File

@@ -252,7 +252,7 @@ export class PhoenixClient {
private getPhoenixControlPlanePathPrefix(): string {
if (!this.armResourceId) {
throw new Error("The Phoenix client was not initialized properly: missing ARM resource id");
throw new Error("The Phoenix client was not initialized properly: missing ARM resourcce id");
}
const toolsEndpoint =

View File

@@ -111,7 +111,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
enableCloudShell: true,
enableCloudShell: "true" === get("enablecloudshell"),
};
}

View File

@@ -269,7 +269,7 @@ export const getOfferingIds = async (regions: Array<RegionItem>): Promise<Offeri
host: configContext.CATALOG_ENDPOINT,
path: getOfferingIdPathForRegion(),
method: "GET",
apiVersion: configContext.CATALOG_API_VERSION,
apiVersion: "2023-05-01-preview",
queryParams: {
filter:
"armRegionNameeq '" +

View File

@@ -1,4 +1,3 @@
import { MongoGuidRepresentation } from "Common/Constants";
import { SplitterDirection } from "Common/Splitter";
import * as LocalStorageUtility from "./LocalStorageUtility";
import * as SessionStorageUtility from "./SessionStorageUtility";
@@ -34,7 +33,6 @@ export enum StorageKey {
DocumentsTabPrefs,
DefaultQueryResultsView,
AppState,
MongoGuidRepresentation,
}
export const hasRUThresholdBeenConfigured = (): boolean => {
@@ -67,13 +65,4 @@ export const getDefaultQueryResultsView = (): SplitterDirection => {
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;

View File

@@ -91,11 +91,5 @@ export const getItemName = (): string => {
};
export const isDataplaneRbacSupported = (apiType: string): boolean => {
return (
apiType === "SQL" || apiType === "Tables" || apiType === "Gremlin" || apiType === "Mongo" || apiType === "Cassandra"
);
};
export const hasProxyServer = (apiType: string): boolean => {
return apiType === "Mongo" || apiType === "Cassandra";
return apiType === "SQL" || apiType === "Tables";
};

View File

@@ -1,51 +1,10 @@
import { AuthType } from "../AuthType";
import * as Constants from "../Common/Constants";
import { ApiType, updateUserContext, userContext } from "../UserContext";
import { updateUserContext } from "../UserContext";
import * as AuthorizationUtils from "./AuthorizationUtils";
jest.mock("../Explorer/Explorer");
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()", () => {
it("should return authorization header if authentication type is AAD", () => {
updateUserContext({
@@ -95,41 +54,4 @@ describe("AuthorizationUtils", () => {
).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);
});
});
});

View File

@@ -1,7 +1,5 @@
import * as msal from "@azure/msal-browser";
import { getEnvironmentScopeEndpoint } from "Common/EnvironmentUtility";
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import { hasProxyServer, isDataplaneRbacSupported } from "Utils/APITypeUtils";
import { AuthType } from "../AuthType";
import * as Constants from "../Common/Constants";
import * as Logger from "../Common/Logger";
@@ -9,7 +7,7 @@ import { configContext } from "../ConfigContext";
import { DatabaseAccount } from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { trace, traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
import { UserContext, userContext } from "../UserContext";
import { userContext } from "../UserContext";
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
if (userContext.authType === AuthType.EncryptedToken) {
@@ -75,12 +73,10 @@ export async function acquireMsalTokenForAccount(
if (userContext.databaseAccount.properties?.documentEndpoint === undefined) {
throw new Error("Database account has no document endpoint defined");
}
let hrefEndpoint = "";
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
hrefEndpoint = getEnvironmentScopeEndpoint();
} else {
hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(/\/+$/, "/.default");
}
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
/\/+$/,
"/.default",
);
const msalInstance = await getMsalInstance();
const knownAccounts = msalInstance.getAllAccounts();
// If user_hint is provided, we will try to use it to find the account.
@@ -183,14 +179,3 @@ 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);
}

Some files were not shown because too many files have changed in this diff Show More