Compare commits

..

10 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
BChoudhury-ms
012d043c78 Fix CloudShell terminal hanging for Mongo and Cassandra shells due to missing updateTerminalData method (#2199) 2025-08-13 13:02:27 -07:00
Mike Krüger
3afd74a957 Fix faifax default cloud shell region. (#2201) 2025-08-13 11:25:18 -07:00
82 changed files with 16805 additions and 62803 deletions

View File

@@ -143,6 +143,4 @@ src/Explorer/Tree/ResourceTreeAdapter.tsx
__mocks__/monaco-editor.ts
src/Explorer/Tree/ResourceTree.tsx
src/Utils/EndpointUtils.ts
src/Utils/PriorityBasedExecutionUtils.ts
utils/local-proxy/**
src/Utils/PriorityBasedExecutionUtils.ts

23
.vscode/settings.json vendored
View File

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

View File

@@ -1,3 +0,0 @@
{
"EMULATOR_ENDPOINT": "http://localhost:8081"
}

38023
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

36613
preview/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -138,6 +138,14 @@ export enum MongoBackendEndpointType {
remote,
}
export class AadScopeEndpoints {
public static readonly Development: string = "https://cosmos.azure.com";
public static readonly MPAC: string = "https://cosmos.azure.com";
public static readonly Prod: string = "https://cosmos.azure.com";
public static readonly Fairfax: string = "https://cosmos.azure.us";
public static readonly Mooncake: string = "https://cosmos.azure.cn";
}
export class PortalBackendEndpoints {
public static readonly Development: string = "https://localhost:7235";
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 apiType: string = "x-ms-cosmos-apitype";
public static authorization: string = "authorization";
public static entraIdToken: string = "x-ms-entraid-token";
public static collectionIndexTransformationProgress: string =
"x-ms-documentdb-collection-index-transformation-progress";
public static continuation: string = "x-ms-continuation";
@@ -765,3 +774,10 @@ export const ShortenedQueryCopilotSampleContainerSchema = {
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);
});
});
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";
export function normalizeArmEndpoint(uri: string): string {
@@ -27,3 +28,17 @@ export const getEnvironment = (): Environment => {
return environmentMap[configContext.PORTAL_BACKEND_ENDPOINT];
};
export const getEnvironmentScopeEndpoint = (): string => {
const environment = getEnvironment();
const endpoint = AadScopeEndpoints[environment];
if (!endpoint) {
throw new Error("Cannot determine AAD scope endpoint");
}
const hrefEndpoint = new URL(endpoint).href.replace(/\/+$/, "/.default");
Logger.logInfo(
`Using AAD scope endpoint: ${hrefEndpoint}, Environment: ${environment}`,
"EnvironmentUtility/getEnvironmentScopeEndpoint",
);
return hrefEndpoint;
};

View File

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

View File

@@ -19,7 +19,6 @@ export enum Platform {
Hosted = "Hosted",
Emulator = "Emulator",
Fabric = "Fabric",
VNextEmulator = "VNextEmulator",
}
export interface ConfigContext {
@@ -226,7 +225,6 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
case Platform.Fabric:
case Platform.Hosted:
case Platform.Emulator:
case Platform.VNextEmulator:
updateConfigContext({ platform });
}
}

View File

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

View File

@@ -15,7 +15,6 @@ import { useDatabases } from "Explorer/useDatabases";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isRunningOnPublicCloud } from "Utils/CloudUtils";
import { isFeatureSupported, PlatformFeature } from "Utils/PlatformFeatureUtils";
import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg";
import SaveIcon from "../../../../images/save-cosmos.svg";
@@ -61,15 +60,15 @@ import {
AddMongoIndexProps,
ChangeFeedPolicyState,
GeospatialConfigType,
MongoIndexTypes,
SettingsV2TabTypes,
TtlType,
getMongoNotification,
getTabTitle,
hasDatabaseSharedThroughput,
isDirty,
MongoIndexTypes,
parseConflictResolutionMode,
parseConflictResolutionProcedure,
SettingsV2TabTypes,
TtlType,
} from "./SettingsUtils";
interface SettingsV2TabInfo {
@@ -277,14 +276,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.saveSettingsButton = {
isEnabled: this.isSaveSettingsButtonEnabled,
isVisible: () => {
return isFeatureSupported(PlatformFeature.UpdateCollection);
return true;
},
};
this.discardSettingsChangesButton = {
isEnabled: this.isDiscardSettingsButtonEnabled,
isVisible: () => {
return isFeatureSupported(PlatformFeature.UpdateCollection);
return true;
},
};

View File

@@ -3,7 +3,7 @@ import { Link } from "@fluentui/react/lib/Link";
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { Environment, getEnvironment } from "Common/EnvironmentUtility";
import { sendMessage } from "Common/MessageHandler";
import { configContext, Platform } from "ConfigContext";
import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { getCopilotEnabled, isCopilotFeatureRegistered } from "Explorer/QueryCopilot/Shared/QueryCopilotClient";
@@ -18,13 +18,12 @@ import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
import { featureRegistered } from "Utils/FeatureRegistrationUtils";
import { isFeatureSupported, PlatformFeature } from "Utils/PlatformFeatureUtils";
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as ko from "knockout";
import React from "react";
import _ from "underscore";
import shallow from "zustand/shallow";
import { shallow } from "zustand/shallow";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants";
@@ -113,8 +112,8 @@ export default class Explorer {
this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
useNotebook.subscribe(
() => this.refreshCommandBarButtons(),
(state) => state.isNotebooksEnabledForAccount,
() => this.refreshCommandBarButtons(),
);
this.queriesClient = new QueriesClient(this);
@@ -137,13 +136,13 @@ export default class Explorer {
});
useTabs.subscribe(
(state) => state.openedTabs,
(openedTabs: TabsBase[]) => {
if (openedTabs.length === 0) {
useSelectedNode.getState().setSelectedNode(undefined);
useCommandBar.getState().setContextButtons([]);
}
},
(state) => state.openedTabs,
}
);
this.isTabsContentExpanded = ko.observable(false);
@@ -171,9 +170,9 @@ export default class Explorer {
);
useNotebook.subscribe(
async () => this.initiateAndRefreshNotebookList(),
(state) => [state.isNotebookEnabled, state.isRefreshed],
shallow,
async () => this.initiateAndRefreshNotebookList(),
{ equalityFn: shallow },
);
this.resourceTree = new ResourceTreeAdapter(this);
@@ -1188,7 +1187,6 @@ export default class Explorer {
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
const isNotebookEnabled =
isFeatureSupported(PlatformFeature.Notebooks) &&
configContext.platform !== Platform.Fabric &&
(userContext.features.notebooksDownBanner ||
useNotebook.getState().isPhoenixNotebooks ||
@@ -1196,11 +1194,7 @@ export default class Explorer {
useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
useNotebook
.getState()
.setIsShellEnabled(
isFeatureSupported(PlatformFeature.CloudShell) &&
useNotebook.getState().isPhoenixFeatures &&
isPublicInternetAccessAllowed(),
);
.setIsShellEnabled(useNotebook.getState().isPhoenixFeatures && isPublicInternetAccessAllowed());
TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
isNotebookEnabled,
@@ -1221,7 +1215,6 @@ export default class Explorer {
public async configureCopilot(): Promise<void> {
if (
!isFeatureSupported(PlatformFeature.Copilot) ||
userContext.apiType !== "SQL" ||
!userContext.subscriptionId ||
![Environment.Development, Environment.Mpac, Environment.Prod].includes(getEnvironment())

View File

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

View File

@@ -1,8 +1,6 @@
import { KeyboardAction } from "KeyboardShortcuts";
import { isDataplaneRbacSupported } from "Utils/APITypeUtils";
import { areAdvancedScriptsSupported, isFeatureSupported, PlatformFeature } from "Utils/PlatformFeatureUtils";
import * as React from "react";
import { useEffect, useState } from "react";
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
import AddStoredProcedureIcon from "../../../../images/AddStoredProcedure.svg";
import AddTriggerIcon from "../../../../images/AddTrigger.svg";
@@ -18,7 +16,7 @@ import SynapseIcon from "../../../../images/synapse-link.svg";
import VSCodeIcon from "../../../../images/vscode.svg";
import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants";
import { configContext, Platform } from "../../../ConfigContext";
import { Platform, configContext } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels";
import { userContext } from "../../../UserContext";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
@@ -64,22 +62,12 @@ export function createStaticCommandBarButtons(
}
if (userContext.apiType !== "Gremlin") {
const addVsCode = createOpenVsCodeDialogButton(container);
if (addVsCode) {
buttons.push(addVsCode);
}
buttons.push(addVsCode);
}
}
if (isDataplaneRbacSupported(userContext.apiType)) {
const [loginButtonProps, setLoginButtonProps] = useState<CommandButtonComponentProps | undefined>(undefined);
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
useEffect(() => {
const buttonProps = createLoginForEntraIDButton(container);
setLoginButtonProps(buttonProps);
}, [dataPlaneRbacEnabled, aadTokenUpdated, container]);
const loginButtonProps = createLoginForEntraIDButton(container);
if (loginButtonProps) {
addDivider();
buttons.push(loginButtonProps);
@@ -245,17 +233,11 @@ export function createDivider(): CommandButtonComponentProps {
function areScriptsSupported(): boolean {
return (
areAdvancedScriptsSupported() &&
configContext.platform !== Platform.Fabric &&
(userContext.apiType === "SQL" || userContext.apiType === "Gremlin")
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin")
);
}
function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
if (!isFeatureSupported(PlatformFeature.SynapseLink)) {
return undefined;
}
if (configContext.platform === Platform.Emulator) {
return undefined;
}
@@ -283,10 +265,6 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
}
function createOpenVsCodeDialogButton(container: Explorer): CommandButtonComponentProps {
if (!isFeatureSupported(PlatformFeature.VSCodeIntegration)) {
return undefined;
}
const label = "Visual Studio Code";
return {
iconSrc: VSCodeIcon,

View File

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

View File

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

View File

@@ -50,7 +50,6 @@ import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils";
import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isFeatureSupported, PlatformFeature } from "Utils/PlatformFeatureUtils";
import { getUpsellMessage } from "Utils/PricingUtils";
import { ValidCosmosDbIdDescription, ValidCosmosDbIdInputPattern } from "Utils/ValidationUtils";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
@@ -1161,15 +1160,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
private shouldShowVectorSearchParameters() {
return (
isFeatureSupported(PlatformFeature.VectorSearch) &&
isVectorSearchEnabled() &&
(isServerlessAccount() || this.shouldShowCollectionThroughputInput())
);
return isVectorSearchEnabled() && (isServerlessAccount() || this.shouldShowCollectionThroughputInput());
}
private shouldShowFullTextSearchParameters() {
return isFeatureSupported(PlatformFeature.FullTextSearch) && !isFabricNative() && this.showFullTextSearch;
return !isFabricNative() && this.showFullTextSearch;
}
private parseUniqueKeys(): DataModels.UniqueKeyPolicy {

View File

@@ -6,7 +6,6 @@ import { getFullTextLanguageOptions } from "Explorer/Controls/FullTextSeach/Full
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import React from "react";
import { userContext } from "UserContext";
import { isFeatureSupported, PlatformFeature } from "Utils/PlatformFeatureUtils";
export function getPartitionKeyTooltipText(): string {
if (userContext.apiType === "Mongo") {
@@ -86,11 +85,7 @@ export function UniqueKeysHeader(): JSX.Element {
}
export function shouldShowAnalyticalStoreOptions(): boolean {
if (
!isFeatureSupported(PlatformFeature.AnalyticalStore) ||
isFabricNative() ||
configContext.platform === Platform.Emulator
) {
if (isFabricNative() || configContext.platform === Platform.Emulator) {
return false;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,6 +22,7 @@ import { formatErrorMessage, formatInfoMessage, formatWarningMessage } from "./U
// Constants
const DEFAULT_CLOUDSHELL_REGION = "westus";
const DEFAULT_FAIRFAX_CLOUDSHELL_REGION = "usgovvirginia";
const POLLING_INTERVAL_MS = 2000;
const MAX_RETRY_COUNT = 10;
const MAX_PING_COUNT = 120 * 60; // 120 minutes (60 seconds/minute)
@@ -153,7 +154,9 @@ export const ensureCloudShellProviderRegistered = async (): Promise<void> => {
* Determines the appropriate CloudShell region
*/
export const determineCloudShellRegion = (): string => {
return getNormalizedRegion(userContext.databaseAccount?.location, DEFAULT_CLOUDSHELL_REGION);
const defaultRegion =
userContext.portalEnv === "fairfax" ? DEFAULT_FAIRFAX_CLOUDSHELL_REGION : DEFAULT_CLOUDSHELL_REGION;
return getNormalizedRegion(userContext.databaseAccount?.location, defaultRegion);
};
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,10 @@
import { userContext } from "../../../../UserContext";
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND } from "./AbstractShellHandler";
import { filterAndCleanTerminalOutput, getMongoShellRemoveInfoText } from "../Utils/CommonUtils";
import { AbstractShellHandler, DISABLE_TELEMETRY_COMMAND, EXIT_COMMAND_MONGO } from "./AbstractShellHandler";
export class VCoreMongoShellHandler extends AbstractShellHandler {
private _endpoint: string | undefined;
private _textFilterRules: string[] = [
"For mongosh info see: https://www.mongodb.com/docs/mongodb-shell/",
"disableTelemetry() command",
"https://www.mongodb.com/legal/privacy-policy",
];
private _removeInfoText: string[] = getMongoShellRemoveInfoText();
constructor() {
super();
@@ -38,12 +35,14 @@ export class VCoreMongoShellHandler extends AbstractShellHandler {
return ["Warning: Non-Genuine MongoDB Detected", "Telemetry is now disabled."];
}
updateTerminalData(content: string): string {
const updatedContent = content
.split("\n")
.filter((line) => !this._textFilterRules.some((part) => line.includes(part)))
.filter((line, idx, arr) => (arr.length > 3 && idx <= arr.length - 3 ? !["", "\r"].includes(line) : true)) // Filter out empty lines and carriage returns, but keep the last 3 lines if they exist
.join("\n");
return updatedContent;
/**
* Override getExitCommand to include MongoDB networking guidance
*/
protected getExitCommand(): string {
return EXIT_COMMAND_MONGO;
}
updateTerminalData(data: string): string {
return filterAndCleanTerminalOutput(data, this._removeInfoText);
}
}

View File

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

View File

@@ -50,3 +50,34 @@ export const getShellNameForDisplay = (terminalKind: TerminalKind): string => {
return "";
}
};
/**
* Get MongoDB shell information text that should be removed from terminal output
*/
export const getMongoShellRemoveInfoText = (): string[] => {
return [
"For mongosh info see: https://www.mongodb.com/docs/mongodb-shell/",
"disableTelemetry() command",
"https://www.mongodb.com/legal/privacy-policy",
];
};
export const filterAndCleanTerminalOutput = (data: string, removeInfoText: string[]): string => {
if (!data || removeInfoText.length === 0) {
return data;
}
const lines = data.split("\n");
const filteredLines: string[] = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const shouldRemove = removeInfoText.some((text) => line.includes(text));
if (!shouldRemove) {
filteredLines.push(line);
}
}
return filteredLines.join("\n").replace(/((\r\n)|\n|\r){2,}/g, "\r\n");
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { useSelectedNode } from "../useSelectedNode";
export const shouldShowScriptNodes = (): boolean => {
return !isFabric() && configContext.platform !== Platform.Emulator && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
return !isFabric() && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
};
const TreeDatabaseIcon = <DatabaseRegular fontSize={16} />;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -79,22 +79,7 @@ export const defaultAllowedCassandraProxyEndpoints: ReadonlyArray<string> = [
CassandraProxyEndpoints.Mooncake,
];
export const allowedCassandraProxyEndpoints_ToBeDeprecated: ReadonlyArray<string> = [
"https://main.documentdb.ext.azure.com",
"https://main.documentdb.ext.azure.cn",
"https://main.documentdb.ext.azure.us",
"https://main.cosmos.ext.azure",
"https://localhost:12901",
];
export const CassandraProxyOutboundIPs: { [key: string]: string[] } = {
[CassandraProxyEndpoints.Mpac]: ["40.113.96.14", "104.42.11.145"],
[CassandraProxyEndpoints.Prod]: ["137.117.230.240", "168.61.72.237"],
[CassandraProxyEndpoints.Fairfax]: ["52.244.50.101", "52.227.165.24"],
[CassandraProxyEndpoints.Mooncake]: ["40.73.99.146", "143.64.62.47"],
};
export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localhost:8081", "http://localhost:8081"];
export const allowedEmulatorEndpoints: ReadonlyArray<string> = ["https://localhost:8081"];
export const allowedArcadiaEndpoints: ReadonlyArray<string> = ["https://workspaceartifacts.projectarcadia.net"];

View File

@@ -1,112 +0,0 @@
import { Platform, configContext } from "../ConfigContext";
/**
* Feature flags enumeration - centralized feature definitions
*/
export enum PlatformFeature {
// UI/Core Features
Queries = "Queries",
Notebooks = "Notebooks",
SynapseLink = "SynapseLink",
VSCodeIntegration = "VSCodeIntegration",
GlobalSecondaryIndex = "GlobalSecondaryIndex",
DataPlaneRbac = "DataPlaneRbac",
EntraIDLogin = "EntraIDLogin",
EntreIDRbac = "EntreIDRbac",
RetrySettings = "RetrySettings",
GraphAutoVizOption = "GraphAutoVizOption",
CrossPartitionOption = "CrossPartitionOption",
EnhancedQueryControl = "EnhancedQueryControl",
ParallelismOption = "ParallelismOption",
EnableEntraIdRbac = "EnableEntraIdRbac",
PriorityBasedExecution = "PriorityBasedExecution",
RegionSelection = "RegionSelection",
Copilot = "Copilot",
CloudShell = "CloudShell",
ContainerPagination = "ContainerPagination",
FullTextSearch = "FullTextSearch",
VectorSearch = "VectorSearch",
ThroughputBucketing = "ThroughputBucketing",
ComputedProperties = "ComputedProperties",
AnalyticalStore = "AnalyticalStore",
// CRUD Operations - Database
CreateDatabase = "CreateDatabase",
ReadDatabase = "ReadDatabase",
DeleteDatabase = "DeleteDatabase",
// CRUD Operations - Collection
CreateCollection = "CreateCollection",
ReadCollection = "ReadCollection",
UpdateCollection = "UpdateCollection",
DeleteCollection = "DeleteCollection",
// CRUD Operations - Document
CreateDocument = "CreateDocument",
ReadDocument = "ReadDocument",
UpdateDocument = "UpdateDocument",
DeleteDocument = "DeleteDocument",
// Advanced Database Features
StoredProcedures = "StoredProcedures",
UDF = "UDF",
Trigger = "Trigger",
}
/**
* Feature matrix per platform.
* - Only list platforms that have restrictions. If a platform is not present, all features are considered supported.
* - Start with VNextEmulator today; add more platforms/flags here later without touching calling code.
*/
const FEATURE_MATRIX: ReadonlyMap<Platform, ReadonlySet<PlatformFeature>> = new Map([
[
Platform.VNextEmulator,
new Set<PlatformFeature>([
PlatformFeature.Queries,
PlatformFeature.CreateDatabase,
PlatformFeature.ReadDatabase,
PlatformFeature.DeleteDatabase,
PlatformFeature.CreateCollection,
PlatformFeature.ReadCollection,
PlatformFeature.UpdateCollection,
PlatformFeature.DeleteCollection,
PlatformFeature.CreateDocument,
PlatformFeature.ReadDocument,
PlatformFeature.UpdateDocument,
PlatformFeature.DeleteDocument,
]),
],
]);
/**
* Central feature flag function - checks if a feature is enabled for current platform
* @param feature The feature to check
* @param platform Optional platform override, defaults to current platform
* @returns True if the feature is enabled for the platform, false otherwise
*/
export const isFeatureSupported = (feature: PlatformFeature, platform?: Platform): boolean => {
const currentPlatform = platform ?? configContext.platform;
if (currentPlatform !== Platform.VNextEmulator) {
return true;
}
// VNextEmulator: check from the feature matrix
const vnextFeatures = FEATURE_MATRIX.get(Platform.VNextEmulator);
return vnextFeatures?.has(feature) ?? false;
};
export const areAdvancedScriptsSupported = (platform?: Platform): boolean => {
const currentPlatform = platform ?? configContext.platform;
if (currentPlatform !== Platform.VNextEmulator) {
return true;
}
// Otherwise, require all script features to be enabled
return (
isFeatureSupported(PlatformFeature.StoredProcedures, currentPlatform) &&
isFeatureSupported(PlatformFeature.UDF, currentPlatform) &&
isFeatureSupported(PlatformFeature.Trigger, currentPlatform)
);
};

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import * as Constants from "Common/Constants";
import { getEnvironmentScopeEndpoint } from "Common/EnvironmentUtility";
import { createUri } from "Common/UrlUtility";
import { DATA_EXPLORER_RPC_VERSION } from "Contracts/DataExplorerMessagesContract";
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
@@ -62,6 +63,7 @@ import {
acquireTokenWithMsal,
getAuthorizationHeader,
getMsalInstance,
isDataplaneRbacEnabledForProxyApi,
} from "../Utils/AuthorizationUtils";
import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation";
import { get, getReadOnlyKeys, listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
@@ -86,7 +88,7 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
let explorer: Explorer;
if (platform === Platform.Hosted) {
explorer = await configureHosted();
} else if (platform === Platform.Emulator || platform === Platform.VNextEmulator) {
} else if (platform === Platform.Emulator) {
explorer = configureEmulator();
} else if (platform === Platform.Portal) {
explorer = await configurePortal();
@@ -331,7 +333,12 @@ async function configureHostedWithAAD(config: AAD): Promise<Explorer> {
const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0];
let aadToken;
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 cachedAccount = msalInstance.getAllAccounts()?.[0];
msalInstance.setActiveAccount(cachedAccount);

View File

@@ -1,4 +1,4 @@
import create, { UseStore } from "zustand";
import { create } from "zustand";
export interface NotebookSnapshotHooks {
snapshot?: string;
@@ -7,7 +7,7 @@ export interface NotebookSnapshotHooks {
setError: (error: string) => void;
}
export const useNotebookSnapshotStore: UseStore<NotebookSnapshotHooks> = create((set) => ({
export const useNotebookSnapshotStore = create<NotebookSnapshotHooks>((set) => ({
snapshot: undefined,
error: undefined,
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";
export interface NotificationConsoleState {
@@ -15,7 +15,7 @@ export interface NotificationConsoleState {
setConsoleAnimationFinished: (consoleAnimationFinished: boolean) => void;
}
export const useNotificationConsole: UseStore<NotificationConsoleState> = create((set) => ({
export const useNotificationConsole = create<NotificationConsoleState>((set) => ({
isExpanded: false,
consoleData: undefined,
inProgressConsoleDataIdToBeDeleted: "",

View File

@@ -1,4 +1,5 @@
import create, { UseStore } from "zustand";
import { create } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
interface TeachingBubbleState {
showPostgreTeachingBubble: boolean;
@@ -7,9 +8,13 @@ interface TeachingBubbleState {
setShowResetPasswordBubble: (showResetPasswordBubble: boolean) => void;
}
export const usePostgres: UseStore<TeachingBubbleState> = create((set) => ({
showPostgreTeachingBubble: false,
showResetPasswordBubble: false,
setShowPostgreTeachingBubble: (showPostgreTeachingBubble: boolean) => set({ showPostgreTeachingBubble }),
setShowResetPasswordBubble: (showResetPasswordBubble: boolean) => set({ showResetPasswordBubble }),
}));
export const usePostgres = create<TeachingBubbleState>()(
subscribeWithSelector(
(set) => ({
showPostgreTeachingBubble: false,
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 { guid } from "Explorer/Tables/Utilities";
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 { ContainerInfo } from "../Contracts/DataModels";
@@ -96,120 +97,12 @@ export interface QueryCopilotState {
resetQueryCopilotStates: () => void;
}
type QueryCopilotStore = UseStore<Partial<QueryCopilotState>>;
export const useQueryCopilot: QueryCopilotStore = create((set) => ({
copilotEnabled: false,
copilotUserDBEnabled: 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,
export const useQueryCopilot = create<Partial<QueryCopilotState>>()(
subscribeWithSelector(
(set) => ({
copilotEnabled: false,
copilotUserDBEnabled: false,
copilotSampleDBEnabled: false,
generatedQuery: "",
likeQuery: false,
userPrompt: "",
@@ -218,15 +111,15 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
correlationId: "",
query: "SELECT * FROM c",
selectedQuery: "",
isGeneratingQuery: false,
isGeneratingQuery: null as boolean,
isGeneratingExplanation: false,
isExecuting: false,
dislikeQuery: undefined,
dislikeQuery: undefined as (boolean | undefined),
showCallout: false,
showSamplePrompts: false,
queryIterator: undefined,
queryResults: undefined,
errors: [],
queryIterator: undefined as MinimalQueryIterator | undefined,
queryResults: undefined as QueryResults | undefined,
errors: [] as QueryError[],
isSamplePromptsOpen: false,
showDeletePopup: false,
showFeedbackBar: false,
@@ -235,25 +128,135 @@ export const useQueryCopilot: QueryCopilotStore = create((set) => ({
showInvalidQueryMessageBar: false,
generatedQueryComments: "",
wasCopilotUsed: false,
showWelcomeSidebar: true,
showCopilotSidebar: false,
chatMessages: [],
chatMessages: [] as CopilotMessage[],
shouldIncludeInMessages: true,
showExplanationBubble: false,
notebookServerInfo: {
notebookServerEndpoint: undefined,
authToken: undefined,
forwardingId: undefined,
},
} as DataModels.NotebookWorkspaceConnectionInfo,
containerStatus: {
status: undefined,
durationLeftInMinutes: undefined,
phoenixServerInfo: undefined,
},
} as ContainerInfo,
schemaAllocationInfo: {
databaseId: undefined,
containerId: undefined,
},
} as CopilotSchemaAllocationInfo,
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 {
isOpen: boolean;
@@ -9,7 +9,7 @@ export interface SidePanelState {
closeSidePanel: () => void;
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,
panelWidth: "440px",
openSidePanel: (headerText, panelContent, panelWidth = "440px") =>

View File

@@ -7,7 +7,8 @@ import {
OPEN_TABS_SUBCOMPONENT_NAME,
saveSubComponentState,
} from "Shared/AppStatePersistenceUtility";
import create, { UseStore } from "zustand";
import { create } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
import * as ViewModels from "../Contracts/ViewModels";
import { CollectionTabKind } from "../Contracts/ViewModels";
import NotebookTabV2 from "../Explorer/Tabs/NotebookV2Tab";
@@ -51,194 +52,198 @@ export enum ReactTabKind {
QueryCopilot,
}
export const useTabs: UseStore<TabsState> = create((set, get) => ({
openedTabs: [] as TabsBase[],
openedReactTabs: [ReactTabKind.Home],
activeTab: undefined as TabsBase,
activeReactTab: ReactTabKind.Home,
queryCopilotTabInitialInput: "",
isTabExecuting: false,
isQueryErrorThrown: false,
activateTab: (tab: TabsBase): void => {
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;
export const useTabs = create<TabsState>()(
subscribeWithSelector(
(set, get) => ({
openedTabs: [] as TabsBase[],
openedReactTabs: [ReactTabKind.Home],
activeTab: undefined as TabsBase,
activeReactTab: ReactTabKind.Home,
queryCopilotTabInitialInput: "",
isTabExecuting: false,
isQueryErrorThrown: false,
activateTab: (tab: TabsBase): void => {
if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) {
set({ activeTab: tab, activeReactTab: undefined });
tab.onActivate();
}
return openedTab;
}),
}));
},
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 });
}
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);
},
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 });
}
});
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((state) => ({
openedTabs: state.openedTabs.map((openedTab) => {
if (openedTab.tabId === tab.tabId) {
return tab;
}
return openedTab;
}),
}));
},
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 });
},
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] });
}
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({ 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;
}
}
set({ openedTabs: updatedTabs });
return -1;
},
selectTabByIndex: (index: number) => {
const state = get();
const totalTabCount = state.openedReactTabs.length + state.openedTabs.length;
const clampedIndex = clamp(index, totalTabCount - 1, 0);
if (updatedTabs.length === 0 && openedReactTabs.length > 0) {
set({ activeTab: undefined, activeReactTab: openedReactTabs[openedReactTabs.length - 1] });
}
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());
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;
};
saveSubComponentState<OpenTab[]>(
AppStateComponentNames.DataExplorerAction,
OPEN_TABS_SUBCOMPONENT_NAME,
undefined,
openTabsStates,
);
},
}));
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({ 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 create, { UseStore } from "zustand";
import { create } from "zustand";
interface TeachingBubbleState {
step: number;
@@ -12,7 +12,7 @@ interface TeachingBubbleState {
setSampleCollection: (sampleCollection: Collection) => void;
}
export const useTeachingBubble: UseStore<TeachingBubbleState> = create((set) => ({
export const useTeachingBubble = create<TeachingBubbleState>((set) => ({
step: 1,
isSampleDBExpanded: false,
isDocumentsTabOpened: false,

View File

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

View File

@@ -1,291 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0" />
<title>Azure Cosmos DB Emulator</title>
</head>
<body>
<div id="divQuickStart">
<div class="Introlines">
<p class="Introline1">Congratulations! Your Azure Cosmos DB emulator is running.</p>
<p class="Introline2">Now, let's connect a sample app to it.</p>
<div id="divQuickStartConnections">
<p class="Introline2">URI</p>
<input type="text" class="codeblock" readonly="readonly" value="http://localhost:8081" />
<p class="Introline2">Primary Key</p>
<input
type="text"
class="codeblock"
readonly="readonly"
value="C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
/>
<p class="Introline2">Primary Connection String</p>
<input
type="text"
class="codeblock"
readonly="readonly"
value="AccountEndpoint=http://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
/>
</div>
<p class="Introline3"><b>Choose a platform</b></p>
</div>
<div class="container-fluid">
<ul class="nav nav-tabs qslevel">
<li class="active">
<a data-toggle="tab" href="#net"
><img class="qsmenuicons" src="../images/dotnet.png" alt=".NET platform" />.NET</a
>
</li>
<li>
<a data-toggle="tab" href="#corenet"
><img class="qsmenuicons" src="../images/dotnet.png" alt=".NET Core platform" />.NET Core</a
>
</li>
<li>
<a data-toggle="tab" href="#Java"
><img class="qsmenuicons" src="../images/java.png" alt="Java platform" />Java</a
>
</li>
<li>
<a data-toggle="tab" href="#NodeJs"
><img class="qsmenuicons" src="../images/nodejs.png" alt="Node.js platform" />Node.js</a
>
</li>
<li>
<a data-toggle="tab" href="#Python"
><img class="qsmenuicons" src="../images/python.png" alt="Python platform" />Python</a
>
</li>
</ul>
<div class="tab-content tab-content-override">
<div id="net" class="tab-pane fade in active">
<div class="netApp">
<div class="numbersize numbersizePadding">1</div>
<div class="numberheading">
Open and run a sample .NET app
<p>
We created a sample .NET app connected to your Azure Cosmos DB Emulator instance. Download, extract,
build and run the app.
</p>
<a href="quickstart/DocumentDB-Quickstart-DotNet.zip"
><button class="btncreatecoll">Download</button></a
>
</div>
</div>
<div class="netApp">
<div class="numbersize">2</div>
<div class="numberheading">
Learn more about Azure Cosmos DB
<ul>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/samples/dotnet"
>Code Samples</a
>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/docs">Documentation</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/pricing">Pricing</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/capacity-planner"
>Capacity Planner</a
>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/stackoverflow">Forum</a>
</li>
</ul>
</div>
</div>
</div>
<div id="corenet" class="tab-pane fade">
<div class="netApp">
<div class="numbersize numbersizePadding">1</div>
<div class="numberheading">
Open and run a sample .NET Core app
<p>
We created a sample .NET Core app connected to your Azure Cosmos DB Emulator instance. Download,
extract, build and run the app.
</p>
<a href="quickstart/DocumentDB-Quickstart-DotNetCore.zip"
><button class="btncreatecoll">Download</button></a
>
</div>
</div>
<div class="netApp">
<div class="numbersize">2</div>
<div class="numberheading">
Learn more about Azure Cosmos DB.
<ul>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/samples/dotnet"
>Code Samples</a
>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/docs">Documentation</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/pricing">Pricing</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/capacity-planner"
>Capacity Planner</a
>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/stackoverflow">Forum</a>
</li>
</ul>
</div>
</div>
</div>
<div id="Java" class="tab-pane fade">
<div class="step1">
<div class="numbersize numbersizePadding">1</div>
<div class="numberheading">
Open and run a sample Java app
<p>
We created a sample Java app connected to your Azure Cosmos DB Emulator instance. Download, extract,
build and run the app.
</p>
<a href="quickstart/DocumentDB-Quickstart-Java.zip"><button class="btncreatecoll">Download</button></a>
<p>
Follow instructions in the readme.md to setup prerequisites needed to run Java web apps, if you
havent already.
</p>
</div>
</div>
<div class="step1">
<div class="numbersize">2</div>
<div class="numberheading">
Learn more about Azure Cosmos DB.
<ul>
<!--<li><a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/samples/java">Code Samples</a></li>-->
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/docs">Documentation</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/pricing">Pricing</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/capacity-planner"
>Capacity Planner</a
>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/stackoverflow">Forum</a>
</li>
</ul>
</div>
</div>
</div>
<div id="NodeJs" class="tab-pane fade">
<div class="step1">
<div class="numbersize numbersizePadding">1</div>
<div class="numberheading">
Open and run a sample Node.js app
<p>
We created a sample Node.js app connected to your Azure Cosmos DB Emulator instance. Download,
extract, build and run the app.
</p>
<a href="quickstart/DocumentDB-Quickstart-NodeJs.zip"
><button class="btncreatecoll">Download</button></a
>
<p>
Run <strong>npm install</strong> and <strong>npm start</strong>, and navigate to
<a href="http://localhost:3000" _targe="blank">http://localhost:3000</a>.
</p>
</div>
</div>
<div class="step1">
<div class="numbersize">2</div>
<div class="numberheading">
Learn more about Azure Cosmos DB.
<ul>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/samples/nodejs"
>Code Samples</a
>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/docs">Documentation</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/pricing">Pricing</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/capacity-planner"
>Capacity Planner</a
>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/stackoverflow">Forum</a>
</li>
</ul>
</div>
</div>
</div>
<div id="Python" class="tab-pane fade">
<div class="pythonApp">
<div class="numbersize numbersizePadding">1</div>
<div class="numberheading">
Create a new Python app.
<p>
Follow this
<a href="https://aka.ms/cosmos-db-emulator/tutorial/python" target="_blank">tutorial</a>
to create a new Python app connected to Azure Cosmos DB.
</p>
</div>
</div>
<div class="pythonApp">
<div class="numbersize">2</div>
<div class="numberheading">
Learn more about Azure Cosmos DB.
<ul>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/samples/python"
>Code Samples</a
>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/docs">Documentation</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/pricing">Pricing</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/capacity-planner"
>Capacity planner</a
>
</li>
<li>
<a
target="_blank"
class="atags"
href="https://social.msdn.microsoft.com/forums/azure/home?forum=AzureDocumentDB"
>Forum</a
>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,291 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0" />
<title>Azure Cosmos DB Emulator</title>
</head>
<body>
<div id="divQuickStart">
<div class="Introlines">
<p class="Introline1">Congratulations! Your Azure Cosmos DB emulator is running.</p>
<p class="Introline2">Now, let's connect a sample app to it.</p>
<div id="divQuickStartConnections">
<p class="Introline2">URI</p>
<input type="text" class="codeblock" readonly="readonly" value="https://localhost:8081" />
<p class="Introline2">Primary Key</p>
<input
type="text"
class="codeblock"
readonly="readonly"
value="C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
/>
<p class="Introline2">Primary Connection String</p>
<input
type="text"
class="codeblock"
readonly="readonly"
value="AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
/>
</div>
<p class="Introline3"><b>Choose a platform</b></p>
</div>
<div class="container-fluid">
<ul class="nav nav-tabs qslevel">
<li class="active">
<a data-toggle="tab" href="#net"
><img class="qsmenuicons" src="../images/dotnet.png" alt=".NET platform" />.NET</a
>
</li>
<li>
<a data-toggle="tab" href="#corenet"
><img class="qsmenuicons" src="../images/dotnet.png" alt=".NET Core platform" />.NET Core</a
>
</li>
<li>
<a data-toggle="tab" href="#Java"
><img class="qsmenuicons" src="../images/java.png" alt="Java platform" />Java</a
>
</li>
<li>
<a data-toggle="tab" href="#NodeJs"
><img class="qsmenuicons" src="../images/nodejs.png" alt="Node.js platform" />Node.js</a
>
</li>
<li>
<a data-toggle="tab" href="#Python"
><img class="qsmenuicons" src="../images/python.png" alt="Python platform" />Python</a
>
</li>
</ul>
<div class="tab-content tab-content-override">
<div id="net" class="tab-pane fade in active">
<div class="netApp">
<div class="numbersize numbersizePadding">1</div>
<div class="numberheading">
Open and run a sample .NET app
<p>
We created a sample .NET app connected to your Azure Cosmos DB Emulator instance. Download, extract,
build and run the app.
</p>
<a href="quickstart/DocumentDB-Quickstart-DotNet.zip"
><button class="btncreatecoll">Download</button></a
>
</div>
</div>
<div class="netApp">
<div class="numbersize">2</div>
<div class="numberheading">
Learn more about Azure Cosmos DB
<ul>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/samples/dotnet"
>Code Samples</a
>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/docs">Documentation</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/pricing">Pricing</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/capacity-planner"
>Capacity Planner</a
>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/stackoverflow">Forum</a>
</li>
</ul>
</div>
</div>
</div>
<div id="corenet" class="tab-pane fade">
<div class="netApp">
<div class="numbersize numbersizePadding">1</div>
<div class="numberheading">
Open and run a sample .NET Core app
<p>
We created a sample .NET Core app connected to your Azure Cosmos DB Emulator instance. Download,
extract, build and run the app.
</p>
<a href="quickstart/DocumentDB-Quickstart-DotNetCore.zip"
><button class="btncreatecoll">Download</button></a
>
</div>
</div>
<div class="netApp">
<div class="numbersize">2</div>
<div class="numberheading">
Learn more about Azure Cosmos DB.
<ul>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/samples/dotnet"
>Code Samples</a
>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/docs">Documentation</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/pricing">Pricing</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/capacity-planner"
>Capacity Planner</a
>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/stackoverflow">Forum</a>
</li>
</ul>
</div>
</div>
</div>
<div id="Java" class="tab-pane fade">
<div class="step1">
<div class="numbersize numbersizePadding">1</div>
<div class="numberheading">
Open and run a sample Java app
<p>
We created a sample Java app connected to your Azure Cosmos DB Emulator instance. Download, extract,
build and run the app.
</p>
<a href="quickstart/DocumentDB-Quickstart-Java.zip"><button class="btncreatecoll">Download</button></a>
<p>
Follow instructions in the readme.md to setup prerequisites needed to run Java web apps, if you
havent already.
</p>
</div>
</div>
<div class="step1">
<div class="numbersize">2</div>
<div class="numberheading">
Learn more about Azure Cosmos DB.
<ul>
<!--<li><a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/samples/java">Code Samples</a></li>-->
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/docs">Documentation</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/pricing">Pricing</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/capacity-planner"
>Capacity Planner</a
>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/stackoverflow">Forum</a>
</li>
</ul>
</div>
</div>
</div>
<div id="NodeJs" class="tab-pane fade">
<div class="step1">
<div class="numbersize numbersizePadding">1</div>
<div class="numberheading">
Open and run a sample Node.js app
<p>
We created a sample Node.js app connected to your Azure Cosmos DB Emulator instance. Download,
extract, build and run the app.
</p>
<a href="quickstart/DocumentDB-Quickstart-NodeJs.zip"
><button class="btncreatecoll">Download</button></a
>
<p>
Run <strong>npm install</strong> and <strong>npm start</strong>, and navigate to
<a href="http://localhost:3000" _targe="blank">http://localhost:3000</a>.
</p>
</div>
</div>
<div class="step1">
<div class="numbersize">2</div>
<div class="numberheading">
Learn more about Azure Cosmos DB.
<ul>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/samples/nodejs"
>Code Samples</a
>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/docs">Documentation</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/pricing">Pricing</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/capacity-planner"
>Capacity Planner</a
>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/stackoverflow">Forum</a>
</li>
</ul>
</div>
</div>
</div>
<div id="Python" class="tab-pane fade">
<div class="pythonApp">
<div class="numbersize numbersizePadding">1</div>
<div class="numberheading">
Create a new Python app.
<p>
Follow this
<a href="https://aka.ms/cosmos-db-emulator/tutorial/python" target="_blank">tutorial</a>
to create a new Python app connected to Azure Cosmos DB.
</p>
</div>
</div>
<div class="pythonApp">
<div class="numbersize">2</div>
<div class="numberheading">
Learn more about Azure Cosmos DB.
<ul>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/samples/python"
>Code Samples</a
>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/docs">Documentation</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/pricing">Pricing</a>
</li>
<li>
<a target="_blank" class="atags" href="https://aka.ms/cosmos-db-emulator/capacity-planner"
>Capacity planner</a
>
</li>
<li>
<a
target="_blank"
class="atags"
href="https://social.msdn.microsoft.com/forums/azure/home?forum=AzureDocumentDB"
>Forum</a
>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -15,12 +15,18 @@
"target": "es2017",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"lib": ["es5", "es6", "dom"],
"lib": [
"es5",
"es6",
"dom"
],
"jsx": "react",
"moduleResolution": "node",
"resolveJsonModule": true,
"noEmit": true,
"types": ["jest"],
"types": [
"jest"
],
"baseUrl": "src"
},
"typedocOptions": {
@@ -37,11 +43,17 @@
"includes": "./src/SelfServe/Documentation",
"disableSources": true
},
"include": ["src", "./src/**/*", "./utils/**/*"],
"exclude": ["./src/**/__mocks__/**/*", "./utils/local-proxy/**/*"],
"include": [
"src",
"./src/**/*",
"./utils/**/*"
],
"exclude": [
"./src/**/__mocks__/**/*"
],
"ts-node": {
"compilerOptions": {
"module": "CommonJS"
}
}
}
}

View File

@@ -1,6 +0,0 @@
dist*
node_modules
*.cert
*.key
*.pfx
*.log

View File

@@ -1,177 +0,0 @@
const express = require("express");
const { createProxyMiddleware } = require("http-proxy-middleware");
const { inspect } = require("util");
const fs = require("fs");
const https = require("https");
const conf = {};
conf.PORT = process.env.EXPLORER_PORT || 1234;
conf.LOG_LEVEL = process.env.LOG_LEVEL || "info";
conf.EMULATOR_ENDPOINT = process.env.EMULATOR_ENDPOINT || "http://localhost:8081";
conf.ENDPOINT_DISCOVERY_ENABLED = (process.env.ENDPOINT_DISCOVERY_ENABLED || "false").toLowerCase() === "true";
conf.GATEWAY_TLS_ENABLED = (process.env.GATEWAY_TLS_ENABLED || "false").toLowerCase() === "true";
conf.CERT_PATH = process.env.CERT_PATH;
conf.CERT_SECRET = process.env.CERT_SECRET;
const LOG_NUM = levelToNumber(conf.LOG_LEVEL);
function _log(level, msg, color) {
if (levelToNumber(level) >= LOG_NUM) {
console.log(`${colorToCode(color)}[${level || "debug"}]${msg}\x1b[0m`);
}
}
function _debug(msg, color) {
_log("debug", msg, color);
}
function _info(msg, color) {
_log("info", msg, color);
}
function _warn(msg, color) {
_log("warn", msg, color || "yellow");
}
function _err(msg, color) {
_log("error", msg, color || "red");
}
function levelToNumber(level) {
switch (level) {
case "debug":
return 0;
case "info":
return 1;
case "warn":
return 2;
case "error":
return 3;
default:
return 0;
}
}
function colorToCode(color) {
switch (color) {
case "red":
return "\x1b[31m";
case "green":
return "\x1b[32m";
case "blue":
return "\x1b[34m";
case "yellow":
return "\x1b[33m";
default:
return "\x1b[0m";
}
}
function statusToColor(status) {
if (status < 300) {
return "green";
} else if (status < 400) {
return "blue";
} else {
return "red";
}
}
const testEndpoint = () => {
fetch(conf.EMULATOR_ENDPOINT)
.then(async (res) => {
const body = await res.json();
_info("[EMU] Emulator is accessible");
})
.catch((e) => {
_warn("[EMU] Emulator is not accessible");
_warn(`[EMU] ${inspect(e)}`);
});
};
testEndpoint();
const app = express();
app.use((e, req, res, next) => {
_err(`[APP] ${inspect(e)}`);
res.status(500).json({ error: _err.message });
});
app.use((req, res, next) => {
req.startTime = new Date();
res.append("Access-Control-Allow-Origin", "*");
res.append("Access-Control-Allow-Credentials", "true");
res.append("Access-Control-Max-Age", "3600");
res.append("Access-Control-Allow-Headers", "*");
res.append("Access-Control-Allow-Methods", "*");
res.once("finish", () => {
const ms = new Date() - req.startTime;
(res.statusCode < 400 ? _debug : _err)(
`[APP] ${req.method} ${req.url} ${res.statusCode} - ${ms}ms`,
statusToColor(res.statusCode),
);
});
next();
});
app.get("/_ready", (_, res) => {
res.status(200).send("Compilation complete.");
});
const appConf = {
PROXY_PATH: "/proxy",
EMULATOR_ENDPOINT: conf.EMULATOR_ENDPOINT,
platform: "VNextEmulator",
};
app.get("/config.json", (_, res) => {
res.status(200).json(appConf).end();
});
const proxyProxy = createProxyMiddleware({
target: "https://cdb-ms-mpac-pbe.cosmos.azure.com",
changeOrigin: true,
secure: false,
logLevel: conf.LOG_LEVEL,
pathRewrite: { "^/proxy": "" },
router: (req) => {
if (conf.ENDPOINT_DISCOVERY_ENABLED) {
let newTarget = req.headers["x-ms-proxy-target"];
return newTarget;
} else {
return conf.EMULATOR_ENDPOINT;
}
},
});
app.use("/proxy", proxyProxy);
const unsupported = (req, res) => {
res.status(404).send("Unexpected operation. Please create issue.");
};
// TODO: andersonc - I don't believe these are needed for emulator, should confirm and remove.
app.use("/api", unsupported);
app.use("/_explorer", unsupported);
app.use("/explorerProxy", unsupported);
app.use(`/${conf.AZURE_TENANT_ID}`, unsupported);
app.use(express.static("dist"));
_info(`[EMU] Expecting emulator on [${conf.EMULATOR_ENDPOINT}]`);
_info(`[APP] Listening on [${conf.PORT}]`);
if (conf.GATEWAY_TLS_ENABLED) {
if (!conf.CERT_PATH || !conf.CERT_SECRET) {
_err("[APP] Certificate path or secret not provided");
process.exit(1);
}
const options = {
pfx: fs.readFileSync(conf.CERT_PATH),
passphrase: conf.CERT_SECRET,
};
const server = https.createServer(options, app);
server.listen(conf.PORT);
} else {
app.listen(conf.PORT);
}

View File

@@ -1 +0,0 @@
require('./index.js');

View File

@@ -1,984 +0,0 @@
{
"name": "local-proxy",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "local-proxy",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"express": "^4.21.1",
"http-proxy-middleware": "^3.0.3"
}
},
"node_modules/@types/http-proxy": {
"version": "1.17.15",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz",
"integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "22.10.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz",
"integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/express": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-proxy-middleware": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz",
"integrity": "sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==",
"license": "MIT",
"dependencies": {
"@types/http-proxy": "^1.17.15",
"debug": "^4.3.6",
"http-proxy": "^1.18.1",
"is-glob": "^4.0.3",
"is-plain-object": "^5.0.0",
"micromatch": "^4.0.8"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/http-proxy-middleware/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/http-proxy-middleware/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-inspect": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
"integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.4",
"object-inspect": "^1.13.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"license": "MIT"
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
}
}
}

View File

@@ -1,17 +0,0 @@
{
"name": "local-proxy",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node main.js",
"pack": "cd ../.. && npm run build:proxy && cd utils/local-proxy"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"express": "^4.21.1",
"http-proxy-middleware": "^3.0.3"
}
}

View File

@@ -1,39 +0,0 @@
# local-proxy
Lightweight host for Cosmos Explorer
## Quickstart
1. Pre-req - install packages for root project (`cd ../.. && npm ci && cd utils/local-proxy`)
2. Install - install packages for local-proxy (`npm ci`)
3. Pack - `npm run pack` - builds and packs Cosmos Explorer and copies files into project
4. Start - `npm start` - starts the proxy
```bash
cd ../..
npm ci
cd utils/local-proxy
npm ci
npm run pack
npm start
```
## Config
All config is current set via environment variables
| Name | Options (Default) | Description |
| ---------------------------- | ----------------------------------------- | ------------------------------------------------------------ |
| `PORT` | number (`1234`) | The port on which the proxy runs. |
| `LOG_LEVEL` | `debug`, `info`, `warn`, `error` (`info`) | The logging level for the proxy. |
| `EMULATOR_ENDPOINT` | string (`http://localhost:8081`) | The endpoint for the emulator which will be proxied. |
| `ENDPOINT_DISCOVERY_ENABLED` | boolean (`false`) | Determine whether the proxy will rewrite the endpoint or not |
## Dependenies
Node.js v20+
npm (optional)
## Deployment
Copy the entire local-proxy directory to wherever you'd like. If you have npm, you can use `npm start`, else `node main.js`

View File

@@ -1,41 +0,0 @@
#!/usr/bin/env bash
pushd $(dirname $0) > /dev/null
echo Creating self-signed certificate
# Create a self-signed certificate
export CERT_SECRET=$(openssl rand -base64 20)
openssl genrsa 2048 > host.key
chmod 400 host.key
#openssl req -new -x509 -nodes -sha256 -days 365 -key host.key -out host.cert --passin env:CERT_SECRET -subj "/C=US/ST=WA/L=BELLEVUE/O=Microsoft/OU=Azure Cosmos DB/CN=CHRIS ANDERSON/emailAddress=andersonc@microsoft.com"
openssl pkcs12 -export -out host.pfx -inkey host.key -in host.cert --passout env:CERT_SECRET --name "CHRIS ANDERSON"
export CERT_PATH=$(realpath host.pfx)
echo CERT_PATH=$CERT_PATH
popd > /dev/null
export GATEWAY_TLS_ENABLED=true
export EXPLORER_PORT=12345
# Use node to start so we can kill it later
node main.js > ./https-test.log &
node_pid=$!
echo node pid=$node_pid
sleep .5
output=$(curl --insecure -s "https://localhost:12345/_ready")
kill -KILL $node_pid
if [ "$output" != "Compilation complete." ]; then
echo "Failed to start HTTPS server"
cat ./https-test.log
exit 1
fi
echo https test completed

View File

@@ -24,8 +24,6 @@ const AZURE_TENANT_ID = "72f988bf-86f1-41af-91ab-2d7cd011db47";
const RESOURCE_GROUP = "de-e2e-tests";
const AZURE_CLIENT_SECRET = process.env.AZURE_CLIENT_SECRET || process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET; // TODO Remove. Exists for backwards compat with old .env files. Prefer AZURE_CLIENT_SECRET
const ishttps = process.env.GATEWAY_TLS_ENABLED !== "false"; // false -> false, true -> true, default -> true
if (!AZURE_CLIENT_SECRET) {
console.warn("AZURE_CLIENT_SECRET is not set. testExplorer.html will not work.");
}
@@ -133,18 +131,11 @@ module.exports = function (_env = {}, argv = {}) {
template: "src/Terminal/index.html",
chunks: ["terminal"],
}),
//todo - dynamically include apis
ishttps
? new HtmlWebpackPlugin({
filename: "quickstart.html",
template: "src/quickstart-sql-only.html",
chunks: ["quickstart"],
})
: new HtmlWebpackPlugin({
filename: "quickstart.html",
template: "src/quickstart-sql-only-http.html",
chunks: ["quickstart"],
}),
new HtmlWebpackPlugin({
filename: "quickstart.html",
template: "src/quickstart.html",
chunks: ["quickstart"],
}),
new HtmlWebpackPlugin({
filename: "index.html",
template: "src/index.html",
@@ -225,14 +216,6 @@ module.exports = function (_env = {}, argv = {}) {
new EnvironmentPlugin(envVars),
];
if (process.env.EXPLORER_CONFIG_PATH) {
plugins.push(
new CopyWebpackPlugin({
patterns: [{ from: process.env.EXPLORER_CONFIG_PATH, to: "config.json" }],
}),
);
}
if (argv.analyze) {
plugins.push(new BundleAnalyzerPlugin());
}
@@ -297,7 +280,7 @@ module.exports = function (_env = {}, argv = {}) {
// disableHostCheck: true,
liveReload: !isCI,
server: {
type: ishttps ? "https" : "http",
type: "https",
},
host: "0.0.0.0",
port: envVars.PORT,