Compare commits

...

8 Commits

Author SHA1 Message Date
Bikram Choudhury
695729a8b6 upgrade react to v18 and other packages 2025-10-01 17:18:51 +05:30
jawelton74
a5c3e6bea0 Preview site - update Node and dependencies. (#2218)
* Update to Node 20.

* Update vulnerable dependencies.
2025-09-29 06:17:29 -07:00
BChoudhury-ms
76e63818d3 Enable RBAC support for MongoDB and Cassandra APIs (#2198)
* enable RBAC support for Mongo & Cassandra API

* fix formatting issue

* Handling AAD integration for Mongo Shell

* remove empty aadToken error

* fix formatting issue

* added environment specific scope endpoints
2025-09-19 01:25:35 +05:30
Nishtha Ahuja
cfb5db4df6 Removed screenshot for mongo cloudshell (#2211)
Co-authored-by: nishthaAhujaa <nishtha17354@iiittd.ac.in>
2025-09-16 12:16:19 +05:30
Dmitry Shilov
922ca5c523 chore: Update help link in FabricHome component to point to the new documentation (#2206) 2025-09-04 11:58:07 +02:00
Dmitry Shilov
bafe002fa3 chore: Enhance accessibility (#2208)
- Add tabIndex to button
- Add aria attributes to icons and headings
2025-09-04 11:39:11 +02:00
vchske
0817acf404 Commenting or deleting UI references to Query Advisor (#2209)
* Commenting or deleting UI references to Query Advisor

* Removing (commenting out) QueryTabComponent from two views

* Added new splash screen button, commented out copilot prompt bar

* Fixing unit test
2025-08-28 15:47:29 -07:00
asier-isayas
8e2c46301d Allow Mongo users to change thee Guid Representation when conducting CRUD operations for documents (#2204)
* mongo guid representation

* format

* fix return type

---------

Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2025-08-18 12:30:04 -07:00
60 changed files with 16710 additions and 60259 deletions

23
.vscode/settings.json vendored
View File

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

37956
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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

36203
preview/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -138,6 +138,14 @@ 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";
@@ -255,6 +263,7 @@ export class HttpHeaders {
public static activityId: string = "x-ms-activity-id"; public static activityId: string = "x-ms-activity-id";
public static apiType: string = "x-ms-cosmos-apitype"; public static apiType: string = "x-ms-cosmos-apitype";
public static authorization: string = "authorization"; public static authorization: string = "authorization";
public static entraIdToken: string = "x-ms-entraid-token";
public static collectionIndexTransformationProgress: string = public static collectionIndexTransformationProgress: string =
"x-ms-documentdb-collection-index-transformation-progress"; "x-ms-documentdb-collection-index-transformation-progress";
public static continuation: string = "x-ms-continuation"; public static continuation: string = "x-ms-continuation";
@@ -765,3 +774,10 @@ export const ShortenedQueryCopilotSampleContainerSchema = {
userPrompt: "find all products", userPrompt: "find all products",
}; };
export enum MongoGuidRepresentation {
Standard = "Standard",
CSharpLegacy = "CSharpLegacy",
JavaLegacy = "JavaLegacy",
PythonLegacy = "PythonLegacy",
}

View File

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

View File

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

View File

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

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, { UseStore } from "zustand"; import { create } from "zustand";
export interface DialogState { export interface DialogState {
visible: boolean; visible: boolean;
@@ -38,7 +38,7 @@ export interface DialogState {
showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps) => void; showOkModalDialog: (title: string, subText: string, linkProps?: LinkProps) => void;
} }
export const useDialog: UseStore<DialogState> = create((set, get) => ({ export const useDialog = create<DialogState>((set, get) => ({
visible: false, visible: false,
openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })), openDialog: (props: DialogProps) => set(() => ({ visible: true, dialogProps: props })),
closeDialog: () => closeDialog: () =>

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(
() => this.refreshCommandBarButtons(),
(state) => state.isNotebooksEnabledForAccount, (state) => state.isNotebooksEnabledForAccount,
() => this.refreshCommandBarButtons(),
); );
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(
async () => this.initiateAndRefreshNotebookList(),
(state) => [state.isNotebookEnabled, state.isRefreshed], (state) => [state.isNotebookEnabled, state.isRefreshed],
shallow, async () => this.initiateAndRefreshNotebookList(),
{ equalityFn: shallow },
); );
this.resourceTree = new ResourceTreeAdapter(this); this.resourceTree = new ResourceTreeAdapter(this);

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility"; import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { PhoenixClient } from "Phoenix/PhoenixClient"; import { PhoenixClient } from "Phoenix/PhoenixClient";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import create, { UseStore } from "zustand"; import { create } from "zustand";
import { subscribeWithSelector } from 'zustand/middleware';
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { ConnectionStatusType, HttpStatusCodes } from "../../Common/Constants"; import { ConnectionStatusType, HttpStatusCodes } from "../../Common/Constants";
@@ -66,270 +67,274 @@ interface NotebookState {
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void; setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
} }
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({ export const useNotebook = create<NotebookState>()(
isNotebookEnabled: false, subscribeWithSelector(
isNotebooksEnabledForAccount: false, (set, get) => ({
notebookServerInfo: { isNotebookEnabled: false,
notebookServerEndpoint: undefined, isNotebooksEnabledForAccount: false,
authToken: undefined, notebookServerInfo: {
forwardingId: undefined, notebookServerEndpoint: "",
}, authToken: "",
sparkClusterConnectionInfo: { forwardingId: "",
userName: undefined, },
password: undefined, sparkClusterConnectionInfo: {
endpoints: [], userName: "",
}, password: "",
isSynapseLinkUpdating: false, endpoints: [] as DataModels.SparkClusterEndpoint[],
memoryUsageInfo: undefined, },
isShellEnabled: false, isSynapseLinkUpdating: false,
notebookBasePath: Constants.Notebook.defaultBasePath, memoryUsageInfo: undefined as DataModels.MemoryUsageInfo,
isInitializingNotebooks: false, isShellEnabled: false,
myNotebooksContentRoot: undefined, notebookBasePath: Constants.Notebook.defaultBasePath,
gitHubNotebooksContentRoot: undefined, isInitializingNotebooks: false,
galleryContentRoot: undefined, myNotebooksContentRoot: undefined as NotebookContentItem,
connectionInfo: { gitHubNotebooksContentRoot: undefined as NotebookContentItem,
status: ConnectionStatusType.Connect, galleryContentRoot: undefined as NotebookContentItem,
}, connectionInfo: {
notebookFolderName: undefined, status: ConnectionStatusType.Connect,
isAllocating: false, },
isRefreshed: false, notebookFolderName: "",
containerStatus: { isAllocating: false,
status: undefined, isRefreshed: false,
durationLeftInMinutes: undefined, containerStatus: {
phoenixServerInfo: undefined, status: undefined,
}, durationLeftInMinutes: undefined,
isPhoenixNotebooks: undefined, phoenixServerInfo: undefined,
isPhoenixFeatures: undefined, } as ContainerInfo,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }), isPhoenixNotebooks: undefined as boolean,
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }), isPhoenixFeatures: undefined as boolean,
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
set({ notebookServerInfo }), setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) => setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
set({ sparkClusterConnectionInfo }), set({ notebookServerInfo }),
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }), setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) =>
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }), set({ sparkClusterConnectionInfo }),
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }), setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }), setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }), setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => { setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
await get().getPhoenixStatus(); setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
const { databaseAccount, authType } = userContext; refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
if ( await get().getPhoenixStatus();
authType === AuthType.EncryptedToken || const { databaseAccount, authType } = userContext;
authType === AuthType.ResourceToken || if (
authType === AuthType.MasterKey authType === AuthType.EncryptedToken ||
) { authType === AuthType.ResourceToken ||
set({ isNotebooksEnabledForAccount: false }); authType === AuthType.MasterKey
return; ) {
} set({ isNotebooksEnabledForAccount: false });
return;
const firstWriteLocation =
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
? databaseAccount?.location
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`;
const authorizationHeader = getAuthorizationHeader();
try {
const response = await fetch(disallowedLocationsUri, {
method: "POST",
body: JSON.stringify({
resourceTypes: [Constants.ArmResourceTypes.notebookWorkspaces],
}),
headers: {
[authorizationHeader.header]: authorizationHeader.token,
[Constants.HttpHeaders.contentType]: "application/json",
},
});
if (!response.ok) {
throw new Error("Failed to fetch disallowed locations");
}
const disallowedLocations: string[] = await response.json();
if (!disallowedLocations) {
Logger.logInfo("No disallowed locations found", "Explorer/isNotebooksEnabledForAccount");
set({ isNotebooksEnabledForAccount: true });
return;
}
// firstWriteLocation should not be disallowed
const isAccountInAllowedLocation = firstWriteLocation && disallowedLocations.indexOf(firstWriteLocation) === -1;
set({ isNotebooksEnabledForAccount: isAccountInAllowedLocation });
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
set({ isNotebooksEnabledForAccount: false });
}
},
findItem: (root: NotebookContentItem, item: NotebookContentItem): NotebookContentItem => {
const currentItem = root || get().myNotebooksContentRoot;
if (currentItem) {
if (currentItem.path === item.path && currentItem.name === item.name) {
return currentItem;
}
if (currentItem.children) {
for (const childItem of currentItem.children) {
const result = get().findItem(childItem, item);
if (result) {
return result;
}
} }
}
}
return undefined; const firstWriteLocation =
}, userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => { ? databaseAccount?.location
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); : databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
const parentItem = get().findItem(root, parent); const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`;
item.parent = parentItem; const authorizationHeader = getAuthorizationHeader();
if (parentItem.children) { try {
parentItem.children.push(item); const response = await fetch(disallowedLocationsUri, {
} else { method: "POST",
parentItem.children = [item]; body: JSON.stringify({
} resourceTypes: [Constants.ArmResourceTypes.notebookWorkspaces],
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); }),
}, headers: {
updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => { [authorizationHeader.header]: authorizationHeader.token,
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); [Constants.HttpHeaders.contentType]: "application/json",
const parentItem = get().findItem(root, item.parent); },
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
parentItem.children.push(item);
item.parent = parentItem;
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, item.parent);
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
const notebookFolderName = get().isPhoenixNotebooks ? "Temporary Notebooks" : "My Notebooks";
set({ notebookFolderName });
const myNotebooksContentRoot = {
name: get().notebookFolderName,
path: get().notebookBasePath,
type: NotebookContentItemType.Directory,
};
const galleryContentRoot = {
name: "Gallery",
path: "Gallery",
type: NotebookContentItemType.File,
};
const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn()
? {
name: "GitHub repos",
path: "PsuedoDir",
type: NotebookContentItemType.Directory,
}
: undefined;
set({
myNotebooksContentRoot,
galleryContentRoot,
gitHubNotebooksContentRoot,
});
if (get().notebookServerInfo?.notebookServerEndpoint) {
const updatedRoot = await notebookManager?.notebookContentClient?.updateItemChildren(myNotebooksContentRoot);
set({ myNotebooksContentRoot: updatedRoot });
if (updatedRoot?.children) {
// Count 1st generation children (tree is lazy-loaded)
const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
updatedRoot.children.forEach((notebookItem) => {
switch (notebookItem.type) {
case NotebookContentItemType.File:
nodeCounts.files++;
break;
case NotebookContentItemType.Directory:
nodeCounts.directories++;
break;
case NotebookContentItemType.Notebook:
nodeCounts.notebooks++;
break;
default:
break;
}
});
TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts });
}
}
},
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]): void => {
const gitHubNotebooksContentRoot = cloneDeep(get().gitHubNotebooksContentRoot);
if (gitHubNotebooksContentRoot) {
gitHubNotebooksContentRoot.children = [];
pinnedRepos?.forEach((pinnedRepo) => {
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
const repoTreeItem: NotebookContentItem = {
name: repoFullName,
path: "PsuedoDir",
type: NotebookContentItemType.Directory,
children: [],
parent: gitHubNotebooksContentRoot,
};
pinnedRepo.branches.forEach((branch) => {
repoTreeItem.children.push({
name: branch.name,
path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""),
type: NotebookContentItemType.Directory,
parent: repoTreeItem,
}); });
if (!response.ok) {
throw new Error("Failed to fetch disallowed locations");
}
const disallowedLocations: string[] = await response.json();
if (!disallowedLocations) {
Logger.logInfo("No disallowed locations found", "Explorer/isNotebooksEnabledForAccount");
set({ isNotebooksEnabledForAccount: true });
return;
}
// firstWriteLocation should not be disallowed
const isAccountInAllowedLocation = firstWriteLocation && disallowedLocations.indexOf(firstWriteLocation) === -1;
set({ isNotebooksEnabledForAccount: isAccountInAllowedLocation });
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
set({ isNotebooksEnabledForAccount: false });
}
},
findItem: (root: NotebookContentItem, item: NotebookContentItem): NotebookContentItem => {
const currentItem = root || get().myNotebooksContentRoot;
if (currentItem) {
if (currentItem.path === item.path && currentItem.name === item.name) {
return currentItem;
}
if (currentItem.children) {
for (const childItem of currentItem.children) {
const result = get().findItem(childItem, item);
if (result) {
return result;
}
}
}
}
return undefined;
},
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => {
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, parent);
item.parent = parentItem;
if (parentItem.children) {
parentItem.children.push(item);
} else {
parentItem.children = [item];
}
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, item.parent);
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
parentItem.children.push(item);
item.parent = parentItem;
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => {
const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot);
const parentItem = get().findItem(root, item.parent);
parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
},
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
const notebookFolderName = get().isPhoenixNotebooks ? "Temporary Notebooks" : "My Notebooks";
set({ notebookFolderName });
const myNotebooksContentRoot = {
name: get().notebookFolderName,
path: get().notebookBasePath,
type: NotebookContentItemType.Directory,
};
const galleryContentRoot = {
name: "Gallery",
path: "Gallery",
type: NotebookContentItemType.File,
};
const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn()
? {
name: "GitHub repos",
path: "PsuedoDir",
type: NotebookContentItemType.Directory,
}
: undefined;
set({
myNotebooksContentRoot,
galleryContentRoot,
gitHubNotebooksContentRoot,
}); });
gitHubNotebooksContentRoot.children.push(repoTreeItem); if (get().notebookServerInfo?.notebookServerEndpoint) {
}); const updatedRoot = await notebookManager?.notebookContentClient?.updateItemChildren(myNotebooksContentRoot);
set({ myNotebooksContentRoot: updatedRoot });
set({ gitHubNotebooksContentRoot }); if (updatedRoot?.children) {
} // Count 1st generation children (tree is lazy-loaded)
}, const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }), updatedRoot.children.forEach((notebookItem) => {
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }), switch (notebookItem.type) {
resetContainerConnection: (connectionStatus: ContainerConnectionInfo): void => { case NotebookContentItemType.File:
useTabs.getState().closeAllNotebookTabs(true); nodeCounts.files++;
useNotebook.getState().setConnectionInfo(connectionStatus); break;
useNotebook.getState().setNotebookServerInfo(undefined); case NotebookContentItemType.Directory:
useNotebook.getState().setIsAllocating(false); nodeCounts.directories++;
useNotebook.getState().setContainerStatus({ break;
status: undefined, case NotebookContentItemType.Notebook:
durationLeftInMinutes: undefined, nodeCounts.notebooks++;
phoenixServerInfo: undefined, break;
}); default:
}, break;
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }), }
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }), });
getPhoenixStatus: async () => { TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts });
if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) { }
let isPhoenixNotebooks = false;
let isPhoenixFeatures = false;
const isPublicInternetAllowed = isPublicInternetAccessAllowed();
const phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
const dbAccountAllowedInfo = await phoenixClient.getDbAccountAllowedStatus();
if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) {
if (dbAccountAllowedInfo?.type === PhoenixErrorType.PhoenixFlightFallback) {
isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks === true;
isPhoenixFeatures =
isPublicInternetAllowed &&
// phoenix needs to be enabled for Postgres and VCoreMongo accounts since the PSQL and mongo shell requires phoenix containers
(userContext.features.phoenixFeatures === true ||
userContext.apiType === "Postgres" ||
userContext.apiType === "VCoreMongo");
} else {
isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed;
} }
} else { },
isPhoenixNotebooks = isPhoenixFeatures = false; initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]): void => {
} const gitHubNotebooksContentRoot = cloneDeep(get().gitHubNotebooksContentRoot);
set({ isPhoenixNotebooks: isPhoenixNotebooks }); if (gitHubNotebooksContentRoot) {
set({ isPhoenixFeatures: isPhoenixFeatures }); gitHubNotebooksContentRoot.children = [];
} pinnedRepos?.forEach((pinnedRepo) => {
}, const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }), const repoTreeItem: NotebookContentItem = {
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }), name: repoFullName,
})); path: "PsuedoDir",
type: NotebookContentItemType.Directory,
children: [],
parent: gitHubNotebooksContentRoot,
};
pinnedRepo.branches.forEach((branch) => {
repoTreeItem.children.push({
name: branch.name,
path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""),
type: NotebookContentItemType.Directory,
parent: repoTreeItem,
});
});
gitHubNotebooksContentRoot.children.push(repoTreeItem);
});
set({ gitHubNotebooksContentRoot });
}
},
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
resetContainerConnection: (connectionStatus: ContainerConnectionInfo): void => {
useTabs.getState().closeAllNotebookTabs(true);
useNotebook.getState().setConnectionInfo(connectionStatus);
useNotebook.getState().setNotebookServerInfo(undefined);
useNotebook.getState().setIsAllocating(false);
useNotebook.getState().setContainerStatus({
status: undefined,
durationLeftInMinutes: undefined,
phoenixServerInfo: undefined,
});
},
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
getPhoenixStatus: async () => {
if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) {
let isPhoenixNotebooks = false;
let isPhoenixFeatures = false;
const isPublicInternetAllowed = isPublicInternetAccessAllowed();
const phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
const dbAccountAllowedInfo = await phoenixClient.getDbAccountAllowedStatus();
if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) {
if (dbAccountAllowedInfo?.type === PhoenixErrorType.PhoenixFlightFallback) {
isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks === true;
isPhoenixFeatures =
isPublicInternetAllowed &&
// phoenix needs to be enabled for Postgres and VCoreMongo accounts since the PSQL and mongo shell requires phoenix containers
(userContext.features.phoenixFeatures === true ||
userContext.apiType === "Postgres" ||
userContext.apiType === "VCoreMongo");
} else {
isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed;
}
} else {
isPhoenixNotebooks = isPhoenixFeatures = false;
}
set({ isPhoenixNotebooks: isPhoenixNotebooks });
set({ isPhoenixFeatures: isPhoenixFeatures });
}
},
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }),
})
)
);

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 } from "../../Utils/AuthorizationUtils"; import { getAuthorizationHeader, isDataplaneRbacEnabledForProxyApi } from "../../Utils/AuthorizationUtils";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
@@ -551,6 +551,10 @@ export class CassandraAPIDataClient extends TableDataClient {
const authorizationHeaderMetadata: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader(); const authorizationHeaderMetadata: ViewModels.AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
xhr.setRequestHeader(authorizationHeaderMetadata.header, authorizationHeaderMetadata.token); xhr.setRequestHeader(authorizationHeaderMetadata.header, authorizationHeaderMetadata.token);
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
xhr.setRequestHeader(Constants.HttpHeaders.entraIdToken, userContext.aadToken);
}
return true; return true;
}; };

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,12 +14,17 @@ 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
@@ -40,6 +45,14 @@ 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.
* *
@@ -64,7 +77,7 @@ export abstract class AbstractShellHandler {
START_MARKER, START_MARKER,
DISABLE_HISTORY, DISABLE_HISTORY,
...setupCommands, ...setupCommands,
`{ ${connectionCommand}; } || true;${EXIT_COMMAND}`, `{ ${connectionCommand}; } || true;${this.getExitCommand()}`,
]; ];
return allCommands.join("\n").concat("\n"); return allCommands.join("\n").concat("\n");
@@ -84,7 +97,7 @@ export abstract class AbstractShellHandler {
* is not already present in the environment. * is not already present in the environment.
*/ */
protected mongoShellSetupCommands(): string[] { protected mongoShellSetupCommands(): string[] {
const PACKAGE_VERSION: string = "2.5.5"; const PACKAGE_VERSION: string = "2.5.6";
return [ return [
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi", "if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`, `if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,

View File

@@ -18,6 +18,12 @@ interface DatabaseAccount {
interface UserContextType { interface UserContextType {
databaseAccount: DatabaseAccount; databaseAccount: DatabaseAccount;
features: {
enableAadDataPlane: boolean;
};
apiType: string;
dataPlaneRbacEnabled: boolean;
aadToken?: string;
} }
// Mock dependencies // Mock dependencies
@@ -29,6 +35,8 @@ jest.mock("../../../../UserContext", () => ({
mongoEndpoint: "https://test-mongo.documents.azure.com:443/", mongoEndpoint: "https://test-mongo.documents.azure.com:443/",
}, },
}, },
features: { enableAadDataPlane: false },
apiType: "Mongo",
}, },
})); }));
@@ -70,7 +78,7 @@ describe("MongoShellHandler", () => {
expect(Array.isArray(commands)).toBe(true); expect(Array.isArray(commands)).toBe(true);
expect(commands.length).toBe(7); expect(commands.length).toBe(7);
expect(commands[1]).toContain("mongosh-2.5.5-linux-x64.tgz"); expect(commands[1]).toContain("mongosh-2.5.6-linux-x64.tgz");
}); });
}); });
@@ -88,11 +96,12 @@ describe("MongoShellHandler", () => {
kind: "test-kind", kind: "test-kind",
properties: { mongoEndpoint: "https://test-mongo.documents.azure.com:443/" }, properties: { mongoEndpoint: "https://test-mongo.documents.azure.com:443/" },
}; };
(userContext as UserContextType).dataPlaneRbacEnabled = false;
const command = mongoShellHandler.getConnectionCommand(); const command = mongoShellHandler.getConnectionCommand();
expect(command).toBe( expect(command).toBe(
'mongosh --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/");
@@ -115,12 +124,47 @@ 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,17 +1,29 @@
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 } from "./AbstractShellHandler"; import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND, EXIT_COMMAND_MONGO } from "./AbstractShellHandler";
export class MongoShellHandler extends AbstractShellHandler { export class MongoShellHandler extends AbstractShellHandler {
private _key: string; private _key: string;
private _endpoint: string | undefined; private _endpoint: string | undefined;
private _removeInfoText: string[] = getMongoShellRemoveInfoText(); private _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";
} }
@@ -29,25 +41,21 @@ export class MongoShellHandler extends AbstractShellHandler {
if (!dbName) { if (!dbName) {
return "echo 'Database name not found.'"; return "echo 'Database name not found.'";
} }
return ( const connectionCommand = this._isEntraIdEnabled
DISABLE_TELEMETRY_COMMAND + ? this._getAadConnectionCommand(dbName)
" && " + : this._getKeyConnectionCommand(dbName);
"mongosh mongodb://" + const fullCommand = `${DISABLE_TELEMETRY_COMMAND}; ${connectionCommand}`;
getHostFromUrl(this._endpoint) + return fullCommand;
":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,12 +7,24 @@ import { PostgresShellHandler } from "./PostgresShellHandler";
import { getHandler, getKey } from "./ShellTypeFactory"; import { getHandler, getKey } from "./ShellTypeFactory";
import { VCoreMongoShellHandler } from "./VCoreMongoShellHandler"; import { VCoreMongoShellHandler } from "./VCoreMongoShellHandler";
interface UserContextType {
databaseAccount: { name: string };
subscriptionId: string;
resourceGroup: string;
features: { enableAadDataPlane: boolean };
dataPlaneRbacEnabled: boolean;
aadToken?: string;
apiType?: string;
}
// Mock dependencies // Mock dependencies
jest.mock("../../../../UserContext", () => ({ jest.mock("../../../../UserContext", () => ({
userContext: { userContext: {
databaseAccount: { name: "testDbName" }, databaseAccount: { name: "testDbName" },
subscriptionId: "testSubId", subscriptionId: "testSubId",
resourceGroup: "testResourceGroup", resourceGroup: "testResourceGroup",
features: { enableAadDataPlane: false },
dataPlaneRbacEnabled: false,
}, },
})); }));
@@ -109,5 +121,33 @@ describe("ShellTypeHandlerFactory", () => {
expect(key).toBe(mockKey); expect(key).toBe(mockKey);
expect(listKeys).toHaveBeenCalledWith("testSubId", "testResourceGroup", "testDbName"); expect(listKeys).toHaveBeenCalledWith("testSubId", "testResourceGroup", "testDbName");
}); });
it("should return MongoShellHandler with primaryMasterKey for TerminalKind.Mongo when RBAC is disabled", async () => {
(listKeys as jest.Mock).mockResolvedValue({ primaryMasterKey: "primaryKey123" });
(userContext as UserContextType).features.enableAadDataPlane = false;
(userContext as UserContextType).dataPlaneRbacEnabled = false;
const handler = await getHandler(TerminalKind.Mongo);
expect(handler).toBeInstanceOf(MongoShellHandler);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(handler.key).toBe("primaryKey123");
});
it("should return MongoShellHandler with aadToken for TerminalKind.Mongo when RBAC is enabled", async () => {
(userContext as UserContextType).aadToken = "aadToken123";
(userContext as UserContextType).features.enableAadDataPlane = true;
(userContext as UserContextType).dataPlaneRbacEnabled = true;
(userContext as UserContextType).apiType = "Mongo";
const handler = await getHandler(TerminalKind.Mongo);
expect(handler).toBeInstanceOf(MongoShellHandler);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
expect(handler.key).toBe("aadToken123");
});
it("should throw error for unsupported shell type", async () => {
await expect(getHandler("UnknownShell" as unknown as TerminalKind)).rejects.toThrow(
"Unsupported shell type: UnknownShell",
);
});
}); });
}); });

View File

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

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.5-linux-x64.tgz"); expect(commands[1]).toContain("mongosh-2.5.6-linux-x64.tgz");
expect(commands[0]).toContain("mongosh not found"); expect(commands[0]).toContain("mongosh not found");
}); });

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

View File

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

View File

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

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

View File

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

View File

@@ -9,7 +9,6 @@ import { useDialog } from "Explorer/Controls/Dialog";
import { monaco } from "Explorer/LazyMonaco"; import { monaco } from "Explorer/LazyMonaco";
import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal"; import { QueryCopilotFeedbackModal } from "Explorer/QueryCopilot/Modal/QueryCopilotFeedbackModal";
import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext"; import { useCopilotStore } from "Explorer/QueryCopilot/QueryCopilotContext";
import { QueryCopilotPromptbar } from "Explorer/QueryCopilot/QueryCopilotPromptbar";
import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import { readCopilotToggleStatus, saveCopilotToggleStatus } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { OnExecuteQueryClick, QueryDocumentsPerPage } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar"; import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar";
@@ -28,8 +27,9 @@ import { TabsState, useTabs } from "hooks/useTabs";
import React, { Fragment, createRef } from "react"; import React, { Fragment, createRef } from "react";
import "react-splitter-layout/lib/index.css"; import "react-splitter-layout/lib/index.css";
import { format } from "react-string-format"; import { format } from "react-string-format";
import QueryCommandIcon from "../../../../images/CopilotCommand.svg"; //TODO: Uncomment next two lines when query copilot is reinstated in DE
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg"; // import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
// import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg"; import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
import CancelQueryIcon from "../../../../images/Entity_cancel.svg"; import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
@@ -494,53 +494,55 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
}); });
} }
if (this.launchCopilotButton.visible && this.isCopilotTabActive) { //TODO: Uncomment next section when query copilot is reinstated in DE
const mainButtonLabel = "Launch Copilot"; // if (this.launchCopilotButton.visible && this.isCopilotTabActive) {
const chatPaneLabel = "Open Copilot in chat pane (ALT+C)"; // const mainButtonLabel = "Launch Copilot";
const copilotSettingLabel = "Copilot settings"; // const chatPaneLabel = "Open Copilot in chat pane (ALT+C)";
// const copilotSettingLabel = "Copilot settings";
const openCopilotChatButton: CommandButtonComponentProps = { // const openCopilotChatButton: CommandButtonComponentProps = {
iconAlt: chatPaneLabel, // iconAlt: chatPaneLabel,
onCommandClick: this.launchQueryCopilotChat, // onCommandClick: this.launchQueryCopilotChat,
commandButtonLabel: chatPaneLabel, // commandButtonLabel: chatPaneLabel,
ariaLabel: chatPaneLabel, // ariaLabel: chatPaneLabel,
hasPopup: false, // hasPopup: false,
}; // };
const copilotSettingsButton: CommandButtonComponentProps = { // const copilotSettingsButton: CommandButtonComponentProps = {
iconAlt: copilotSettingLabel, // iconAlt: copilotSettingLabel,
onCommandClick: () => undefined, // onCommandClick: () => undefined,
commandButtonLabel: copilotSettingLabel, // commandButtonLabel: copilotSettingLabel,
ariaLabel: copilotSettingLabel, // ariaLabel: copilotSettingLabel,
hasPopup: false, // hasPopup: false,
}; // };
const launchCopilotButton: CommandButtonComponentProps = { // const launchCopilotButton: CommandButtonComponentProps = {
iconSrc: LaunchCopilot, // iconSrc: LaunchCopilot,
iconAlt: mainButtonLabel, // iconAlt: mainButtonLabel,
onCommandClick: this.launchQueryCopilotChat, // onCommandClick: this.launchQueryCopilotChat,
commandButtonLabel: mainButtonLabel, // commandButtonLabel: mainButtonLabel,
ariaLabel: mainButtonLabel, // ariaLabel: mainButtonLabel,
hasPopup: false, // hasPopup: false,
children: [openCopilotChatButton, copilotSettingsButton], // children: [openCopilotChatButton, copilotSettingsButton],
}; // };
buttons.push(launchCopilotButton); // buttons.push(launchCopilotButton);
} // }
if (this.props.copilotEnabled) { //TODO: Uncomment next section when query copilot is reinstated in DE
const toggleCopilotButton: CommandButtonComponentProps = { // if (this.props.copilotEnabled) {
iconSrc: QueryCommandIcon, // const toggleCopilotButton: CommandButtonComponentProps = {
iconAlt: "Query Advisor", // iconSrc: QueryCommandIcon,
keyboardAction: KeyboardAction.TOGGLE_COPILOT, // iconAlt: "Query Advisor",
onCommandClick: () => { // keyboardAction: KeyboardAction.TOGGLE_COPILOT,
this._toggleCopilot(!this.state.copilotActive); // onCommandClick: () => {
}, // this._toggleCopilot(!this.state.copilotActive);
commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor", // },
ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor", // commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
hasPopup: false, // ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
}; // hasPopup: false,
buttons.push(toggleCopilotButton); // };
} // buttons.push(toggleCopilotButton);
// }
if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) { if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) {
const label = "Cancel query"; const label = "Cancel query";
@@ -725,6 +727,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
return ( return (
<Fragment> <Fragment>
<CosmosFluentProvider id={this.props.tabId} className={this.props.styles.queryTab} role="tabpanel"> <CosmosFluentProvider id={this.props.tabId} className={this.props.styles.queryTab} role="tabpanel">
{/*TODO: Uncomment this section when query copilot is reinstated in DE
{this.props.copilotEnabled && this.state.currentTabActive && this.state.copilotActive && ( {this.props.copilotEnabled && this.state.currentTabActive && this.state.copilotActive && (
<QueryCopilotPromptbar <QueryCopilotPromptbar
explorer={this.props.collection.container} explorer={this.props.collection.container}
@@ -732,7 +735,7 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
databaseId={this.props.collection.databaseId} databaseId={this.props.collection.databaseId}
containerId={this.props.collection.id()} containerId={this.props.collection.id()}
></QueryCopilotPromptbar> ></QueryCopilotPromptbar>
)} )} */}
{/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */} {/* Set 'key' to the value of vertical to force re-rendering when vertical changes, to work around https://github.com/johnwalley/allotment/issues/457 */}
<Allotment <Allotment
key={vertical.toString()} key={vertical.toString()}

View File

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

View File

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

View File

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

View File

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

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, { UseStore } from "zustand"; import { create } from "zustand";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { useTabs } from "../hooks/useTabs"; import { useTabs } from "../hooks/useTabs";
export interface SelectedNodeState { export interface SelectedNodeState {
@@ -17,7 +17,7 @@ export interface SelectedNodeState {
isQueryCopilotCollectionSelected: () => boolean; isQueryCopilotCollectionSelected: () => boolean;
} }
export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) => ({ export const useSelectedNode = create<SelectedNodeState>((set, get) => ({
selectedNode: undefined, selectedNode: undefined,
setSelectedNode: (node: ViewModels.TreeNode) => set({ selectedNode: node }), setSelectedNode: (node: ViewModels.TreeNode) => set({ selectedNode: node }),
isDatabaseNodeOrNoneSelected: (): boolean => { isDatabaseNodeOrNoneSelected: (): boolean => {

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, { UseStore } from "zustand"; import { create } from "zustand";
/** /**
* Represents a keyboard shortcut handler. * Represents a keyboard shortcut handler.
@@ -126,7 +126,7 @@ export const clearKeyboardActionGroup = (group: KeyboardActionGroup) => {
useKeyboardActionHandlers.getState().setHandlers(group, {}); useKeyboardActionHandlers.getState().setHandlers(group, {});
}; };
const useKeyboardActionHandlers: UseStore<KeyboardShortcutState> = create((set, get) => ({ const useKeyboardActionHandlers = create<KeyboardShortcutState>((set, get) => ({
allHandlers: {}, allHandlers: {},
groups: {}, groups: {},
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => { setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => {

View File

@@ -1,3 +1,4 @@
import { MongoGuidRepresentation } from "Common/Constants";
import { SplitterDirection } from "Common/Splitter"; import { SplitterDirection } from "Common/Splitter";
import * as LocalStorageUtility from "./LocalStorageUtility"; import * as LocalStorageUtility from "./LocalStorageUtility";
import * as SessionStorageUtility from "./SessionStorageUtility"; import * as SessionStorageUtility from "./SessionStorageUtility";
@@ -33,6 +34,7 @@ export enum StorageKey {
DocumentsTabPrefs, DocumentsTabPrefs,
DefaultQueryResultsView, DefaultQueryResultsView,
AppState, AppState,
MongoGuidRepresentation,
} }
export const hasRUThresholdBeenConfigured = (): boolean => { export const hasRUThresholdBeenConfigured = (): boolean => {
@@ -65,4 +67,13 @@ export const getDefaultQueryResultsView = (): SplitterDirection => {
return SplitterDirection.Horizontal; return SplitterDirection.Horizontal;
}; };
export const getMongoGuidRepresentation = (): MongoGuidRepresentation => {
const mongoGuidRepresentation: string | null = LocalStorageUtility.getEntryString(StorageKey.MongoGuidRepresentation);
if (mongoGuidRepresentation) {
return mongoGuidRepresentation as MongoGuidRepresentation;
}
return MongoGuidRepresentation.CSharpLegacy;
};
export const DefaultRUThreshold = 5000; export const DefaultRUThreshold = 5000;

View File

@@ -91,5 +91,11 @@ export const getItemName = (): string => {
}; };
export const isDataplaneRbacSupported = (apiType: string): boolean => { export const isDataplaneRbacSupported = (apiType: string): boolean => {
return apiType === "SQL" || apiType === "Tables" || apiType === "Gremlin"; return (
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"].forEach((type) => { ["SQL", "Tables", "Gremlin", "Mongo", "Cassandra"].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);
["Mongo", "Cassandra", "Postgres", "VCoreMongo"].forEach((type) => { ["Postgres", "VCoreMongo"].forEach((type) => {
updateUserContext({ updateUserContext({
dataPlaneRbacEnabled: true, dataPlaneRbacEnabled: true,
apiType: type as ApiType, apiType: type as ApiType,

View File

@@ -1,6 +1,7 @@
import * as msal from "@azure/msal-browser"; import * as msal from "@azure/msal-browser";
import { getEnvironmentScopeEndpoint } from "Common/EnvironmentUtility";
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import { isDataplaneRbacSupported } from "Utils/APITypeUtils"; import { hasProxyServer, isDataplaneRbacSupported } from "Utils/APITypeUtils";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
@@ -74,10 +75,12 @@ export async function acquireMsalTokenForAccount(
if (userContext.databaseAccount.properties?.documentEndpoint === undefined) { if (userContext.databaseAccount.properties?.documentEndpoint === undefined) {
throw new Error("Database account has no document endpoint defined"); throw new Error("Database account has no document endpoint defined");
} }
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace( let hrefEndpoint = "";
/\/+$/, if (isDataplaneRbacEnabledForProxyApi(userContext)) {
"/.default", hrefEndpoint = getEnvironmentScopeEndpoint();
); } else {
hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(/\/+$/, "/.default");
}
const msalInstance = await getMsalInstance(); const msalInstance = await getMsalInstance();
const knownAccounts = msalInstance.getAllAccounts(); const knownAccounts = msalInstance.getAllAccounts();
// If user_hint is provided, we will try to use it to find the account. // If user_hint is provided, we will try to use it to find the account.
@@ -183,7 +186,11 @@ 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,4 +1,5 @@
import create, { UseStore } from "zustand"; import { create } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
interface CarouselState { interface CarouselState {
shouldOpen: boolean; shouldOpen: boolean;
@@ -9,11 +10,15 @@ interface CarouselState {
setShowCopilotCarousel: (showCopilotCarousel: boolean) => void; setShowCopilotCarousel: (showCopilotCarousel: boolean) => void;
} }
export const useCarousel: UseStore<CarouselState> = create((set) => ({ export const useCarousel = create<CarouselState>()(
shouldOpen: false, subscribeWithSelector(
showCoachMark: false, (set) => ({
showCopilotCarousel: false, shouldOpen: false,
setShouldOpen: (shouldOpen: boolean) => set({ shouldOpen }), showCoachMark: false,
setShowCoachMark: (showCoachMark: boolean) => set({ showCoachMark }), showCopilotCarousel: false,
setShowCopilotCarousel: (showCopilotCarousel: boolean) => set({ showCopilotCarousel }), setShouldOpen: (shouldOpen: boolean) => set({ shouldOpen }),
})); setShowCoachMark: (showCoachMark: boolean) => set({ showCoachMark }),
setShowCopilotCarousel: (showCopilotCarousel: boolean) => set({ showCopilotCarousel }),
})
)
);

View File

@@ -1,10 +1,10 @@
import create, { UseStore } from "zustand"; import { create } from "zustand";
interface ClientWriteEnabledState { interface ClientWriteEnabledState {
clientWriteEnabled: boolean; clientWriteEnabled: boolean;
setClientWriteEnabled: (writeEnabled: boolean) => void; setClientWriteEnabled: (writeEnabled: boolean) => void;
} }
export const useClientWriteEnabled: UseStore<ClientWriteEnabledState> = create((set) => ({ export const useClientWriteEnabled = create<ClientWriteEnabledState>((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, { UseStore } from "zustand"; import { create } from "zustand";
export interface DataTransferJobsState { export interface DataTransferJobsState {
dataTransferJobs: DataTransferJobGetResults[]; dataTransferJobs: DataTransferJobGetResults[];
@@ -9,9 +9,7 @@ export interface DataTransferJobsState {
setPollingDataTransferJobs: (pollingDataTransferJobs: Set<string>) => void; setPollingDataTransferJobs: (pollingDataTransferJobs: Set<string>) => void;
} }
type DataTransferJobStore = UseStore<DataTransferJobsState>; export const useDataTransferJobs = create<DataTransferJobsState>((set) => ({
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,4 +1,5 @@
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";
@@ -62,6 +63,7 @@ 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";
@@ -331,7 +333,12 @@ 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) {
const hrefEndpoint = new URL(account.properties.documentEndpoint).href.replace(/\/$/, "/.default"); let hrefEndpoint = "";
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, { UseStore } from "zustand"; import { create } 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: UseStore<NotebookSnapshotHooks> = create((set) => ({ export const useNotebookSnapshotStore = create<NotebookSnapshotHooks>((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, { UseStore } from "zustand"; import { create } 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: UseStore<NotificationConsoleState> = create((set) => ({ export const useNotificationConsole = create<NotificationConsoleState>((set) => ({
isExpanded: false, isExpanded: false,
consoleData: undefined, consoleData: undefined,
inProgressConsoleDataIdToBeDeleted: "", inProgressConsoleDataIdToBeDeleted: "",

View File

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

View File

@@ -4,7 +4,8 @@ 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, { UseStore } from "zustand"; import { create } 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";
@@ -96,120 +97,12 @@ export interface QueryCopilotState {
resetQueryCopilotStates: () => void; resetQueryCopilotStates: () => void;
} }
type QueryCopilotStore = UseStore<Partial<QueryCopilotState>>; export const useQueryCopilot = create<Partial<QueryCopilotState>>()(
subscribeWithSelector(
export const useQueryCopilot: QueryCopilotStore = create((set) => ({ (set) => ({
copilotEnabled: false, copilotEnabled: false,
copilotUserDBEnabled: false, copilotUserDBEnabled: false,
copilotSampleDBEnabled: false, copilotSampleDBEnabled: false,
generatedQuery: "",
likeQuery: false,
userPrompt: "",
showFeedbackModal: false,
hideFeedbackModalForLikedQueries: false,
correlationId: "",
query: "SELECT * FROM c",
selectedQuery: "",
isGeneratingQuery: null,
isGeneratingExplanation: false,
isExecuting: false,
dislikeQuery: undefined,
showCallout: false,
showSamplePrompts: false,
queryIterator: undefined,
queryResults: undefined,
errors: [],
isSamplePromptsOpen: false,
showDeletePopup: false,
showFeedbackBar: false,
showCopyPopup: false,
showErrorMessageBar: false,
showInvalidQueryMessageBar: false,
generatedQueryComments: "",
wasCopilotUsed: false,
showWelcomeSidebar: true,
showCopilotSidebar: false,
chatMessages: [],
shouldIncludeInMessages: true,
showExplanationBubble: false,
notebookServerInfo: {
notebookServerEndpoint: undefined,
authToken: undefined,
forwardingId: undefined,
},
containerStatus: {
status: undefined,
durationLeftInMinutes: undefined,
phoenixServerInfo: undefined,
},
schemaAllocationInfo: {
databaseId: undefined,
containerId: undefined,
},
isAllocatingContainer: false,
copilotEnabledforExecution: false,
setCopilotEnabled: (copilotEnabled: boolean) => set({ copilotEnabled }),
setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => set({ copilotUserDBEnabled }),
setCopilotSampleDBEnabled: (copilotSampleDBEnabled: boolean) => set({ copilotSampleDBEnabled }),
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
closeFeedbackModal: () => set({ showFeedbackModal: false }),
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
set({ hideFeedbackModalForLikedQueries }),
refreshCorrelationId: () => set({ correlationId: guid() }),
setUserPrompt: (userPrompt: string) => set({ userPrompt }),
setQuery: (query: string) => set({ query }),
setGeneratedQuery: (generatedQuery: string) => set({ generatedQuery }),
setSelectedQuery: (selectedQuery: string) => set({ selectedQuery }),
setIsGeneratingQuery: (isGeneratingQuery: boolean) => set({ isGeneratingQuery }),
setIsGeneratingExplanation: (isGeneratingExplanation: boolean) => set({ isGeneratingExplanation }),
setIsExecuting: (isExecuting: boolean) => set({ isExecuting }),
setLikeQuery: (likeQuery: boolean) => set({ likeQuery }),
setDislikeQuery: (dislikeQuery: boolean | undefined) => set({ dislikeQuery }),
setShowCallout: (showCallout: boolean) => set({ showCallout }),
setShowSamplePrompts: (showSamplePrompts: boolean) => set({ showSamplePrompts }),
setQueryIterator: (queryIterator: MinimalQueryIterator | undefined) => set({ queryIterator }),
setQueryResults: (queryResults: QueryResults | undefined) => set({ queryResults }),
setErrors: (errors: QueryError[]) => set({ errors }),
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }),
setShowDeletePopup: (showDeletePopup: boolean) => set({ showDeletePopup }),
setShowFeedbackBar: (showFeedbackBar: boolean) => set({ showFeedbackBar }),
setshowCopyPopup: (showCopyPopup: boolean) => set({ showCopyPopup }),
setShowErrorMessageBar: (showErrorMessageBar: boolean) => set({ showErrorMessageBar }),
setShowInvalidQueryMessageBar: (showInvalidQueryMessageBar: boolean) => set({ showInvalidQueryMessageBar }),
setGeneratedQueryComments: (generatedQueryComments: string) => set({ generatedQueryComments }),
setWasCopilotUsed: (wasCopilotUsed: boolean) => set({ wasCopilotUsed }),
setShowWelcomeSidebar: (showWelcomeSidebar: boolean) => set({ showWelcomeSidebar }),
setShowCopilotSidebar: (showCopilotSidebar: boolean) => set({ showCopilotSidebar }),
setChatMessages: (chatMessages: CopilotMessage[]) => set({ chatMessages }),
setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => set({ shouldIncludeInMessages }),
setShowExplanationBubble: (showExplanationBubble: boolean) => set({ showExplanationBubble }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
set({ notebookServerInfo }),
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
setIsAllocatingContainer: (isAllocatingContainer: boolean) => set({ isAllocatingContainer }),
setSchemaAllocationInfo: (schemaAllocationInfo: CopilotSchemaAllocationInfo) => set({ schemaAllocationInfo }),
setCopilotEnabledforExecution: (copilotEnabledforExecution: boolean) => set({ copilotEnabledforExecution }),
resetContainerConnection: (): void => {
useTabs.getState().closeAllNotebookTabs(true);
useQueryCopilot.getState().setNotebookServerInfo(undefined);
useQueryCopilot.getState().setIsAllocatingContainer(false);
useQueryCopilot.getState().setContainerStatus({
status: undefined,
durationLeftInMinutes: undefined,
phoenixServerInfo: undefined,
});
useQueryCopilot.getState().setSchemaAllocationInfo({
databaseId: undefined,
containerId: undefined,
});
},
resetQueryCopilotStates: () => {
set((state) => ({
...state,
generatedQuery: "", generatedQuery: "",
likeQuery: false, likeQuery: false,
userPrompt: "", userPrompt: "",
@@ -218,15 +111,15 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
correlationId: "", correlationId: "",
query: "SELECT * FROM c", query: "SELECT * FROM c",
selectedQuery: "", selectedQuery: "",
isGeneratingQuery: false, isGeneratingQuery: null as boolean,
isGeneratingExplanation: false, isGeneratingExplanation: false,
isExecuting: false, isExecuting: false,
dislikeQuery: undefined, dislikeQuery: undefined as (boolean | undefined),
showCallout: false, showCallout: false,
showSamplePrompts: false, showSamplePrompts: false,
queryIterator: undefined, queryIterator: undefined as MinimalQueryIterator | undefined,
queryResults: undefined, queryResults: undefined as QueryResults | undefined,
errors: [], errors: [] as QueryError[],
isSamplePromptsOpen: false, isSamplePromptsOpen: false,
showDeletePopup: false, showDeletePopup: false,
showFeedbackBar: false, showFeedbackBar: false,
@@ -235,25 +128,135 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
showInvalidQueryMessageBar: false, showInvalidQueryMessageBar: false,
generatedQueryComments: "", generatedQueryComments: "",
wasCopilotUsed: false, wasCopilotUsed: false,
showWelcomeSidebar: true,
showCopilotSidebar: false, showCopilotSidebar: false,
chatMessages: [], chatMessages: [] as CopilotMessage[],
shouldIncludeInMessages: true, shouldIncludeInMessages: true,
showExplanationBubble: false, showExplanationBubble: false,
notebookServerInfo: { notebookServerInfo: {
notebookServerEndpoint: undefined, notebookServerEndpoint: undefined,
authToken: undefined, authToken: undefined,
forwardingId: undefined, forwardingId: undefined,
}, } as DataModels.NotebookWorkspaceConnectionInfo,
containerStatus: { containerStatus: {
status: undefined, status: undefined,
durationLeftInMinutes: undefined, durationLeftInMinutes: undefined,
phoenixServerInfo: undefined, phoenixServerInfo: undefined,
}, } as ContainerInfo,
schemaAllocationInfo: { schemaAllocationInfo: {
databaseId: undefined, databaseId: undefined,
containerId: undefined, containerId: undefined,
}, } as CopilotSchemaAllocationInfo,
isAllocatingContainer: false, isAllocatingContainer: false,
})); copilotEnabledforExecution: false,
},
})); setCopilotEnabled: (copilotEnabled: boolean) => set({ copilotEnabled }),
setCopilotUserDBEnabled: (copilotUserDBEnabled: boolean) => set({ copilotUserDBEnabled }),
setCopilotSampleDBEnabled: (copilotSampleDBEnabled: boolean) => set({ copilotSampleDBEnabled }),
openFeedbackModal: (generatedQuery: string, likeQuery: boolean, userPrompt: string) =>
set({ generatedQuery, likeQuery, userPrompt, showFeedbackModal: true }),
closeFeedbackModal: () => set({ showFeedbackModal: false }),
setHideFeedbackModalForLikedQueries: (hideFeedbackModalForLikedQueries: boolean) =>
set({ hideFeedbackModalForLikedQueries }),
refreshCorrelationId: () => set({ correlationId: guid() }),
setUserPrompt: (userPrompt: string) => set({ userPrompt }),
setQuery: (query: string) => set({ query }),
setGeneratedQuery: (generatedQuery: string) => set({ generatedQuery }),
setSelectedQuery: (selectedQuery: string) => set({ selectedQuery }),
setIsGeneratingQuery: (isGeneratingQuery: boolean) => set({ isGeneratingQuery }),
setIsGeneratingExplanation: (isGeneratingExplanation: boolean) => set({ isGeneratingExplanation }),
setIsExecuting: (isExecuting: boolean) => set({ isExecuting }),
setLikeQuery: (likeQuery: boolean) => set({ likeQuery }),
setDislikeQuery: (dislikeQuery: boolean | undefined) => set({ dislikeQuery }),
setShowCallout: (showCallout: boolean) => set({ showCallout }),
setShowSamplePrompts: (showSamplePrompts: boolean) => set({ showSamplePrompts }),
setQueryIterator: (queryIterator: MinimalQueryIterator | undefined) => set({ queryIterator }),
setQueryResults: (queryResults: QueryResults | undefined) => set({ queryResults }),
setErrors: (errors: QueryError[]) => set({ errors }),
setIsSamplePromptsOpen: (isSamplePromptsOpen: boolean) => set({ isSamplePromptsOpen }),
setShowDeletePopup: (showDeletePopup: boolean) => set({ showDeletePopup }),
setShowFeedbackBar: (showFeedbackBar: boolean) => set({ showFeedbackBar }),
setshowCopyPopup: (showCopyPopup: boolean) => set({ showCopyPopup }),
setShowErrorMessageBar: (showErrorMessageBar: boolean) => set({ showErrorMessageBar }),
setShowInvalidQueryMessageBar: (showInvalidQueryMessageBar: boolean) => set({ showInvalidQueryMessageBar }),
setGeneratedQueryComments: (generatedQueryComments: string) => set({ generatedQueryComments }),
setWasCopilotUsed: (wasCopilotUsed: boolean) => set({ wasCopilotUsed }),
setShowWelcomeSidebar: (showWelcomeSidebar: boolean) => set({ showWelcomeSidebar }),
setShowCopilotSidebar: (showCopilotSidebar: boolean) => set({ showCopilotSidebar }),
setChatMessages: (chatMessages: CopilotMessage[]) => set({ chatMessages }),
setShouldIncludeInMessages: (shouldIncludeInMessages: boolean) => set({ shouldIncludeInMessages }),
setShowExplanationBubble: (showExplanationBubble: boolean) => set({ showExplanationBubble }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
set({ notebookServerInfo }),
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
setIsAllocatingContainer: (isAllocatingContainer: boolean) => set({ isAllocatingContainer }),
setSchemaAllocationInfo: (schemaAllocationInfo: CopilotSchemaAllocationInfo) => set({ schemaAllocationInfo }),
setCopilotEnabledforExecution: (copilotEnabledforExecution: boolean) => set({ copilotEnabledforExecution }),
resetContainerConnection: (): void => {
useTabs.getState().closeAllNotebookTabs(true);
useQueryCopilot.getState().setNotebookServerInfo(undefined);
useQueryCopilot.getState().setIsAllocatingContainer(false);
useQueryCopilot.getState().setContainerStatus({
status: undefined,
durationLeftInMinutes: undefined,
phoenixServerInfo: undefined,
});
useQueryCopilot.getState().setSchemaAllocationInfo({
databaseId: undefined,
containerId: undefined,
});
},
resetQueryCopilotStates: () => {
set((state) => ({
...state,
generatedQuery: "",
likeQuery: false,
userPrompt: "",
showFeedbackModal: false,
hideFeedbackModalForLikedQueries: false,
correlationId: "",
query: "SELECT * FROM c",
selectedQuery: "",
isGeneratingQuery: false,
isGeneratingExplanation: false,
isExecuting: false,
dislikeQuery: undefined,
showCallout: false,
showSamplePrompts: false,
queryIterator: undefined,
queryResults: undefined,
errors: [],
isSamplePromptsOpen: false,
showDeletePopup: false,
showFeedbackBar: false,
showCopyPopup: false,
showErrorMessageBar: false,
showInvalidQueryMessageBar: false,
generatedQueryComments: "",
wasCopilotUsed: false,
showCopilotSidebar: false,
chatMessages: [],
shouldIncludeInMessages: true,
showExplanationBubble: false,
notebookServerInfo: {
notebookServerEndpoint: undefined,
authToken: undefined,
forwardingId: undefined,
},
containerStatus: {
status: undefined,
durationLeftInMinutes: undefined,
phoenixServerInfo: undefined,
},
schemaAllocationInfo: {
databaseId: undefined,
containerId: undefined,
},
isAllocatingContainer: false,
}));
},
})
)
);

View File

@@ -1,4 +1,4 @@
import create, { UseStore } from "zustand"; import { create } 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: UseStore<SidePanelState> = create((set) => ({ export const useSidePanel = create<SidePanelState>((set) => ({
isOpen: false, isOpen: false,
panelWidth: "440px", panelWidth: "440px",
openSidePanel: (headerText, panelContent, panelWidth = "440px") => openSidePanel: (headerText, panelContent, panelWidth = "440px") =>

View File

@@ -7,7 +7,8 @@ import {
OPEN_TABS_SUBCOMPONENT_NAME, OPEN_TABS_SUBCOMPONENT_NAME,
saveSubComponentState, saveSubComponentState,
} from "Shared/AppStatePersistenceUtility"; } from "Shared/AppStatePersistenceUtility";
import create, { UseStore } from "zustand"; import { create } 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";
@@ -51,194 +52,198 @@ export enum ReactTabKind {
QueryCopilot, QueryCopilot,
} }
export const useTabs: UseStore<TabsState> = create((set, get) => ({ export const useTabs = create<TabsState>()(
openedTabs: [] as TabsBase[], subscribeWithSelector(
openedReactTabs: [ReactTabKind.Home], (set, get) => ({
activeTab: undefined as TabsBase, openedTabs: [] as TabsBase[],
activeReactTab: ReactTabKind.Home, openedReactTabs: [ReactTabKind.Home],
queryCopilotTabInitialInput: "", activeTab: undefined as TabsBase,
isTabExecuting: false, activeReactTab: ReactTabKind.Home,
isQueryErrorThrown: false, queryCopilotTabInitialInput: "",
activateTab: (tab: TabsBase): void => { isTabExecuting: false,
if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) { isQueryErrorThrown: false,
set({ activeTab: tab, activeReactTab: undefined }); activateTab: (tab: TabsBase): void => {
tab.onActivate(); if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) {
} set({ activeTab: tab, activeReactTab: undefined });
}, tab.onActivate();
activateNewTab: (tab: TabsBase): void => {
set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab, activeReactTab: undefined }));
tab.triggerPersistState = get().persistTabsState;
tab.onActivate();
get().persistTabsState();
},
activateReactTab: (tabKind: ReactTabKind): void => {
// Clear the selected node when switching to a react tab.
useSelectedNode.getState().setSelectedNode(undefined);
set({ activeTab: undefined, activeReactTab: tabKind });
},
updateTab: (tab: TabsBase) => {
if (get().activeTab?.tabId === tab.tabId) {
set({ activeTab: tab });
}
set((state) => ({
openedTabs: state.openedTabs.map((openedTab) => {
if (openedTab.tabId === tab.tabId) {
return tab;
} }
return openedTab; },
}), activateNewTab: (tab: TabsBase): void => {
})); set((state) => ({ openedTabs: [...state.openedTabs, tab], activeTab: tab, activeReactTab: undefined }));
}, tab.triggerPersistState = get().persistTabsState;
getTabs: (tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean): TabsBase[] => tab.onActivate();
get().openedTabs.filter((tab) => tab.tabKind === tabKind && (!comparator || comparator(tab))), get().persistTabsState();
refreshActiveTab: (comparator: (tab: TabsBase) => boolean): void => { },
// ensures that the tab selects/highlights the right node based on resource tree expand/collapse state activateReactTab: (tabKind: ReactTabKind): void => {
const activeTab = get().activeTab; // Clear the selected node when switching to a react tab.
activeTab && comparator(activeTab) && activeTab.onActivate(); useSelectedNode.getState().setSelectedNode(undefined);
}, set({ activeTab: undefined, activeReactTab: tabKind });
closeTabsByComparator: (comparator: (tab: TabsBase) => boolean): void => },
get() updateTab: (tab: TabsBase) => {
.openedTabs.filter(comparator) if (get().activeTab?.tabId === tab.tabId) {
.forEach((tab) => tab.onCloseTabButtonClick()), set({ activeTab: tab });
closeTab: (tab: TabsBase): void => {
let tabIndex: number;
const { activeTab, openedTabs, openedReactTabs } = get();
const updatedTabs = openedTabs.filter((openedTab, index) => {
if (tab.tabId === openedTab.tabId) {
tabIndex = index;
return false;
}
return true;
});
if (updatedTabs.length === 0 && !isFabricMirrored()) {
set({ activeTab: undefined, activeReactTab: undefined });
}
if (tab.tabId === activeTab?.tabId && tabIndex !== -1) {
const tabToTheRight = updatedTabs[tabIndex];
const lastOpenTab = updatedTabs[updatedTabs.length - 1];
const newActiveTab = tabToTheRight ?? lastOpenTab;
set({ activeTab: newActiveTab });
if (newActiveTab) {
newActiveTab.onActivate();
}
}
set({ openedTabs: updatedTabs });
if (updatedTabs.length === 0 && openedReactTabs.length > 0) {
set({ activeTab: undefined, activeReactTab: openedReactTabs[openedReactTabs.length - 1] });
}
get().persistTabsState();
},
closeAllNotebookTabs: (hardClose): void => {
const isNotebook = (tabKind: CollectionTabKind): boolean => {
if (
tabKind === CollectionTabKind.Notebook ||
tabKind === CollectionTabKind.NotebookV2 ||
tabKind === CollectionTabKind.SchemaAnalyzer ||
tabKind === CollectionTabKind.Terminal
) {
return true;
}
return false;
};
const tabList = get().openedTabs;
if (tabList && tabList.length > 0) {
tabList.forEach((tab: NotebookTabV2) => {
const tabKind: CollectionTabKind = tab.tabKind;
if (tabKind && isNotebook(tabKind)) {
tab.onCloseTabButtonClick(hardClose);
} }
});
if (get().openedTabs.length === 0 && !isFabricMirrored()) { set((state) => ({
set({ activeTab: undefined, activeReactTab: undefined }); openedTabs: state.openedTabs.map((openedTab) => {
} if (openedTab.tabId === tab.tabId) {
} return tab;
}, }
openAndActivateReactTab: (tabKind: ReactTabKind) => { return openedTab;
if (get().openedReactTabs.indexOf(tabKind) === -1) { }),
set((state) => ({ }));
openedReactTabs: [...state.openedReactTabs, tabKind], },
})); getTabs: (tabKind: ViewModels.CollectionTabKind, comparator?: (tab: TabsBase) => boolean): TabsBase[] =>
} get().openedTabs.filter((tab) => tab.tabKind === tabKind && (!comparator || comparator(tab))),
refreshActiveTab: (comparator: (tab: TabsBase) => boolean): void => {
// ensures that the tab selects/highlights the right node based on resource tree expand/collapse state
const activeTab = get().activeTab;
activeTab && comparator(activeTab) && activeTab.onActivate();
},
closeTabsByComparator: (comparator: (tab: TabsBase) => boolean): void =>
get()
.openedTabs.filter(comparator)
.forEach((tab) => tab.onCloseTabButtonClick()),
closeTab: (tab: TabsBase): void => {
let tabIndex: number;
const { activeTab, openedTabs, openedReactTabs } = get();
const updatedTabs = openedTabs.filter((openedTab, index) => {
if (tab.tabId === openedTab.tabId) {
tabIndex = index;
return false;
}
return true;
});
if (updatedTabs.length === 0 && !isFabricMirrored()) {
set({ activeTab: undefined, activeReactTab: undefined });
}
set({ activeTab: undefined, activeReactTab: tabKind }); if (tab.tabId === activeTab?.tabId && tabIndex !== -1) {
}, const tabToTheRight = updatedTabs[tabIndex];
closeReactTab: (tabKind: ReactTabKind) => { const lastOpenTab = updatedTabs[updatedTabs.length - 1];
const { activeReactTab, openedTabs, openedReactTabs } = get(); const newActiveTab = tabToTheRight ?? lastOpenTab;
const updatedOpenedReactTabs = openedReactTabs.filter((tab: ReactTabKind) => tabKind !== tab); set({ activeTab: newActiveTab });
if (activeReactTab === tabKind) { if (newActiveTab) {
openedTabs?.length > 0 newActiveTab.onActivate();
? set({ activeTab: openedTabs[0], activeReactTab: undefined }) }
: set({ activeTab: undefined, activeReactTab: updatedOpenedReactTabs[0] }); }
}
set({ openedReactTabs: updatedOpenedReactTabs }); set({ openedTabs: updatedTabs });
},
setQueryCopilotTabInitialInput: (input: string) => set({ queryCopilotTabInitialInput: input }),
setIsTabExecuting: (state: boolean) => {
set({ isTabExecuting: state });
},
setIsQueryErrorThrown: (state: boolean) => {
set({ isQueryErrorThrown: state });
},
getCurrentTabIndex: () => {
const state = get();
if (state.activeReactTab !== undefined) {
return state.openedReactTabs.indexOf(state.activeReactTab);
} else if (state.activeTab !== undefined) {
const nonReactTabIndex = state.openedTabs.indexOf(state.activeTab);
if (nonReactTabIndex !== -1) {
return state.openedReactTabs.length + nonReactTabIndex;
}
}
return -1; if (updatedTabs.length === 0 && openedReactTabs.length > 0) {
}, set({ activeTab: undefined, activeReactTab: openedReactTabs[openedReactTabs.length - 1] });
selectTabByIndex: (index: number) => { }
const state = get();
const totalTabCount = state.openedReactTabs.length + state.openedTabs.length;
const clampedIndex = clamp(index, totalTabCount - 1, 0);
if (clampedIndex < state.openedReactTabs.length) { get().persistTabsState();
set({ activeTab: undefined, activeReactTab: state.openedReactTabs[clampedIndex] }); },
} else { closeAllNotebookTabs: (hardClose): void => {
set({ activeTab: state.openedTabs[clampedIndex - state.openedReactTabs.length], activeReactTab: undefined }); const isNotebook = (tabKind: CollectionTabKind): boolean => {
} if (
}, tabKind === CollectionTabKind.Notebook ||
selectLeftTab: () => { tabKind === CollectionTabKind.NotebookV2 ||
const state = get(); tabKind === CollectionTabKind.SchemaAnalyzer ||
state.selectTabByIndex(state.getCurrentTabIndex() - 1); tabKind === CollectionTabKind.Terminal
}, ) {
selectRightTab: () => { return true;
const state = get(); }
state.selectTabByIndex(state.getCurrentTabIndex() + 1); return false;
}, };
closeActiveTab: () => {
const state = get();
if (state.activeReactTab !== undefined) {
state.closeReactTab(state.activeReactTab);
} else if (state.activeTab !== undefined) {
state.closeTab(state.activeTab);
}
},
closeAllTabs: () => {
set({ openedTabs: [], openedReactTabs: [], activeTab: undefined, activeReactTab: undefined });
},
persistTabsState: () => {
const state = get();
const openTabsStates = state.openedTabs.map((tab) => tab.getPersistedState());
saveSubComponentState<OpenTab[]>( const tabList = get().openedTabs;
AppStateComponentNames.DataExplorerAction, if (tabList && tabList.length > 0) {
OPEN_TABS_SUBCOMPONENT_NAME, tabList.forEach((tab: NotebookTabV2) => {
undefined, const tabKind: CollectionTabKind = tab.tabKind;
openTabsStates, if (tabKind && isNotebook(tabKind)) {
); tab.onCloseTabButtonClick(hardClose);
}, }
})); });
if (get().openedTabs.length === 0 && !isFabricMirrored()) {
set({ activeTab: undefined, activeReactTab: undefined });
}
}
},
openAndActivateReactTab: (tabKind: ReactTabKind) => {
if (get().openedReactTabs.indexOf(tabKind) === -1) {
set((state) => ({
openedReactTabs: [...state.openedReactTabs, tabKind],
}));
}
set({ activeTab: undefined, activeReactTab: tabKind });
},
closeReactTab: (tabKind: ReactTabKind) => {
const { activeReactTab, openedTabs, openedReactTabs } = get();
const updatedOpenedReactTabs = openedReactTabs.filter((tab: ReactTabKind) => tabKind !== tab);
if (activeReactTab === tabKind) {
openedTabs?.length > 0
? set({ activeTab: openedTabs[0], activeReactTab: undefined })
: set({ activeTab: undefined, activeReactTab: updatedOpenedReactTabs[0] });
}
set({ openedReactTabs: updatedOpenedReactTabs });
},
setQueryCopilotTabInitialInput: (input: string) => set({ queryCopilotTabInitialInput: input }),
setIsTabExecuting: (state: boolean) => {
set({ isTabExecuting: state });
},
setIsQueryErrorThrown: (state: boolean) => {
set({ isQueryErrorThrown: state });
},
getCurrentTabIndex: () => {
const state = get();
if (state.activeReactTab !== undefined) {
return state.openedReactTabs.indexOf(state.activeReactTab);
} else if (state.activeTab !== undefined) {
const nonReactTabIndex = state.openedTabs.indexOf(state.activeTab);
if (nonReactTabIndex !== -1) {
return state.openedReactTabs.length + nonReactTabIndex;
}
}
return -1;
},
selectTabByIndex: (index: number) => {
const state = get();
const totalTabCount = state.openedReactTabs.length + state.openedTabs.length;
const clampedIndex = clamp(index, totalTabCount - 1, 0);
if (clampedIndex < state.openedReactTabs.length) {
set({ activeTab: undefined, activeReactTab: state.openedReactTabs[clampedIndex] });
} else {
set({ activeTab: state.openedTabs[clampedIndex - state.openedReactTabs.length], activeReactTab: undefined });
}
},
selectLeftTab: () => {
const state = get();
state.selectTabByIndex(state.getCurrentTabIndex() - 1);
},
selectRightTab: () => {
const state = get();
state.selectTabByIndex(state.getCurrentTabIndex() + 1);
},
closeActiveTab: () => {
const state = get();
if (state.activeReactTab !== undefined) {
state.closeReactTab(state.activeReactTab);
} else if (state.activeTab !== undefined) {
state.closeTab(state.activeTab);
}
},
closeAllTabs: () => {
set({ openedTabs: [], openedReactTabs: [], activeTab: undefined, activeReactTab: undefined });
},
persistTabsState: () => {
const state = get();
const openTabsStates = state.openedTabs.map((tab) => tab.getPersistedState());
saveSubComponentState<OpenTab[]>(
AppStateComponentNames.DataExplorerAction,
OPEN_TABS_SUBCOMPONENT_NAME,
undefined,
openTabsStates,
);
},
})
)
);

View File

@@ -1,5 +1,5 @@
import { Collection } from "Contracts/ViewModels"; import { Collection } from "Contracts/ViewModels";
import create, { UseStore } from "zustand"; import { create } 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: UseStore<TeachingBubbleState> = create((set) => ({ export const useTeachingBubble = create<TeachingBubbleState>((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, { UseStore } from "zustand"; import { create } 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: UseStore<TerminalState> = create((set, get) => ({ export const useTerminal = create<TerminalState>((set, get) => ({
terminalWindow: undefined, terminalWindow: undefined,
setTerminal: (terminalWindow: Window) => { setTerminal: (terminalWindow: Window) => {
set({ terminalWindow }); set({ terminalWindow });