Compare commits

..

1 Commits

Author SHA1 Message Date
nishthaAhujaa
ac137c994b partly 2025-09-02 12:40:58 +05:30
58 changed files with 60154 additions and 16409 deletions

23
.vscode/settings.json vendored
View File

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

37624
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

36267
preview/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -138,14 +138,6 @@ export enum MongoBackendEndpointType {
remote, 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 PortalBackendEndpoints { export class PortalBackendEndpoints {
public static readonly Development: string = "https://localhost:7235"; public static readonly Development: string = "https://localhost:7235";
public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com"; public static readonly Mpac: string = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
@@ -263,7 +255,6 @@ export class HttpHeaders {
public static activityId: string = "x-ms-activity-id"; public static activityId: string = "x-ms-activity-id";
public static apiType: string = "x-ms-cosmos-apitype"; public static apiType: string = "x-ms-cosmos-apitype";
public static authorization: string = "authorization"; public static authorization: string = "authorization";
public static entraIdToken: string = "x-ms-entraid-token";
public static collectionIndexTransformationProgress: string = public static collectionIndexTransformationProgress: string =
"x-ms-documentdb-collection-index-transformation-progress"; "x-ms-documentdb-collection-index-transformation-progress";
public static continuation: string = "x-ms-continuation"; public static continuation: string = "x-ms-continuation";

View File

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

View File

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

View File

@@ -7,7 +7,6 @@ import { MessageTypes } from "../Contracts/ExplorerContracts";
import { Collection } from "../Contracts/ViewModels"; import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { isDataplaneRbacEnabledForProxyApi } from "../Utils/AuthorizationUtils";
import { logConsoleError } from "../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants"; import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
import { MinimalQueryIterator } from "./IteratorUtilities"; import { MinimalQueryIterator } from "./IteratorUtilities";
@@ -23,13 +22,7 @@ function authHeaders() {
if (userContext.authType === AuthType.EncryptedToken) { if (userContext.authType === AuthType.EncryptedToken) {
return { [HttpHeaders.guestAccessToken]: userContext.accessToken }; return { [HttpHeaders.guestAccessToken]: userContext.accessToken };
} else { } else {
const headers: { [key: string]: string } = { return { [HttpHeaders.authorization]: userContext.authorizationToken };
[HttpHeaders.authorization]: userContext.authorizationToken,
};
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
headers[HttpHeaders.entraIdToken] = userContext.aadToken;
}
return headers;
} }
} }

View File

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

View File

@@ -23,7 +23,7 @@ import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as ko from "knockout"; import * as ko from "knockout";
import React from "react"; import React from "react";
import _ from "underscore"; import _ from "underscore";
import { shallow } from "zustand/shallow"; import shallow from "zustand/shallow";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
@@ -112,8 +112,8 @@ export default class Explorer {
this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id); this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
useNotebook.subscribe( useNotebook.subscribe(
(state) => state.isNotebooksEnabledForAccount,
() => this.refreshCommandBarButtons(), () => this.refreshCommandBarButtons(),
(state) => state.isNotebooksEnabledForAccount,
); );
this.queriesClient = new QueriesClient(this); this.queriesClient = new QueriesClient(this);
@@ -136,13 +136,13 @@ export default class Explorer {
}); });
useTabs.subscribe( useTabs.subscribe(
(state) => state.openedTabs,
(openedTabs: TabsBase[]) => { (openedTabs: TabsBase[]) => {
if (openedTabs.length === 0) { if (openedTabs.length === 0) {
useSelectedNode.getState().setSelectedNode(undefined); useSelectedNode.getState().setSelectedNode(undefined);
useCommandBar.getState().setContextButtons([]); useCommandBar.getState().setContextButtons([]);
} }
} },
(state) => state.openedTabs,
); );
this.isTabsContentExpanded = ko.observable(false); this.isTabsContentExpanded = ko.observable(false);
@@ -170,9 +170,9 @@ export default class Explorer {
); );
useNotebook.subscribe( useNotebook.subscribe(
(state) => [state.isNotebookEnabled, state.isRefreshed],
async () => this.initiateAndRefreshNotebookList(), async () => this.initiateAndRefreshNotebookList(),
{ equalityFn: shallow }, (state) => [state.isNotebookEnabled, state.isRefreshed],
shallow,
); );
this.resourceTree = new ResourceTreeAdapter(this); this.resourceTree = new ResourceTreeAdapter(this);

View File

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

View File

@@ -1,6 +1,7 @@
import { KeyboardAction } from "KeyboardShortcuts"; import { KeyboardAction } from "KeyboardShortcuts";
import { isDataplaneRbacSupported } from "Utils/APITypeUtils"; import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
import * as React from "react"; import * as React from "react";
import { useEffect, useState } from "react";
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg"; import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg"; import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
import AddTriggerIcon from "../../../../images/AddTrigger.svg"; import AddTriggerIcon from "../../../../images/AddTrigger.svg";
@@ -67,7 +68,15 @@ export function createStaticCommandBarButtons(
} }
if (isDataplaneRbacSupported(userContext.apiType)) { 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) { if (loginButtonProps) {
addDivider(); addDivider();
buttons.push(loginButtonProps); buttons.push(loginButtonProps);

View File

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

View File

@@ -1,8 +1,7 @@
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { PhoenixClient } from "Phoenix/PhoenixClient"; import { PhoenixClient } from "Phoenix/PhoenixClient";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { create } from "zustand"; import create, { UseStore } from "zustand";
import { subscribeWithSelector } from 'zustand/middleware';
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { ConnectionStatusType, HttpStatusCodes } from "../../Common/Constants"; import { ConnectionStatusType, HttpStatusCodes } from "../../Common/Constants";
@@ -67,42 +66,40 @@ interface NotebookState {
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void; setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
} }
export const useNotebook = create<NotebookState>()( export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
subscribeWithSelector(
(set, get) => ({
isNotebookEnabled: false, isNotebookEnabled: false,
isNotebooksEnabledForAccount: false, isNotebooksEnabledForAccount: false,
notebookServerInfo: { notebookServerInfo: {
notebookServerEndpoint: "", notebookServerEndpoint: undefined,
authToken: "", authToken: undefined,
forwardingId: "", forwardingId: undefined,
}, },
sparkClusterConnectionInfo: { sparkClusterConnectionInfo: {
userName: "", userName: undefined,
password: "", password: undefined,
endpoints: [] as DataModels.SparkClusterEndpoint[], endpoints: [],
}, },
isSynapseLinkUpdating: false, isSynapseLinkUpdating: false,
memoryUsageInfo: undefined as DataModels.MemoryUsageInfo, memoryUsageInfo: undefined,
isShellEnabled: false, isShellEnabled: false,
notebookBasePath: Constants.Notebook.defaultBasePath, notebookBasePath: Constants.Notebook.defaultBasePath,
isInitializingNotebooks: false, isInitializingNotebooks: false,
myNotebooksContentRoot: undefined as NotebookContentItem, myNotebooksContentRoot: undefined,
gitHubNotebooksContentRoot: undefined as NotebookContentItem, gitHubNotebooksContentRoot: undefined,
galleryContentRoot: undefined as NotebookContentItem, galleryContentRoot: undefined,
connectionInfo: { connectionInfo: {
status: ConnectionStatusType.Connect, status: ConnectionStatusType.Connect,
}, },
notebookFolderName: "", notebookFolderName: undefined,
isAllocating: false, isAllocating: false,
isRefreshed: false, isRefreshed: false,
containerStatus: { containerStatus: {
status: undefined, status: undefined,
durationLeftInMinutes: undefined, durationLeftInMinutes: undefined,
phoenixServerInfo: undefined, phoenixServerInfo: undefined,
} as ContainerInfo, },
isPhoenixNotebooks: undefined as boolean, isPhoenixNotebooks: undefined,
isPhoenixFeatures: undefined as boolean, isPhoenixFeatures: undefined,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
@@ -335,6 +332,4 @@ export const useNotebook = create<NotebookState>()(
}, },
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }), setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }), setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }),
}) }));
)
);

View File

@@ -51,7 +51,7 @@ import { useClientWriteEnabled } from "hooks/useClientWriteEnabled";
import { useQueryCopilot } from "hooks/useQueryCopilot"; import { useQueryCopilot } from "hooks/useQueryCopilot";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import React, { FunctionComponent, useState } from "react"; import React, { FunctionComponent, useState } from "react";
import { create } from "zustand"; import create, { UseStore } from "zustand";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
@@ -65,6 +65,8 @@ export interface DataPlaneRbacState {
setAadDataPlaneUpdated: (aadTokenUpdated: boolean) => void; setAadDataPlaneUpdated: (aadTokenUpdated: boolean) => void;
} }
type DataPlaneRbacStore = UseStore<Partial<DataPlaneRbacState>>;
const useStyles = makeStyles({ const useStyles = makeStyles({
bulletList: { bulletList: {
listStyleType: "disc", listStyleType: "disc",
@@ -98,7 +100,7 @@ const useStyles = makeStyles({
}, },
}); });
export const useDataPlaneRbac = create<Partial<DataPlaneRbacState>>(() => ({ export const useDataPlaneRbac: DataPlaneRbacStore = create(() => ({
dataPlaneRbacEnabled: false, dataPlaneRbacEnabled: false,
})); }));

View File

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

View File

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

View File

@@ -77,39 +77,39 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
this.subscriptions.push( this.subscriptions.push(
{ {
dispose: useNotebook.subscribe( dispose: useNotebook.subscribe(
(state) => state.isNotebookEnabled,
() => this.setState({}), () => this.setState({}),
(state) => state.isNotebookEnabled,
), ),
}, },
{ dispose: useSelectedNode.subscribe(() => this.setState({})) }, { dispose: useSelectedNode.subscribe(() => this.setState({})) },
{ {
dispose: useCarousel.subscribe( dispose: useCarousel.subscribe(
() => this.setState({}),
(state) => state.showCoachMark, (state) => state.showCoachMark,
() => this.setState({}),
), ),
}, },
{ {
dispose: usePostgres.subscribe( dispose: usePostgres.subscribe(
() => this.setState({}),
(state) => state.showPostgreTeachingBubble, (state) => state.showPostgreTeachingBubble,
() => this.setState({}),
), ),
}, },
{ {
dispose: usePostgres.subscribe( dispose: usePostgres.subscribe(
(state) => state.showResetPasswordBubble,
() => this.setState({}), () => this.setState({}),
(state) => state.showResetPasswordBubble,
), ),
}, },
{ {
dispose: useDatabases.subscribe( dispose: useDatabases.subscribe(
(state) => state.sampleDataResourceTokenCollection,
() => this.setState({}), () => this.setState({}),
(state) => state.sampleDataResourceTokenCollection,
), ),
}, },
{ {
dispose: useQueryCopilot.subscribe( dispose: useQueryCopilot.subscribe(
(state) => state.copilotEnabled,
() => this.setState({}), () => this.setState({}),
(state) => state.copilotEnabled,
), ),
}, },
); );

View File

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

View File

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

View File

@@ -14,17 +14,12 @@ export const DISABLE_HISTORY = `set +o history`;
* Used when shell initialization or connection fails. * Used when shell initialization or connection fails.
*/ */
export const EXIT_COMMAND = ` printf "\\033[1;31mSession ended. Please close this tab and initiate a new shell session if needed.\\033[0m\\n" && disown -a && exit`; export const EXIT_COMMAND = ` printf "\\033[1;31mSession ended. Please close this tab and initiate a new shell session if needed.\\033[0m\\n" && disown -a && exit`;
/**
* Command that displays error message with MongoDB networking guidance and exits the shell session.
* Used when MongoDB shell connection fails due to networking issues.
*/
export const EXIT_COMMAND_MONGO = ` printf "\\033[1;31mSession ended. Please close this tab and initiate a new shell session if needed.\\033[0m\\n" && printf "\\033[1;36mPlease use the 'Add Azure Cloud Shell IPs' button in the Networking blade to allow Cloud Shell access, if not already configured.\\033[0m\\n" && disown -a && exit`;
/** /**
* This command runs mongosh in no-database and quiet mode, * This command runs mongosh in no-database and quiet mode,
* and evaluates the `disableTelemetry()` function to turn off telemetry collection. * and evaluates the `disableTelemetry()` function to turn off telemetry collection.
*/ */
export const DISABLE_TELEMETRY_COMMAND = `mongosh --nodb --quiet --eval 'disableTelemetry()'`; export const DISABLE_TELEMETRY_COMMAND = `mongosh --nodb --quiet --eval "disableTelemetry()"`;
/** /**
* Abstract class that defines the interface for shell-specific handlers * Abstract class that defines the interface for shell-specific handlers
@@ -45,14 +40,6 @@ export abstract class AbstractShellHandler {
abstract getTerminalSuppressedData(): string[]; abstract getTerminalSuppressedData(): string[];
updateTerminalData?(data: string): string; updateTerminalData?(data: string): string;
/**
* Gets the exit command to use when connection fails.
* Can be overridden by subclasses to provide custom exit commands.
*/
protected getExitCommand(): string {
return EXIT_COMMAND;
}
/** /**
* Constructs the complete initialization command sequence for the shell. * Constructs the complete initialization command sequence for the shell.
* *
@@ -77,7 +64,7 @@ export abstract class AbstractShellHandler {
START_MARKER, START_MARKER,
DISABLE_HISTORY, DISABLE_HISTORY,
...setupCommands, ...setupCommands,
`{ ${connectionCommand}; } || true;${this.getExitCommand()}`, `{ ${connectionCommand}; } || true;${EXIT_COMMAND}`,
]; ];
return allCommands.join("\n").concat("\n"); return allCommands.join("\n").concat("\n");
@@ -97,7 +84,7 @@ export abstract class AbstractShellHandler {
* is not already present in the environment. * is not already present in the environment.
*/ */
protected mongoShellSetupCommands(): string[] { protected mongoShellSetupCommands(): string[] {
const PACKAGE_VERSION: string = "2.5.6"; const PACKAGE_VERSION: string = "2.5.5";
return [ return [
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi", "if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`, `if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,

View File

@@ -18,12 +18,6 @@ interface DatabaseAccount {
interface UserContextType { interface UserContextType {
databaseAccount: DatabaseAccount; databaseAccount: DatabaseAccount;
features: {
enableAadDataPlane: boolean;
};
apiType: string;
dataPlaneRbacEnabled: boolean;
aadToken?: string;
} }
// Mock dependencies // Mock dependencies
@@ -35,8 +29,6 @@ jest.mock("../../../../UserContext", () => ({
mongoEndpoint: "https://test-mongo.documents.azure.com:443/", mongoEndpoint: "https://test-mongo.documents.azure.com:443/",
}, },
}, },
features: { enableAadDataPlane: false },
apiType: "Mongo",
}, },
})); }));
@@ -78,7 +70,7 @@ describe("MongoShellHandler", () => {
expect(Array.isArray(commands)).toBe(true); expect(Array.isArray(commands)).toBe(true);
expect(commands.length).toBe(7); expect(commands.length).toBe(7);
expect(commands[1]).toContain("mongosh-2.5.6-linux-x64.tgz"); expect(commands[1]).toContain("mongosh-2.5.5-linux-x64.tgz");
}); });
}); });
@@ -96,12 +88,11 @@ describe("MongoShellHandler", () => {
kind: "test-kind", kind: "test-kind",
properties: { mongoEndpoint: "https://test-mongo.documents.azure.com:443/" }, properties: { mongoEndpoint: "https://test-mongo.documents.azure.com:443/" },
}; };
(userContext as UserContextType).dataPlaneRbacEnabled = false;
const command = mongoShellHandler.getConnectionCommand(); const command = mongoShellHandler.getConnectionCommand();
expect(command).toBe( expect(command).toBe(
"mongosh --nodb --quiet --eval 'disableTelemetry()'; mongosh mongodb://test-mongo.documents.azure.com:10255?appName=CosmosExplorerTerminal --username test-account --password test-key --tls --tlsAllowInvalidCertificates", 'mongosh --nodb --quiet --eval "disableTelemetry()" && mongosh mongodb://test-mongo.documents.azure.com:10255?appName=CosmosExplorerTerminal --username test-account --password test-key --tls --tlsAllowInvalidCertificates',
); );
expect(CommonUtils.getHostFromUrl).toHaveBeenCalledWith("https://test-mongo.documents.azure.com:443/"); expect(CommonUtils.getHostFromUrl).toHaveBeenCalledWith("https://test-mongo.documents.azure.com:443/");
@@ -124,47 +115,12 @@ describe("MongoShellHandler", () => {
}; };
const command = mongoShellHandler.getConnectionCommand(); const command = mongoShellHandler.getConnectionCommand();
expect(command).toBe("echo 'Database name not found.'"); expect(command).toBe("echo 'Database name not found.'");
// Restore original // Restore original
(userContext as UserContextType).databaseAccount = originalDatabaseAccount; (userContext as UserContextType).databaseAccount = originalDatabaseAccount;
}); });
it("should return echo if endpoint is missing", () => {
const testKey = "test-key";
(userContext as UserContextType).databaseAccount = {
id: "test-id",
name: "", // Empty name to simulate missing name
location: "test-location",
type: "test-type",
kind: "test-kind",
properties: { mongoEndpoint: "" },
};
const mongoShellHandler = new MongoShellHandler(testKey);
const command = mongoShellHandler.getConnectionCommand();
expect(command).toBe("echo 'MongoDB endpoint not found.'");
});
it("should use _getAadConnectionCommand when _isEntraIdEnabled is true", () => {
const testKey = "aad-key";
(userContext as UserContextType).databaseAccount = {
id: "test-id",
name: "test-account",
location: "test-location",
type: "test-type",
kind: "test-kind",
properties: { mongoEndpoint: "https://test-mongo.documents.azure.com:443/" },
};
(userContext as UserContextType).dataPlaneRbacEnabled = true;
const mongoShellHandler = new MongoShellHandler(testKey);
const command = mongoShellHandler.getConnectionCommand();
expect(command).toContain(
"mongosh 'mongodb://test-account:aad-key@test-account.mongo.cosmos.azure.com:10255/?ssl=true&replicaSet=globaldb&authMechanism=PLAIN&retryWrites=false' --tls --tlsAllowInvalidCertificates",
);
expect(command.startsWith("mongosh --nodb")).toBeTruthy();
});
}); });
describe("getTerminalSuppressedData", () => { describe("getTerminalSuppressedData", () => {

View File

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

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ describe("VCoreMongoShellHandler", () => {
expect(Array.isArray(commands)).toBe(true); expect(Array.isArray(commands)).toBe(true);
expect(commands.length).toBe(7); expect(commands.length).toBe(7);
expect(commands[1]).toContain("mongosh-2.5.6-linux-x64.tgz"); expect(commands[1]).toContain("mongosh-2.5.5-linux-x64.tgz");
expect(commands[0]).toContain("mongosh not found"); expect(commands[0]).toContain("mongosh not found");
}); });

View File

@@ -1,6 +1,6 @@
import { userContext } from "../../../../UserContext"; import { userContext } from "../../../../UserContext";
import { filterAndCleanTerminalOutput, getMongoShellRemoveInfoText } from "../Utils/CommonUtils"; import { filterAndCleanTerminalOutput, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND, EXIT_COMMAND_MONGO } from "./AbstractShellHandler"; import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND } from "./AbstractShellHandler";
export class VCoreMongoShellHandler extends AbstractShellHandler { export class VCoreMongoShellHandler extends AbstractShellHandler {
private _endpoint: string | undefined; private _endpoint: string | undefined;
@@ -35,13 +35,6 @@ export class VCoreMongoShellHandler extends AbstractShellHandler {
return ["Warning: Non-Genuine MongoDB Detected", "Telemetry is now disabled."]; 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 { updateTerminalData(data: string): string {
return filterAndCleanTerminalOutput(data, this._removeInfoText); return filterAndCleanTerminalOutput(data, this._removeInfoText);
} }

View File

@@ -92,18 +92,6 @@ export class AttachAddon implements ITerminalAddon {
* @param {Terminal} terminal - The XTerm terminal instance * @param {Terminal} terminal - The XTerm terminal instance
*/ */
public addMessageListener(terminal: Terminal): void { public addMessageListener(terminal: Terminal): void {
let messageBuffer = "";
let bufferTimeout: NodeJS.Timeout | null = null;
const BUFFER_TIMEOUT = 50; // ms - short timeout for prompt detection
const processBuffer = () => {
if (messageBuffer.length > 0) {
this.handleCompleteTerminalData(terminal, messageBuffer);
messageBuffer = "";
}
bufferTimeout = null;
};
this._disposables.push( this._disposables.push(
addSocketListener(this._socket, "message", (ev) => { addSocketListener(this._socket, "message", (ev) => {
let data: ArrayBuffer | string = ev.data; let data: ArrayBuffer | string = ev.data;
@@ -115,114 +103,32 @@ export class AttachAddon implements ITerminalAddon {
data = enc.decode(ev.data as ArrayBuffer); data = enc.decode(ev.data as ArrayBuffer);
} }
// Handle status messages // for example of json object look in TerminalHelper in the socket.onMessage
let processedStatusData = data; if (data.includes(startStatusJson) && data.includes(endStatusJson)) {
// process as one line
// Process status messages with delimiters const statusData = data.split(startStatusJson)[1].split(endStatusJson)[0];
// eslint-disable-next-line no-constant-condition data = data.replace(statusData, "");
while (true) { data = data.replace(startStatusJson, "");
const startIndex = processedStatusData.indexOf(startStatusJson); data = data.replace(endStatusJson, "");
if (startIndex === -1) { } else if (data.includes(startStatusJson)) {
break; // 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 = "";
} }
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);
}
// Add to message buffer
messageBuffer += processedStatusData;
// Clear existing timeout
if (bufferTimeout) {
clearTimeout(bufferTimeout);
bufferTimeout = null;
}
// Check if this looks like a complete message/command
const isComplete = this.isMessageComplete(messageBuffer, processedStatusData);
if (isComplete) {
// Message marked as complete, processing immediately
processBuffer();
} else {
// Set timeout to process buffer after delay
bufferTimeout = setTimeout(processBuffer, BUFFER_TIMEOUT);
}
}),
);
// Clean up timeout on dispose
this._disposables.push({
dispose: () => {
if (bufferTimeout) {
clearTimeout(bufferTimeout);
}
},
});
}
private isMessageComplete(fullBuffer: string, currentChunk: string): boolean {
// Immediate completion indicators
const immediateCompletionPatterns = [
/\n$/, // Ends with newline
/\r$/, // Ends with carriage return
/\r\n$/, // Ends with CRLF
/; \} \|\| true;$/, // Your command pattern
/disown -a && exit$/, // Exit commands
/printf.*?\\033\[0m\\n"$/, // Your printf pattern
];
// Check current chunk for immediate completion
for (const pattern of immediateCompletionPatterns) {
if (pattern.test(currentChunk)) {
return true;
}
}
// ANSI sequence detection - these might be complete prompts
const ansiPromptPatterns = [
/\[\d+G\[0J.*>\s*\[\d+G$/, // Your specific pattern: [1G[0J...> [26G
/\[\d+;\d+H/, // Cursor position sequences
/\]\s*\[\d+G$/, // Ends with cursor positioning
/>\s*\[\d+G$/, // Prompt followed by cursor position
];
// Check if buffer ends with what looks like a complete prompt
for (const pattern of ansiPromptPatterns) {
if (pattern.test(fullBuffer)) {
return true;
}
}
// Check for MongoDB shell prompts specifically
const mongoPromptPatterns = [
/globaldb \[primary\] \w+>\s*\[\d+G$/, // MongoDB replica set prompt
/>\s*\[\d+G$/, // General prompt with cursor positioning
/\w+>\s*$/, // Simple shell prompt
];
for (const pattern of mongoPromptPatterns) {
if (pattern.test(fullBuffer)) {
return true;
}
}
return false;
}
private handleCompleteTerminalData(terminal: Terminal, data: string): void {
if (this._allowTerminalWrite && data.includes(this._startMarker)) { if (this._allowTerminalWrite && data.includes(this._startMarker)) {
this._allowTerminalWrite = false; this._allowTerminalWrite = false;
terminal.write(`Preparing ${this._shellHandler.getShellName()} environment...\r\n`); terminal.write(`Preparing ${this._shellHandler.getShellName()} environment...\r\n`);
@@ -235,6 +141,7 @@ export class AttachAddon implements ITerminalAddon {
: data; : data;
const suppressedData = this._shellHandler?.getTerminalSuppressedData(); const suppressedData = this._shellHandler?.getTerminalSuppressedData();
const shouldNotWrite = suppressedData.filter(Boolean).some((item) => updatedData.includes(item)); const shouldNotWrite = suppressedData.filter(Boolean).some((item) => updatedData.includes(item));
if (!shouldNotWrite) { if (!shouldNotWrite) {
@@ -245,6 +152,8 @@ export class AttachAddon implements ITerminalAddon {
if (data.includes(this._shellHandler.getConnectionCommand())) { if (data.includes(this._shellHandler.getConnectionCommand())) {
this._allowTerminalWrite = true; this._allowTerminalWrite = true;
} }
}),
);
} }
public dispose(): void { public dispose(): void {

View File

@@ -0,0 +1,13 @@
export const CLOUDSHELL_IP_RECOMMENDATIONS = {
centralindia: ["4.247.135.109", "74.225.207.63"],
southeastasia: ["4.194.56.6", "4.194.213.10", "4.194.144.127", "4.194.5.74"],
centraluseuap: ["52.158.186.182", "172.215.26.246", "134.138.154.177", "134.138.129.52", "172.215.31.177"],
eastus2euap: ["135.18.43.51", "20.252.175.33", "40.89.88.111", "135.18.17.187", "135.18.67.251"],
eastus: ["40.71.199.151", "20.42.18.188", "52.190.17.9", "20.120.96.152"],
northeurope: ["74.234.65.146", "52.169.70.113"],
southcentralus: ["4.151.247.81", "20.225.211.35", "4.151.48.133", "4.151.247.225"],
westeurope: ["52.166.126.216", "108.142.162.20", "52.178.13.125", "172.201.33.160"],
westus: ["20.245.161.131", "57.154.182.51", "40.118.133.244", "20.253.192.12", "20.43.245.209", "20.66.22.66"],
usgovarizona: ["62.10.232.179"],
usgovvirginia: ["62.10.26.85"],
};

View File

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

View File

@@ -146,17 +146,11 @@ describe("Documents tab (Mongo API)", () => {
updateConfigContext({ platform: Platform.Hosted }); updateConfigContext({ platform: Platform.Hosted });
const props: IDocumentsTabComponentProps = createMockProps(); const props: IDocumentsTabComponentProps = createMockProps();
wrapper = mount(<DocumentsTabComponent {...props} />); wrapper = mount(<DocumentsTabComponent {...props} />);
wrapper = await waitForComponentToPaint(wrapper);
// Wait for all pending promises
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
}); });
// Wait for any async operations to complete
wrapper = await waitForComponentToPaint(wrapper, 100);
}, 10000);
afterEach(() => { afterEach(() => {
wrapper.unmount(); wrapper.unmount();
}); });

View File

@@ -44,8 +44,8 @@ export default class NotebookTabV2 extends NotebookTabBase {
this.container = options.container; this.container = options.container;
this.notebookPath = ko.observable(options.notebookContentItem.path); this.notebookPath = ko.observable(options.notebookContentItem.path);
useNotebook.subscribe( useNotebook.subscribe(
(state) => state.notebookServerInfo,
() => logConsoleInfo("New notebook server info received."), () => logConsoleInfo("New notebook server info received."),
(state) => state.notebookServerInfo,
); );
this.notebookComponentAdapter = new NotebookComponentAdapter({ this.notebookComponentAdapter = new NotebookComponentAdapter({
contentItem: options.notebookContentItem, contentItem: options.notebookContentItem,

View File

@@ -0,0 +1,40 @@
import { configContext } from "ConfigContext";
import * as DataModels from "Contracts/DataModels";
import { userContext } from "UserContext";
import { armRequest } from "Utils/arm/request";
import { CLOUDSHELL_IP_RECOMMENDATIONS } from "../CloudShellTab/Utils/CloudShellIPUtils";
import { getNormalizedRegion } from "../CloudShellTab/Utils/RegionUtils";
export async function checkCloudShellIPsConfigured() {
const databaseRegion = userContext.databaseAccount?.location;
console.log("db region", databaseRegion);
const normalizedRegion = getNormalizedRegion(databaseRegion, "westus");
const cloudShellIPs = getCloudShellIPsForRegion(normalizedRegion);
console.log("CloudShell IPs for region", normalizedRegion, cloudShellIPs);
if (!cloudShellIPs || cloudShellIPs.length === 0) {
return false;
}
const firewallRules = await getFirewallRules();
console.log("firewall rules", firewallRules);
return false;
}
function getCloudShellIPsForRegion(region: string): string[] {
const regionKey = region.toLowerCase();
const ips = CLOUDSHELL_IP_RECOMMENDATIONS[regionKey as keyof typeof CLOUDSHELL_IP_RECOMMENDATIONS];
return ips ? [...ips] : [];
}
async function getFirewallRules(): Promise<DataModels.FirewallRule[]> {
const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`;
const response: any = await armRequest({
host: configContext.ARM_ENDPOINT,
path: firewallRulesUri,
method: "GET",
apiVersion: "2023-03-01-preview",
});
return response?.data?.value || response?.value || [];
}

View File

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

View File

@@ -8,6 +8,7 @@ import { userContext } from "../../UserContext";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useNotebook } from "../Notebook/useNotebook"; import { useNotebook } from "../Notebook/useNotebook";
import { checkCloudShellIPsConfigured } from "./Shared/CloudShellIPChecker";
import { NotebookTerminalComponentAdapter } from "./ShellAdapters/NotebookTerminalComponentAdapter"; import { NotebookTerminalComponentAdapter } from "./ShellAdapters/NotebookTerminalComponentAdapter";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
@@ -23,11 +24,13 @@ export default class TerminalTab extends TabsBase {
private container: Explorer; private container: Explorer;
private notebookTerminalComponentAdapter: ReactAdapter; private notebookTerminalComponentAdapter: ReactAdapter;
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>; private isAllPublicIPAddressesEnabled: ko.Observable<boolean>;
private isCloudShellIPsConfigured: ko.Observable<boolean>;
constructor(options: TerminalTabOptions) { constructor(options: TerminalTabOptions) {
super(options); super(options);
this.container = options.container; this.container = options.container;
this.isAllPublicIPAddressesEnabled = ko.observable(true); this.isAllPublicIPAddressesEnabled = ko.observable(true);
this.isCloudShellIPsConfigured = ko.observable(true);
const commonArgs: [ const commonArgs: [
() => DataModels.DatabaseAccount, () => DataModels.DatabaseAccount,
@@ -44,11 +47,29 @@ export default class TerminalTab extends TabsBase {
]; ];
if (userContext.features.enableCloudShell) { if (userContext.features.enableCloudShell) {
this.notebookTerminalComponentAdapter = new CloudShellTerminalComponentAdapter(...commonArgs); this.notebookTerminalComponentAdapter = new CloudShellTerminalComponentAdapter(
() => userContext?.databaseAccount,
() => this.tabId,
() => this.getUsername(),
this.isAllPublicIPAddressesEnabled,
options.kind,
options.kind === ViewModels.TerminalKind.VCoreMongo ? this.isCloudShellIPsConfigured : undefined,
);
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => { this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
if (options.kind === ViewModels.TerminalKind.VCoreMongo) {
const cloudShellConfigured = this.isCloudShellIPsConfigured();
return this.isTemplateReady() && cloudShellConfigured;
}
return this.isTemplateReady() && this.isAllPublicIPAddressesEnabled(); return this.isTemplateReady() && this.isAllPublicIPAddressesEnabled();
}); });
if (options.kind === ViewModels.TerminalKind.VCoreMongo) {
(async () => {
const result = await checkCloudShellIPsConfigured();
this.isCloudShellIPsConfigured(result);
})();
}
} else { } else {
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter( this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
() => this.getNotebookServerInfo(options), () => this.getNotebookServerInfo(options),

View File

@@ -16,6 +16,7 @@ import { useQueryCopilot } from "hooks/useQueryCopilot";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react"; import * as React from "react";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import shallow from "zustand/shallow";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useNotebook } from "../Notebook/useNotebook"; import { useNotebook } from "../Notebook/useNotebook";
@@ -37,7 +38,12 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
const [openItems, setOpenItems] = React.useState<TreeItemValue[]>([]); const [openItems, setOpenItems] = React.useState<TreeItemValue[]>([]);
const treeStyles = useTreeStyles(); const treeStyles = useTreeStyles();
const isNotebookEnabled = useNotebook((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. // We intentionally avoid using a state selector here because we want to re-render the tree if the active tab changes.
const { refreshActiveTab } = useTabs(); const { refreshActiveTab } = useTabs();

View File

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

View File

@@ -1,6 +1,5 @@
import _ from "underscore"; import _ from "underscore";
import { create } from "zustand"; import create, { UseStore } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
@@ -27,12 +26,10 @@ interface DatabasesState {
validateCollectionId: (databaseId: string, collectionId: string) => Promise<boolean>; validateCollectionId: (databaseId: string, collectionId: string) => Promise<boolean>;
} }
export const useDatabases = create<DatabasesState>()( export const useDatabases: UseStore<DatabasesState> = create((set, get) => ({
subscribeWithSelector( databases: [],
(set, get) => ({ resourceTokenCollection: undefined,
databases: [] as ViewModels.Database[], sampleDataResourceTokenCollection: undefined,
resourceTokenCollection: undefined as ViewModels.CollectionBase,
sampleDataResourceTokenCollection: undefined as ViewModels.CollectionBase,
updateDatabase: (updatedDatabase: ViewModels.Database) => updateDatabase: (updatedDatabase: ViewModels.Database) =>
set((state) => { set((state) => {
const updatedDatabases = state.databases.map((database: ViewModels.Database) => { const updatedDatabases = state.databases.map((database: ViewModels.Database) => {
@@ -168,6 +165,4 @@ export const useDatabases = create<DatabasesState>()(
await database.loadCollections(); await database.loadCollections();
return !database.collections().some((collection) => collection.id() === collectionId); return !database.collections().some((collection) => collection.id() === collectionId);
}, },
}) }));
)
);

View File

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

View File

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

View File

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

View File

@@ -104,7 +104,7 @@ describe("AuthorizationUtils", () => {
it("should return true if dataPlaneRbacEnabled is set to true and API supports RBAC", () => { it("should return true if dataPlaneRbacEnabled is set to true and API supports RBAC", () => {
setAadDataPlane(false); setAadDataPlane(false);
["SQL", "Tables", "Gremlin", "Mongo", "Cassandra"].forEach((type) => { ["SQL", "Tables", "Gremlin"].forEach((type) => {
updateUserContext({ updateUserContext({
dataPlaneRbacEnabled: true, dataPlaneRbacEnabled: true,
apiType: type as ApiType, apiType: type as ApiType,
@@ -115,7 +115,7 @@ describe("AuthorizationUtils", () => {
it("should return false if dataPlaneRbacEnabled is set to true and API does not support RBAC", () => { it("should return false if dataPlaneRbacEnabled is set to true and API does not support RBAC", () => {
setAadDataPlane(false); setAadDataPlane(false);
["Postgres", "VCoreMongo"].forEach((type) => { ["Mongo", "Cassandra", "Postgres", "VCoreMongo"].forEach((type) => {
updateUserContext({ updateUserContext({
dataPlaneRbacEnabled: true, dataPlaneRbacEnabled: true,
apiType: type as ApiType, apiType: type as ApiType,

View File

@@ -1,7 +1,6 @@
import * as msal from "@azure/msal-browser"; import * as msal from "@azure/msal-browser";
import { getEnvironmentScopeEndpoint } from "Common/EnvironmentUtility";
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import { hasProxyServer, isDataplaneRbacSupported } from "Utils/APITypeUtils"; import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
@@ -75,12 +74,10 @@ export async function acquireMsalTokenForAccount(
if (userContext.databaseAccount.properties?.documentEndpoint === undefined) { if (userContext.databaseAccount.properties?.documentEndpoint === undefined) {
throw new Error("Database account has no document endpoint defined"); throw new Error("Database account has no document endpoint defined");
} }
let hrefEndpoint = ""; const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
if (isDataplaneRbacEnabledForProxyApi(userContext)) { /\/+$/,
hrefEndpoint = getEnvironmentScopeEndpoint(); "/.default",
} else { );
hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(/\/+$/, "/.default");
}
const msalInstance = await getMsalInstance(); const msalInstance = await getMsalInstance();
const knownAccounts = msalInstance.getAllAccounts(); const knownAccounts = msalInstance.getAllAccounts();
// If user_hint is provided, we will try to use it to find the account. // If user_hint is provided, we will try to use it to find the account.
@@ -186,11 +183,7 @@ export async function acquireTokenWithMsal(
export function useDataplaneRbacAuthorization(userContext: UserContext): boolean { export function useDataplaneRbacAuthorization(userContext: UserContext): boolean {
return ( return (
userContext.features?.enableAadDataPlane || userContext.features.enableAadDataPlane ||
(userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType)) (userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType))
); );
} }
export function isDataplaneRbacEnabledForProxyApi(userContext: UserContext): boolean {
return useDataplaneRbacAuthorization(userContext) && hasProxyServer(userContext.apiType);
}

View File

@@ -1,5 +1,4 @@
import { create } from "zustand"; import create, { UseStore } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
interface CarouselState { interface CarouselState {
shouldOpen: boolean; shouldOpen: boolean;
@@ -10,15 +9,11 @@ interface CarouselState {
setShowCopilotCarousel: (showCopilotCarousel: boolean) => void; setShowCopilotCarousel: (showCopilotCarousel: boolean) => void;
} }
export const useCarousel = create<CarouselState>()( export const useCarousel: UseStore<CarouselState> = create((set) => ({
subscribeWithSelector(
(set) => ({
shouldOpen: false, shouldOpen: false,
showCoachMark: false, showCoachMark: false,
showCopilotCarousel: false, showCopilotCarousel: false,
setShouldOpen: (shouldOpen: boolean) => set({ shouldOpen }), setShouldOpen: (shouldOpen: boolean) => set({ shouldOpen }),
setShowCoachMark: (showCoachMark: boolean) => set({ showCoachMark }), setShowCoachMark: (showCoachMark: boolean) => set({ showCoachMark }),
setShowCopilotCarousel: (showCopilotCarousel: boolean) => set({ showCopilotCarousel }), setShowCopilotCarousel: (showCopilotCarousel: boolean) => set({ showCopilotCarousel }),
}) }));
)
);

View File

@@ -1,10 +1,10 @@
import { create } from "zustand"; import create, { UseStore } from "zustand";
interface ClientWriteEnabledState { interface ClientWriteEnabledState {
clientWriteEnabled: boolean; clientWriteEnabled: boolean;
setClientWriteEnabled: (writeEnabled: boolean) => void; setClientWriteEnabled: (writeEnabled: boolean) => void;
} }
export const useClientWriteEnabled = create<ClientWriteEnabledState>((set) => ({ export const useClientWriteEnabled: UseStore<ClientWriteEnabledState> = create((set) => ({
clientWriteEnabled: true, clientWriteEnabled: true,
setClientWriteEnabled: (clientWriteEnabled: boolean) => set({ clientWriteEnabled }), setClientWriteEnabled: (clientWriteEnabled: boolean) => set({ clientWriteEnabled }),
})); }));

View File

@@ -1,6 +1,6 @@
import { getDataTransferJobs } from "Common/dataAccess/dataTransfers"; import { getDataTransferJobs } from "Common/dataAccess/dataTransfers";
import { DataTransferJobGetResults } from "Utils/arm/generatedClients/dataTransferService/types"; import { DataTransferJobGetResults } from "Utils/arm/generatedClients/dataTransferService/types";
import { create } from "zustand"; import create, { UseStore } from "zustand";
export interface DataTransferJobsState { export interface DataTransferJobsState {
dataTransferJobs: DataTransferJobGetResults[]; dataTransferJobs: DataTransferJobGetResults[];
@@ -9,7 +9,9 @@ export interface DataTransferJobsState {
setPollingDataTransferJobs: (pollingDataTransferJobs: Set<string>) => void; setPollingDataTransferJobs: (pollingDataTransferJobs: Set<string>) => void;
} }
export const useDataTransferJobs = create<DataTransferJobsState>((set) => ({ type DataTransferJobStore = UseStore<DataTransferJobsState>;
export const useDataTransferJobs: DataTransferJobStore = create((set) => ({
dataTransferJobs: [], dataTransferJobs: [],
pollingDataTransferJobs: new Set<string>(), pollingDataTransferJobs: new Set<string>(),
setDataTransferJobs: (dataTransferJobs: DataTransferJobGetResults[]) => set({ dataTransferJobs }), setDataTransferJobs: (dataTransferJobs: DataTransferJobGetResults[]) => set({ dataTransferJobs }),

View File

@@ -1,5 +1,4 @@
import * as Constants from "Common/Constants"; import * as Constants from "Common/Constants";
import { getEnvironmentScopeEndpoint } from "Common/EnvironmentUtility";
import { createUri } from "Common/UrlUtility"; import { createUri } from "Common/UrlUtility";
import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract"; import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract";
import { FabricMessageTypes } from "Contracts/FabricMessageTypes"; import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
@@ -63,7 +62,6 @@ import {
acquireTokenWithMsal, acquireTokenWithMsal,
getAuthorizationHeader, getAuthorizationHeader,
getMsalInstance, getMsalInstance,
isDataplaneRbacEnabledForProxyApi,
} from "../Utils/AuthorizationUtils"; } from "../Utils/AuthorizationUtils";
import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation"; import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation";
import { get, getReadOnlyKeys, listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { get, getReadOnlyKeys, listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
@@ -333,12 +331,7 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0]; const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0];
let aadToken; let aadToken;
if (account.properties?.documentEndpoint) { if (account.properties?.documentEndpoint) {
let hrefEndpoint = ""; const hrefEndpoint = new URL(account.properties.documentEndpoint).href.replace(/\/$/, "/.default");
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
hrefEndpoint = getEnvironmentScopeEndpoint();
} else {
hrefEndpoint = new URL(account.properties.documentEndpoint).href.replace(/\/$/, "/.default");
}
const msalInstance = await getMsalInstance(); const msalInstance = await getMsalInstance();
const cachedAccount = msalInstance.getAllAccounts()?.[0]; const cachedAccount = msalInstance.getAllAccounts()?.[0];
msalInstance.setActiveAccount(cachedAccount); msalInstance.setActiveAccount(cachedAccount);

View File

@@ -1,4 +1,4 @@
import { create } from "zustand"; import create, { UseStore } from "zustand";
export interface NotebookSnapshotHooks { export interface NotebookSnapshotHooks {
snapshot?: string; snapshot?: string;
@@ -7,7 +7,7 @@ export interface NotebookSnapshotHooks {
setError: (error: string) => void; setError: (error: string) => void;
} }
export const useNotebookSnapshotStore = create<NotebookSnapshotHooks>((set) => ({ export const useNotebookSnapshotStore: UseStore<NotebookSnapshotHooks> = create((set) => ({
snapshot: undefined, snapshot: undefined,
error: undefined, error: undefined,
setSnapshot: (imageSrc: string) => set((state) => ({ ...state, snapshot: imageSrc })), setSnapshot: (imageSrc: string) => set((state) => ({ ...state, snapshot: imageSrc })),

View File

@@ -1,4 +1,4 @@
import { create } from "zustand"; import create, { UseStore } from "zustand";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/ConsoleData"; import { ConsoleData } from "../Explorer/Menus/NotificationConsole/ConsoleData";
export interface NotificationConsoleState { export interface NotificationConsoleState {
@@ -15,7 +15,7 @@ export interface NotificationConsoleState {
setConsoleAnimationFinished: (consoleAnimationFinished: boolean) => void; setConsoleAnimationFinished: (consoleAnimationFinished: boolean) => void;
} }
export const useNotificationConsole = create<NotificationConsoleState>((set) => ({ export const useNotificationConsole: UseStore<NotificationConsoleState> = create((set) => ({
isExpanded: false, isExpanded: false,
consoleData: undefined, consoleData: undefined,
inProgressConsoleDataIdToBeDeleted: "", inProgressConsoleDataIdToBeDeleted: "",

View File

@@ -1,5 +1,4 @@
import { create } from "zustand"; import create, { UseStore } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
interface TeachingBubbleState { interface TeachingBubbleState {
showPostgreTeachingBubble: boolean; showPostgreTeachingBubble: boolean;
@@ -8,13 +7,9 @@ interface TeachingBubbleState {
setShowResetPasswordBubble: (showResetPasswordBubble: boolean) => void; setShowResetPasswordBubble: (showResetPasswordBubble: boolean) => void;
} }
export const usePostgres = create<TeachingBubbleState>()( export const usePostgres: UseStore<TeachingBubbleState> = create((set) => ({
subscribeWithSelector(
(set) => ({
showPostgreTeachingBubble: false, showPostgreTeachingBubble: false,
showResetPasswordBubble: false, showResetPasswordBubble: false,
setShowPostgreTeachingBubble: (showPostgreTeachingBubble: boolean) => set({ showPostgreTeachingBubble }), setShowPostgreTeachingBubble: (showPostgreTeachingBubble: boolean) => set({ showPostgreTeachingBubble }),
setShowResetPasswordBubble: (showResetPasswordBubble: boolean) => set({ showResetPasswordBubble }), setShowResetPasswordBubble: (showResetPasswordBubble: boolean) => set({ showResetPasswordBubble }),
}) }));
)
);

View File

@@ -4,8 +4,7 @@ import { QueryResults } from "Contracts/ViewModels";
import { CopilotMessage, CopilotSchemaAllocationInfo } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces"; import { CopilotMessage, CopilotSchemaAllocationInfo } from "Explorer/QueryCopilot/Shared/QueryCopilotInterfaces";
import { guid } from "Explorer/Tables/Utilities"; import { guid } from "Explorer/Tables/Utilities";
import { useTabs } from "hooks/useTabs"; import { useTabs } from "hooks/useTabs";
import { create } from "zustand"; import create, { UseStore } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import { ContainerInfo } from "../Contracts/DataModels"; import { ContainerInfo } from "../Contracts/DataModels";
@@ -97,9 +96,9 @@ export interface QueryCopilotState {
resetQueryCopilotStates: () => void; resetQueryCopilotStates: () => void;
} }
export const useQueryCopilot = create<Partial<QueryCopilotState>>()( type QueryCopilotStore = UseStore<Partial<QueryCopilotState>>;
subscribeWithSelector(
(set) => ({ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
copilotEnabled: false, copilotEnabled: false,
copilotUserDBEnabled: false, copilotUserDBEnabled: false,
copilotSampleDBEnabled: false, copilotSampleDBEnabled: false,
@@ -111,15 +110,15 @@ export const useQueryCopilot = create<Partial<QueryCopilotState>>()(
correlationId: "", correlationId: "",
query: "SELECT * FROM c", query: "SELECT * FROM c",
selectedQuery: "", selectedQuery: "",
isGeneratingQuery: null as boolean, isGeneratingQuery: null,
isGeneratingExplanation: false, isGeneratingExplanation: false,
isExecuting: false, isExecuting: false,
dislikeQuery: undefined as (boolean | undefined), dislikeQuery: undefined,
showCallout: false, showCallout: false,
showSamplePrompts: false, showSamplePrompts: false,
queryIterator: undefined as MinimalQueryIterator | undefined, queryIterator: undefined,
queryResults: undefined as QueryResults | undefined, queryResults: undefined,
errors: [] as QueryError[], errors: [],
isSamplePromptsOpen: false, isSamplePromptsOpen: false,
showDeletePopup: false, showDeletePopup: false,
showFeedbackBar: false, showFeedbackBar: false,
@@ -130,23 +129,23 @@ export const useQueryCopilot = create<Partial<QueryCopilotState>>()(
wasCopilotUsed: false, wasCopilotUsed: false,
showWelcomeSidebar: true, showWelcomeSidebar: true,
showCopilotSidebar: false, showCopilotSidebar: false,
chatMessages: [] as CopilotMessage[], chatMessages: [],
shouldIncludeInMessages: true, shouldIncludeInMessages: true,
showExplanationBubble: false, showExplanationBubble: false,
notebookServerInfo: { notebookServerInfo: {
notebookServerEndpoint: undefined, notebookServerEndpoint: undefined,
authToken: undefined, authToken: undefined,
forwardingId: undefined, forwardingId: undefined,
} as DataModels.NotebookWorkspaceConnectionInfo, },
containerStatus: { containerStatus: {
status: undefined, status: undefined,
durationLeftInMinutes: undefined, durationLeftInMinutes: undefined,
phoenixServerInfo: undefined, phoenixServerInfo: undefined,
} as ContainerInfo, },
schemaAllocationInfo: { schemaAllocationInfo: {
databaseId: undefined, databaseId: undefined,
containerId: undefined, containerId: undefined,
} as CopilotSchemaAllocationInfo, },
isAllocatingContainer: false, isAllocatingContainer: false,
copilotEnabledforExecution: false, copilotEnabledforExecution: false,
@@ -257,6 +256,4 @@ export const useQueryCopilot = create<Partial<QueryCopilotState>>()(
isAllocatingContainer: false, isAllocatingContainer: false,
})); }));
}, },
}) }));
)
);

View File

@@ -1,4 +1,4 @@
import { create } from "zustand"; import create, { UseStore } from "zustand";
export interface SidePanelState { export interface SidePanelState {
isOpen: boolean; isOpen: boolean;
@@ -9,7 +9,7 @@ export interface SidePanelState {
closeSidePanel: () => void; closeSidePanel: () => void;
getRef?: React.RefObject<HTMLElement>; // Optional ref for focusing the last element. getRef?: React.RefObject<HTMLElement>; // Optional ref for focusing the last element.
} }
export const useSidePanel = create<SidePanelState>((set) => ({ export const useSidePanel: UseStore<SidePanelState> = create((set) => ({
isOpen: false, isOpen: false,
panelWidth: "440px", panelWidth: "440px",
openSidePanel: (headerText, panelContent, panelWidth = "440px") => openSidePanel: (headerText, panelContent, panelWidth = "440px") =>

View File

@@ -7,8 +7,7 @@ import {
OPEN_TABS_SUBCOMPONENT_NAME, OPEN_TABS_SUBCOMPONENT_NAME,
saveSubComponentState, saveSubComponentState,
} from "Shared/AppStatePersistenceUtility"; } from "Shared/AppStatePersistenceUtility";
import { create } from "zustand"; import create, { UseStore } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { CollectionTabKind } from "../Contracts/ViewModels"; import { CollectionTabKind } from "../Contracts/ViewModels";
import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab"; import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab";
@@ -52,9 +51,7 @@ export enum ReactTabKind {
QueryCopilot, QueryCopilot,
} }
export const useTabs = create<TabsState>()( export const useTabs: UseStore<TabsState> = create((set, get) => ({
subscribeWithSelector(
(set, get) => ({
openedTabs: [] as TabsBase[], openedTabs: [] as TabsBase[],
openedReactTabs: [ReactTabKind.Home], openedReactTabs: [ReactTabKind.Home],
activeTab: undefined as TabsBase, activeTab: undefined as TabsBase,
@@ -244,6 +241,4 @@ export const useTabs = create<TabsState>()(
openTabsStates, openTabsStates,
); );
}, },
}) }));
)
);

View File

@@ -1,5 +1,5 @@
import { Collection } from "Contracts/ViewModels"; import { Collection } from "Contracts/ViewModels";
import { create } from "zustand"; import create, { UseStore } from "zustand";
interface TeachingBubbleState { interface TeachingBubbleState {
step: number; step: number;
@@ -12,7 +12,7 @@ interface TeachingBubbleState {
setSampleCollection: (sampleCollection: Collection) => void; setSampleCollection: (sampleCollection: Collection) => void;
} }
export const useTeachingBubble = create<TeachingBubbleState>((set) => ({ export const useTeachingBubble: UseStore<TeachingBubbleState> = create((set) => ({
step: 1, step: 1,
isSampleDBExpanded: false, isSampleDBExpanded: false,
isDocumentsTabOpened: false, isDocumentsTabOpened: false,

View File

@@ -1,5 +1,5 @@
import postRobot from "post-robot"; import postRobot from "post-robot";
import { create } from "zustand"; import create, { UseStore } from "zustand";
interface TerminalState { interface TerminalState {
terminalWindow: Window; terminalWindow: Window;
@@ -7,7 +7,7 @@ interface TerminalState {
sendMessage: (message: string) => void; sendMessage: (message: string) => void;
} }
export const useTerminal = create<TerminalState>((set, get) => ({ export const useTerminal: UseStore<TerminalState> = create((set, get) => ({
terminalWindow: undefined, terminalWindow: undefined,
setTerminal: (terminalWindow: Window) => { setTerminal: (terminalWindow: Window) => {
set({ terminalWindow }); set({ terminalWindow });