From c3758306990bca73e68fd2c60a81c117e098f915 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 21 Aug 2026 10:04:38 -0700
Subject: [PATCH 01/16] Bump http-proxy-middleware from 2.0.9 to 2.0.10 (#2563)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.9 to 2.0.10.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.10/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.9...v2.0.10)
---
updated-dependencies:
- dependency-name: http-proxy-middleware
dependency-version: 2.0.10
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 712d2cb8f..099acec8d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -102,6 +102,7 @@
"utility-types": "3.10.0",
"uuid": "9.0.0",
"web-vitals": "4.2.4",
+ "ws": "8.21.3",
"xterm": "4.19.0",
"xterm-addon-fit": "0.5.0",
"zustand": "3.5.0"
@@ -15423,10 +15424,11 @@
}
},
"node_modules/http-proxy-middleware": {
- "version": "2.0.9",
- "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
- "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
+ "version": "2.0.10",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz",
+ "integrity": "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@types/http-proxy": "^1.17.8",
"http-proxy": "^1.18.1",
From f8cfb33618d85aa4d1f91913d3d5f00e3e0fdfd4 Mon Sep 17 00:00:00 2001
From: jawelton74 <103591340+jawelton74@users.noreply.github.com>
Date: Wed, 26 Aug 2026 13:42:14 -0700
Subject: [PATCH 02/16] Remove Phoenix and notebooks (Phase 5) (#2561)
* Remove Phoenix and notebook core
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4e4f2ebd-f2f8-4c2b-b63d-40e1a2b1c662
* Fix merge issue.
---------
Copilot-Session: 4e4f2ebd-f2f8-4c2b-b63d-40e1a2b1c662
---
docs/remove-notebooks-plan.md | 31 +-
package-lock.json | 533 +-----------------
package.json | 6 +-
src/Explorer/ContextMenuButtonFactory.tsx | 13 +-
.../SettingsComponent.test.tsx.snap | 60 --
src/Explorer/Explorer.test.tsx | 30 +-
src/Explorer/Explorer.tsx | 225 +-------
.../CommandBar/CommandBarComponentAdapter.tsx | 26 +-
.../CommandBarComponentButtonFactory.test.ts | 20 +-
.../CommandBarComponentButtonFactory.tsx | 29 +-
.../Menus/CommandBar/CommandBarUtil.tsx | 17 -
.../CommandBar/ConnectionStatusComponent.less | 184 ------
.../CommandBar/ConnectionStatusComponent.tsx | 186 ------
.../CommandBar/MemoryTrackerComponent.less | 24 -
.../CommandBar/MemoryTrackerComponent.tsx | 29 -
.../Notebook/NotebookContainerClient.ts | 195 -------
src/Explorer/Notebook/NotebookContentItem.ts | 14 -
src/Explorer/Notebook/NotebookManager.tsx | 32 --
src/Explorer/Notebook/NotebookUtil.test.ts | 42 --
src/Explorer/Notebook/NotebookUtil.ts | 81 ---
src/Explorer/Notebook/useNotebook.ts | 248 --------
...AddGlobalSecondaryIndexPanel.test.tsx.snap | 10 -
src/Explorer/SplashScreen/SplashScreen.tsx | 7 -
.../DocumentsTabV2/DocumentsTabV2.test.tsx | 9 -
.../DocumentsTabV2Mongo.test.tsx | 9 -
src/Explorer/Tree/Collection.ts | 1 -
src/Explorer/Tree/Database.test.tsx | 9 -
src/Explorer/Tree/ResourceTree.tsx | 31 +-
src/Explorer/Tree/ResourceTreeAdapter.tsx | 148 -----
.../__snapshots__/treeNodeUtil.test.ts.snap | 2 +-
src/Explorer/Tree/treeNodeUtil.test.ts | 56 +-
src/Explorer/Tree/treeNodeUtil.tsx | 9 +-
src/Explorer/useSelectedNode.ts | 6 -
src/Main.tsx | 2 -
src/Phoenix/PhoenixClient.ts | 256 ---------
src/Utils/NotebookConfigurationUtils.ts | 87 ---
src/hooks/useKnockoutExplorer.ts | 15 -
src/hooks/useNotebookSnapshotStore.ts | 15 -
src/hooks/useTabs.ts | 29 -
tsconfig.strict.json | 4 +-
40 files changed, 122 insertions(+), 2608 deletions(-)
delete mode 100644 src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less
delete mode 100644 src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx
delete mode 100644 src/Explorer/Menus/CommandBar/MemoryTrackerComponent.less
delete mode 100644 src/Explorer/Menus/CommandBar/MemoryTrackerComponent.tsx
delete mode 100644 src/Explorer/Notebook/NotebookContainerClient.ts
delete mode 100644 src/Explorer/Notebook/NotebookContentItem.ts
delete mode 100644 src/Explorer/Notebook/NotebookManager.tsx
delete mode 100644 src/Explorer/Notebook/NotebookUtil.test.ts
delete mode 100644 src/Explorer/Notebook/NotebookUtil.ts
delete mode 100644 src/Explorer/Notebook/useNotebook.ts
delete mode 100644 src/Phoenix/PhoenixClient.ts
delete mode 100644 src/Utils/NotebookConfigurationUtils.ts
delete mode 100644 src/hooks/useNotebookSnapshotStore.ts
diff --git a/docs/remove-notebooks-plan.md b/docs/remove-notebooks-plan.md
index 6070eaf3e..6ff123c93 100644
--- a/docs/remove-notebooks-plan.md
+++ b/docs/remove-notebooks-plan.md
@@ -7,14 +7,14 @@ the **Phoenix** compute-container service and the **Juno** service, plus a **Git
integration used to pin/browse notebook repositories. This functionality is being
retired. The goal is to remove all notebooks/Phoenix/Juno/GitHub-for-notebooks code,
dependencies, UI surfaces, telemetry, and configuration, while **preserving the
-database shell terminals** (Mongo / Cassandra / Postgres / VCoreMongo), which today
+database shell terminals** (Mongo / Cassandra / Postgres / VCoreMongo / Cosmos DB NoSQL), which today
share the Terminal infrastructure with notebooks.
This document is the implementation plan only. No code changes are made here.
## Scope decisions (confirmed)
-- **Database shells**: Keep the Mongo/Cassandra/Postgres/VCoreMongo shells, but migrate
+- **Database shells**: Keep the Mongo/Cassandra/Postgres/VCoreMongo/Cosmos DB NoSQL shells, but migrate
them to use the **CloudShell** path exclusively. Remove the legacy Phoenix
notebook-server shell path.
- **GitHub integration**: Remove entirely (it exists only for notebook pinned repos).
@@ -108,7 +108,7 @@ This is a continuation of an in-progress removal effort. Reference commits:
Each phase is independently buildable and shippable. Within each phase, **all
references to removed code are also removed** so the tree compiles. After every phase
run: `npm run compile`, `npm run compile:strict`, `npm run lint`, `npm run format:check`,
-`npm test`, and a webpack build (`npm run build:ci`); manually verify the four shells
+`npm test`, and a webpack build (`npm run build:ci`); manually verify the five shells
still open.
### Phase 1 — Decouple database shells to CloudShell-only ✅ COMPLETED
@@ -235,7 +235,28 @@ engine deleted there. No separate work remains for this phase.
pinned-repo methods.
- Remove GitHub-related localization keys from **all** locale files (`en` + non-English).
-### Phase 5 — Remove Phoenix and the notebook container/allocation core
+### Phase 5 — Remove Phoenix and the notebook container/allocation core ✅ COMPLETED
+> **Status:** Implemented on branch `users/jawelton/remove-notebooks-phase5-081426`.
+> The automated verification sweep is green: `compile`, `compile:strict`, `lint`,
+> `format:check`, `test` (1931 passing, 5 skipped), and `build:ci` (webpack) all pass.
+>
+> **Implementation notes / deviations:**
+> - Removed the Phoenix-only connection status and memory tracker command-bar UI, which
+> depended entirely on the deleted `useNotebook` container state.
+> - Moved the unrelated `isSynapseLinkUpdating` flag from `useNotebook` into the existing
+> `useCommandBar` Zustand store and made static command creation react to it.
+> - Finished the deferred Phase 1 cleanup by replacing remaining mixed
+> `isShellEnabled`/notebook availability checks with the `enableCloudShell` feature gate.
+> The non-Phoenix Mongo fallback remains available when CloudShell is disabled.
+> - Removed the remaining direct nteract/runtime dependencies with no surviving importers:
+> `@nteract/commutable`, `@nteract/core`, `@nteract/fixtures`, `@nteract/myths`, and
+> `rx-jupyter`. Added `@types/uuid` explicitly because surviving CloudShell/UserContext
+> code had previously received UUID declarations through the removed transitive graph.
+> - Preserved the newer Cosmos DB (NoSQL) Cloud Shell added after this roadmap was written;
+> all terminal tabs still instantiate `CloudShellTerminalComponentAdapter`.
+> - Residual notebook/Phoenix config, contracts, telemetry, localization, Juno, generated
+> ARM clients, and images remain intentionally deferred to Phase 6.
+
- Delete `src/Phoenix/`, `src/Explorer/Notebook/NotebookContainerClient.ts`,
`src/Explorer/Notebook/NotebookManager.tsx`, `src/Explorer/Notebook/useNotebook.ts`,
`src/Explorer/Notebook/NotebookContentItem.ts`, `src/Explorer/Notebook/NotebookUtil.ts`
@@ -279,7 +300,7 @@ npm run format:check
npm test
npm run build:ci
```
-Plus manual smoke test: open Mongo, Cassandra, Postgres, and VCoreMongo shells.
+Plus manual smoke test: open Mongo, Cassandra, Postgres, VCoreMongo, and Cosmos DB NoSQL shells.
## Notes & considerations
diff --git a/package-lock.json b/package-lock.json
index 099acec8d..66ca2a646 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,11 +19,7 @@
"@fluentui/react": "8.119.0",
"@fluentui/react-components": "9.54.2",
"@microsoft/applicationinsights-web": "2.6.1",
- "@nteract/commutable": "7.5.1",
- "@nteract/core": "15.1.9",
- "@nteract/fixtures": "2.3.0",
"@nteract/markdown": "4.6.0",
- "@nteract/myths": "0.1.9",
"@octokit/request": "8.4.1",
"@octokit/rest": "17.9.2",
"@testing-library/jest-dom": "6.4.6",
@@ -92,7 +88,6 @@
"react-window": "1.8.10",
"react-youtube": "9.0.1",
"reflect-metadata": "0.1.13",
- "rx-jupyter": "5.5.12",
"shell-quote": "1.10.0",
"styled-components": "5.0.1",
"swr": "0.4.0",
@@ -139,6 +134,7 @@
"@types/sinon": "2.3.3",
"@types/styled-components": "5.1.32",
"@types/underscore": "1.7.36",
+ "@types/uuid": "9.0.8",
"@types/youtube-player": "5.5.6",
"@typescript-eslint/eslint-plugin": "6.7.4",
"@typescript-eslint/parser": "6.7.4",
@@ -6396,154 +6392,6 @@
"node": ">= 8"
}
},
- "node_modules/@nteract/actions": {
- "version": "7.1.0",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@nteract/commutable": "^7.4.5",
- "@nteract/messaging": "^7.0.19",
- "@nteract/types": "^7.1.9",
- "immutable": "^4.0.0-rc.12",
- "rx-jupyter": "^5.5.21"
- }
- },
- "node_modules/@nteract/actions/node_modules/rx-jupyter": {
- "version": "5.5.21",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@nteract/commutable": "^7.4.5",
- "@nteract/messaging": "^7.0.19",
- "@types/ungap__url-search-params": "^0.1.0",
- "@ungap/url-search-params": "^0.2.0",
- "js-cookie": "^2.2.0",
- "rxjs": "^6.6.0",
- "url-join": "^4.0.0"
- }
- },
- "node_modules/@nteract/commutable": {
- "version": "7.5.1",
- "license": "BSD-3-Clause",
- "dependencies": {
- "immutable": "^4.0.0-rc.12",
- "uuid": "^8.0.0"
- }
- },
- "node_modules/@nteract/commutable/node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
- "node_modules/@nteract/core": {
- "version": "15.1.9",
- "resolved": "https://registry.npmjs.org/@nteract/core/-/core-15.1.9.tgz",
- "integrity": "sha512-kQf+7d1Qi27aVOvu+q07pXw8dXFUsU7JJLZ3JeAzSxVizNE/PjbEPn9Ruc+AV/I/FqXZovGp7Q04AGr25hsuyA==",
- "dependencies": {
- "@nteract/actions": "^7.0.11",
- "@nteract/commutable": "^7.4.5",
- "@nteract/epics": "^5.0.11",
- "@nteract/reducers": "^5.1.9",
- "@nteract/selectors": "^3.1.9",
- "@nteract/types": "^7.1.9",
- "redux-logger": "^3.0.6"
- },
- "peerDependencies": {
- "immutable": "^4.0.0-rc.12"
- }
- },
- "node_modules/@nteract/epics": {
- "version": "5.1.0",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@nteract/actions": "^7.0.11",
- "@nteract/commutable": "^7.4.5",
- "@nteract/messaging": "^7.0.19",
- "@nteract/mythic-configuration": "^1.0.11",
- "@nteract/mythic-notifications": "^0.2.11",
- "@nteract/selectors": "^3.1.9",
- "@nteract/types": "^7.1.9",
- "file-saver": "^2.0.0",
- "redux": "^4.0.1",
- "redux-observable": "^2.0.0-alpha.0",
- "rx-jupyter": "^5.5.21",
- "rxjs": "^6.3.3"
- }
- },
- "node_modules/@nteract/epics/node_modules/rx-jupyter": {
- "version": "5.5.21",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@nteract/commutable": "^7.4.5",
- "@nteract/messaging": "^7.0.19",
- "@types/ungap__url-search-params": "^0.1.0",
- "@ungap/url-search-params": "^0.2.0",
- "js-cookie": "^2.2.0",
- "rxjs": "^6.6.0",
- "url-join": "^4.0.0"
- }
- },
- "node_modules/@nteract/fixtures": {
- "version": "2.3.0",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@nteract/commutable": "^7.2.6",
- "@nteract/reducers": "^3.1.0",
- "@nteract/types": "^6.0.0"
- }
- },
- "node_modules/@nteract/fixtures/node_modules/@nteract/actions": {
- "version": "5.0.1",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@nteract/commutable": "^7.2.11",
- "@nteract/messaging": "^7.0.6",
- "@nteract/types": "^6.0.6",
- "immutable": "^4.0.0-rc.12",
- "rx-jupyter": "^5.5.8"
- }
- },
- "node_modules/@nteract/fixtures/node_modules/@nteract/reducers": {
- "version": "3.2.1",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@nteract/actions": "^5.0.1",
- "@nteract/commutable": "^7.2.11",
- "@nteract/types": "^6.0.6",
- "escape-carriage": "^1.3.0",
- "immutable": "^4.0.0-rc.12",
- "lodash.has": "^4.5.2",
- "redux": "^4.0.1",
- "redux-immutable": "^4.0.0",
- "uuid": "^7.0.0"
- }
- },
- "node_modules/@nteract/fixtures/node_modules/@nteract/reducers/node_modules/uuid": {
- "version": "7.0.3",
- "license": "MIT",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
- "node_modules/@nteract/fixtures/node_modules/@nteract/types": {
- "version": "6.0.7",
- "license": "MIT",
- "dependencies": {
- "@nteract/commutable": "^7.2.12",
- "immutable": "^4.0.0-rc.12",
- "rxjs": "^6.3.3",
- "uuid": "^8.0.0"
- }
- },
- "node_modules/@nteract/fixtures/node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
"node_modules/@nteract/markdown": {
"version": "4.6.0",
"license": "BSD-3-Clause",
@@ -6568,184 +6416,6 @@
"react-dom": "^16.13.0 || ^17.0.0"
}
},
- "node_modules/@nteract/messaging": {
- "version": "7.0.20",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@nteract/types": "^7.1.9",
- "@types/uuid": "^8.0.0",
- "lodash.clonedeep": "^4.5.0",
- "rxjs": "^6.6.0",
- "uuid": "^8.0.0"
- }
- },
- "node_modules/@nteract/messaging/node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
- "node_modules/@nteract/mythic-configuration": {
- "version": "1.0.12",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@nteract/myths": "^0.2.11",
- "fs-observable": "^4.1.14"
- }
- },
- "node_modules/@nteract/mythic-configuration/node_modules/@nteract/myths": {
- "version": "0.2.13",
- "license": "BSD-3-Clause",
- "dependencies": {
- "redux": "^4.0.0",
- "redux-observable": "^2.0.0-alpha.0",
- "rxjs": "^6.3.3"
- },
- "peerDependencies": {
- "immutable": "^4.0.0-rc.12",
- "react": "^16.3.2",
- "react-dom": "^16.3.2",
- "react-redux": "^7.2.0"
- }
- },
- "node_modules/@nteract/mythic-configuration/node_modules/react-is": {
- "version": "17.0.2",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
- "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/@nteract/mythic-configuration/node_modules/react-redux": {
- "version": "7.2.9",
- "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
- "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/runtime": "^7.15.4",
- "@types/react-redux": "^7.1.20",
- "hoist-non-react-statics": "^3.3.2",
- "loose-envify": "^1.4.0",
- "prop-types": "^15.7.2",
- "react-is": "^17.0.2"
- },
- "peerDependencies": {
- "react": "^16.8.3 || ^17 || ^18"
- },
- "peerDependenciesMeta": {
- "react-dom": {
- "optional": true
- },
- "react-native": {
- "optional": true
- }
- }
- },
- "node_modules/@nteract/mythic-notifications": {
- "version": "0.2.11",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@blueprintjs/core": "^3.7.0",
- "@nteract/myths": "^0.2.11"
- },
- "peerDependencies": {
- "immutable": "^4.0.0-rc.12",
- "react": "^16.3.2",
- "react-dom": "^16.3.2",
- "styled-components": ">= 5.0.1"
- }
- },
- "node_modules/@nteract/mythic-notifications/node_modules/@nteract/myths": {
- "version": "0.2.13",
- "license": "BSD-3-Clause",
- "dependencies": {
- "redux": "^4.0.0",
- "redux-observable": "^2.0.0-alpha.0",
- "rxjs": "^6.3.3"
- },
- "peerDependencies": {
- "immutable": "^4.0.0-rc.12",
- "react": "^16.3.2",
- "react-dom": "^16.3.2",
- "react-redux": "^7.2.0"
- }
- },
- "node_modules/@nteract/mythic-notifications/node_modules/react-is": {
- "version": "17.0.2",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
- "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/@nteract/mythic-notifications/node_modules/react-redux": {
- "version": "7.2.9",
- "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
- "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/runtime": "^7.15.4",
- "@types/react-redux": "^7.1.20",
- "hoist-non-react-statics": "^3.3.2",
- "loose-envify": "^1.4.0",
- "prop-types": "^15.7.2",
- "react-is": "^17.0.2"
- },
- "peerDependencies": {
- "react": "^16.8.3 || ^17 || ^18"
- },
- "peerDependenciesMeta": {
- "react-dom": {
- "optional": true
- },
- "react-native": {
- "optional": true
- }
- }
- },
- "node_modules/@nteract/myths": {
- "version": "0.1.9",
- "resolved": "https://registry.npmjs.org/@nteract/myths/-/myths-0.1.9.tgz",
- "integrity": "sha512-aPLUamd0kTnzy+8usMsGs+kgMfxfx2QNvDuxd5+ko+9DKuyMJx8quNuqphlxaLXNGiJrnfNi5krXaAyNEil3Sw==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "react-redux": "^6.0.0",
- "redux": "^4.0.0",
- "redux-observable": "^2.0.0-alpha.0",
- "rxjs": "^6.3.3"
- },
- "peerDependencies": {
- "immutable": "^4.0.0-rc.12",
- "react": "^16.3.2",
- "react-dom": "^16.3.2"
- }
- },
- "node_modules/@nteract/myths/node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT"
- },
- "node_modules/@nteract/myths/node_modules/react-redux": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-6.0.1.tgz",
- "integrity": "sha512-T52I52Kxhbqy/6TEfBv85rQSDz6+Y28V/pf52vDWs1YRXG19mcFOGfHnY2HsNFHyhP+ST34Aih98fvt6tqwVcQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.3.1",
- "hoist-non-react-statics": "^3.3.0",
- "invariant": "^2.2.4",
- "loose-envify": "^1.4.0",
- "prop-types": "^15.7.2",
- "react-is": "^16.8.2"
- },
- "peerDependencies": {
- "react": "^16.4.0-0",
- "redux": "^2.0.0 || ^3.0.0 || ^4.0.0-0"
- }
- },
"node_modules/@nteract/presentational-components": {
"version": "3.4.12",
"resolved": "https://registry.npmjs.org/@nteract/presentational-components/-/presentational-components-3.4.12.tgz",
@@ -6763,71 +6433,6 @@
"styled-components": ">= 5.0.1"
}
},
- "node_modules/@nteract/reducers": {
- "version": "5.1.13",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@nteract/actions": "^7.0.11",
- "@nteract/commutable": "^7.4.5",
- "@nteract/types": "^7.1.9",
- "escape-carriage": "^1.3.0",
- "immutable": "^4.0.0-rc.12",
- "lodash.has": "^4.5.2",
- "redux": "^4.0.1",
- "redux-immutable": "^4.0.0",
- "uuid": "^8.0.0"
- }
- },
- "node_modules/@nteract/reducers/node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
- "node_modules/@nteract/selectors": {
- "version": "3.2.0",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@nteract/commutable": "^7.4.5",
- "@nteract/types": "^7.1.9",
- "immutable": "^4.0.0-rc.12",
- "reselect": "^4.0.0",
- "rx-jupyter": "^5.5.21"
- }
- },
- "node_modules/@nteract/selectors/node_modules/rx-jupyter": {
- "version": "5.5.21",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@nteract/commutable": "^7.4.5",
- "@nteract/messaging": "^7.0.19",
- "@types/ungap__url-search-params": "^0.1.0",
- "@ungap/url-search-params": "^0.2.0",
- "js-cookie": "^2.2.0",
- "rxjs": "^6.6.0",
- "url-join": "^4.0.0"
- }
- },
- "node_modules/@nteract/types": {
- "version": "7.1.9",
- "license": "MIT",
- "dependencies": {
- "@nteract/commutable": "^7.4.5",
- "immutable": "^4.0.0-rc.12",
- "rxjs": "^6.6.0",
- "uuid": "^8.0.0"
- }
- },
- "node_modules/@nteract/types/node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
"node_modules/@octokit/auth-token": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
@@ -8364,6 +7969,7 @@
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.5",
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@types/react": "*",
@@ -8555,6 +8161,7 @@
},
"node_modules/@types/react-redux": {
"version": "7.1.7",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.0",
@@ -8678,16 +8285,15 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/ungap__url-search-params": {
- "version": "0.1.2",
- "license": "MIT"
- },
"node_modules/@types/unist": {
"version": "2.0.10",
"license": "MIT"
},
"node_modules/@types/uuid": {
- "version": "8.3.4",
+ "version": "9.0.8",
+ "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/uuid/-/uuid-9.0.8.tgz",
+ "integrity": "sha1-dUW6T8PAA9bHVvZR878WPY8PKbo=",
+ "dev": true,
"license": "MIT"
},
"node_modules/@types/ws": {
@@ -9001,10 +8607,6 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
- "node_modules/@ungap/url-search-params": {
- "version": "0.2.2",
- "license": "ISC"
- },
"node_modules/@webassemblyjs/ast": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
@@ -12093,6 +11695,7 @@
},
"node_modules/debounce": {
"version": "1.2.1",
+ "dev": true,
"license": "MIT"
},
"node_modules/debug": {
@@ -12148,10 +11751,6 @@
}
}
},
- "node_modules/deep-diff": {
- "version": "0.3.8",
- "license": "MIT"
- },
"node_modules/deep-equal": {
"version": "1.1.2",
"license": "MIT",
@@ -13012,10 +12611,6 @@
"node": ">=6"
}
},
- "node_modules/escape-carriage": {
- "version": "1.3.1",
- "license": "MIT"
- },
"node_modules/escape-html": {
"version": "1.0.3",
"dev": true,
@@ -14021,10 +13616,6 @@
"node": "^10.12.0 || >=12.0.0"
}
},
- "node_modules/file-saver": {
- "version": "2.0.5",
- "license": "MIT"
- },
"node_modules/filesize": {
"version": "8.0.7",
"dev": true,
@@ -14033,13 +13624,6 @@
"node": ">= 0.4.0"
}
},
- "node_modules/filewatcher": {
- "version": "3.0.1",
- "license": "MIT",
- "dependencies": {
- "debounce": "^1.0.0"
- }
- },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -14489,27 +14073,6 @@
"dev": true,
"license": "Unlicense"
},
- "node_modules/fs-observable": {
- "version": "4.1.14",
- "license": "BSD-3-Clause",
- "dependencies": {
- "filewatcher": "^3.0.1",
- "mkdirp": "^0.5.1"
- },
- "peerDependencies": {
- "rxjs": "^6.3.3"
- }
- },
- "node_modules/fs-observable/node_modules/mkdirp": {
- "version": "0.5.6",
- "license": "MIT",
- "dependencies": {
- "minimist": "^1.2.6"
- },
- "bin": {
- "mkdirp": "bin/cmd.js"
- }
- },
"node_modules/fs.realpath": {
"version": "1.0.0",
"license": "ISC"
@@ -14518,6 +14081,7 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@@ -20659,10 +20223,6 @@
"jquery": ">=1.8.0 <4.0.0"
}
},
- "node_modules/js-cookie": {
- "version": "2.2.1",
- "license": "MIT"
- },
"node_modules/js-tokens": {
"version": "4.0.0",
"license": "MIT"
@@ -21090,10 +20650,6 @@
"version": "4.0.3",
"license": "MIT"
},
- "node_modules/lodash.clonedeep": {
- "version": "4.5.0",
- "license": "MIT"
- },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"license": "MIT"
@@ -21108,10 +20664,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/lodash.has": {
- "version": "4.5.2",
- "license": "MIT"
- },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -23757,42 +23309,6 @@
"@babel/runtime": "^7.9.2"
}
},
- "node_modules/redux-immutable": {
- "version": "4.0.0",
- "license": "BSD-3-Clause",
- "peerDependencies": {
- "immutable": "^3.8.1 || ^4.0.0-rc.1"
- }
- },
- "node_modules/redux-logger": {
- "version": "3.0.6",
- "license": "MIT",
- "dependencies": {
- "deep-diff": "^0.3.5"
- }
- },
- "node_modules/redux-observable": {
- "version": "2.0.0",
- "license": "MIT",
- "dependencies": {
- "rxjs": "^7.0.0",
- "tslib": "~2.1.0"
- },
- "peerDependencies": {
- "redux": ">=4 <5"
- }
- },
- "node_modules/redux-observable/node_modules/rxjs": {
- "version": "7.8.1",
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.1.0"
- }
- },
- "node_modules/redux-observable/node_modules/tslib": {
- "version": "2.1.0",
- "license": "0BSD"
- },
"node_modules/reflect-metadata": {
"version": "0.1.13",
"license": "Apache-2.0"
@@ -24087,10 +23603,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/reselect": {
- "version": "4.1.8",
- "license": "MIT"
- },
"node_modules/resolve": {
"version": "1.22.8",
"devOptional": true,
@@ -24233,29 +23745,6 @@
"version": "1.3.3",
"license": "BSD-3-Clause"
},
- "node_modules/rx-jupyter": {
- "version": "5.5.12",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@nteract/commutable": "^7.3.2",
- "@nteract/messaging": "^7.0.10",
- "@types/ungap__url-search-params": "^0.1.0",
- "@ungap/url-search-params": "^0.2.0",
- "js-cookie": "^2.2.0",
- "rxjs": "^6.6.0",
- "url-join": "^4.0.0"
- }
- },
- "node_modules/rxjs": {
- "version": "6.6.7",
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^1.9.0"
- },
- "engines": {
- "npm": ">=2.0.0"
- }
- },
"node_modules/safe-array-concat": {
"version": "1.1.2",
"license": "MIT",
@@ -26069,10 +25558,6 @@
"punycode": "^2.1.0"
}
},
- "node_modules/url-join": {
- "version": "4.0.1",
- "license": "MIT"
- },
"node_modules/url-loader": {
"version": "4.1.1",
"dev": true,
diff --git a/package.json b/package.json
index a58c864bd..0bffefbbc 100644
--- a/package.json
+++ b/package.json
@@ -14,11 +14,7 @@
"@fluentui/react": "8.119.0",
"@fluentui/react-components": "9.54.2",
"@microsoft/applicationinsights-web": "2.6.1",
- "@nteract/commutable": "7.5.1",
- "@nteract/core": "15.1.9",
- "@nteract/fixtures": "2.3.0",
"@nteract/markdown": "4.6.0",
- "@nteract/myths": "0.1.9",
"@octokit/request": "8.4.1",
"@octokit/rest": "17.9.2",
"@testing-library/jest-dom": "6.4.6",
@@ -87,7 +83,6 @@
"react-window": "1.8.10",
"react-youtube": "9.0.1",
"reflect-metadata": "0.1.13",
- "rx-jupyter": "5.5.12",
"shell-quote": "1.10.0",
"styled-components": "5.0.1",
"swr": "0.4.0",
@@ -149,6 +144,7 @@
"@types/sinon": "2.3.3",
"@types/styled-components": "5.1.32",
"@types/underscore": "1.7.36",
+ "@types/uuid": "9.0.8",
"@types/youtube-player": "5.5.6",
"@typescript-eslint/eslint-plugin": "6.7.4",
"@typescript-eslint/parser": "6.7.4",
diff --git a/src/Explorer/ContextMenuButtonFactory.tsx b/src/Explorer/ContextMenuButtonFactory.tsx
index 59c49e5ac..f1e85d6cd 100644
--- a/src/Explorer/ContextMenuButtonFactory.tsx
+++ b/src/Explorer/ContextMenuButtonFactory.tsx
@@ -28,7 +28,6 @@ import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { useSidePanel } from "../hooks/useSidePanel";
import Explorer from "./Explorer";
-import { useNotebook } from "./Notebook/useNotebook";
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import StoredProcedure from "./Tree/StoredProcedure";
@@ -120,23 +119,17 @@ export const createCollectionContextMenuButton = (
iconSrc: HostedTerminalIcon,
onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
- if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) {
+ if (userContext.features.enableCloudShell) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
}
},
- label:
- useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell
- ? t(Keys.contextMenu.openMongoShell)
- : t(Keys.contextMenu.newShell),
+ label: userContext.features.enableCloudShell ? t(Keys.contextMenu.openMongoShell) : t(Keys.contextMenu.newShell),
});
}
- if (
- (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) &&
- userContext.apiType === "Cassandra"
- ) {
+ if (userContext.features.enableCloudShell && userContext.apiType === "Cassandra") {
items.push({
iconSrc: HostedTerminalIcon,
onClick: () => {
diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap
index 41e126a53..dd5d3bce8 100644
--- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap
+++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap
@@ -109,25 +109,15 @@ exports[`SettingsComponent renders 1`] = `
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
- "_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
- "phoenixClient": PhoenixClient {
- "armResourceId": undefined,
- "retryOptions": {
- "maxTimeout": 5000,
- "minTimeout": 5000,
- "retries": 3,
- },
- },
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
- "refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"parameters": [Function],
@@ -230,25 +220,15 @@ exports[`SettingsComponent renders 1`] = `
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
- "_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
- "phoenixClient": PhoenixClient {
- "armResourceId": undefined,
- "retryOptions": {
- "maxTimeout": 5000,
- "minTimeout": 5000,
- "retries": 3,
- },
- },
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
- "refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"parameters": [Function],
@@ -455,25 +435,15 @@ exports[`SettingsComponent renders 1`] = `
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
- "_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
- "phoenixClient": PhoenixClient {
- "armResourceId": undefined,
- "retryOptions": {
- "maxTimeout": 5000,
- "minTimeout": 5000,
- "retries": 3,
- },
- },
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
- "refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"parameters": [Function],
@@ -526,25 +496,15 @@ exports[`SettingsComponent renders 1`] = `
}
explorer={
Explorer {
- "_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
- "phoenixClient": PhoenixClient {
- "armResourceId": undefined,
- "retryOptions": {
- "maxTimeout": 5000,
- "minTimeout": 5000,
- "retries": 3,
- },
- },
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
- "refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"parameters": [Function],
@@ -694,25 +654,15 @@ exports[`SettingsComponent renders 1`] = `
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
- "_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
- "phoenixClient": PhoenixClient {
- "armResourceId": undefined,
- "retryOptions": {
- "maxTimeout": 5000,
- "minTimeout": 5000,
- "retries": 3,
- },
- },
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
- "refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"parameters": [Function],
@@ -765,25 +715,15 @@ exports[`SettingsComponent renders 1`] = `
}
explorer={
Explorer {
- "_isInitializingNotebooks": false,
"databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function],
- "phoenixClient": PhoenixClient {
- "armResourceId": undefined,
- "retryOptions": {
- "maxTimeout": 5000,
- "minTimeout": 5000,
- "retries": 3,
- },
- },
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
},
- "refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"parameters": [Function],
diff --git a/src/Explorer/Explorer.test.tsx b/src/Explorer/Explorer.test.tsx
index 5a4e89b94..dbb7bc118 100644
--- a/src/Explorer/Explorer.test.tsx
+++ b/src/Explorer/Explorer.test.tsx
@@ -10,6 +10,7 @@ import { Capability, DatabaseAccount } from "../Contracts/DataModels";
import { updateUserContext, userContext } from "../UserContext";
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import Explorer from "./Explorer";
+import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
const mockUpdate = update as jest.MockedFunction;
@@ -26,21 +27,6 @@ jest.mock("./Controls/Dialog", () => ({
},
}));
-// Silence useNotebook subscription calls
-jest.mock("./Notebook/useNotebook", () => ({
- useNotebook: {
- subscribe: jest.fn(),
- getState: jest.fn().mockReturnValue(
- new Proxy(
- {},
- {
- get: () => jest.fn().mockResolvedValue(undefined),
- },
- ),
- ),
- },
-}));
-
describe("Explorer.openEnableSynapseLinkDialog", () => {
let explorer: Explorer;
@@ -69,6 +55,7 @@ describe("Explorer.openEnableSynapseLinkDialog", () => {
beforeEach(() => {
jest.clearAllMocks();
mockUpdate.mockResolvedValue(undefined);
+ useCommandBar.getState().setIsSynapseLinkUpdating(false);
explorer = new Explorer();
});
@@ -102,6 +89,19 @@ describe("Explorer.openEnableSynapseLinkDialog", () => {
expect(userContext.databaseAccount.properties.enableAnalyticalStorage).toBe(true);
});
+
+ it("should track Synapse Link update progress in the command bar store", async () => {
+ mockUpdate.mockImplementation(async () => {
+ expect(useCommandBar.getState().isSynapseLinkUpdating).toBe(true);
+ return {} as Awaited>;
+ });
+ explorer.openEnableSynapseLinkDialog();
+
+ const dialogProps = mockOpenDialog.mock.calls[0][0];
+ await dialogProps.onPrimaryButtonClick();
+
+ expect(useCommandBar.getState().isSynapseLinkUpdating).toBe(false);
+ });
});
describe("with targetAccountOverride", () => {
diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx
index bf1c5a33e..bc7df461d 100644
--- a/src/Explorer/Explorer.tsx
+++ b/src/Explorer/Explorer.tsx
@@ -1,9 +1,6 @@
import * as msal from "@azure/msal-browser";
-import { Link } from "@fluentui/react/lib/Link";
-import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { sendMessage } from "Common/MessageHandler";
import { stringifyError } from "Common/stringifyError";
-import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import {
@@ -13,32 +10,27 @@ import {
scheduleRefreshFabricToken,
} from "Platform/Fabric/FabricUtil";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
-import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
import { featureRegistered } from "Utils/FeatureRegistrationUtils";
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import * as ko from "knockout";
import React from "react";
import _ from "underscore";
-import shallow from "zustand/shallow";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants";
-import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
import { getErrorMessage, getErrorStack } from "../Common/ErrorHandlingUtils";
import * as Logger from "../Common/Logger";
import { QueriesClient } from "../Common/QueriesClient";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import * as DataModels from "../Contracts/DataModels";
-import { ContainerConnectionInfo, IPhoenixServiceInfo, IProvisionData, IResponse } from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { UploadDetailsRecord } from "../Contracts/ViewModels";
import MetricScenario from "../Metrics/MetricEvents";
import { ApplicationMetricPhase } from "../Metrics/ScenarioConfig";
import { scenarioMonitor } from "../Metrics/ScenarioMonitor";
-import { PhoenixClient } from "../Phoenix/PhoenixClient";
import * as ExplorerSettings from "../Shared/ExplorerSettings";
-import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
+import { Action } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext, userContext } from "../UserContext";
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
@@ -49,8 +41,6 @@ import { ReactTabKind, useTabs } from "../hooks/useTabs";
import "./ComponentRegisterer";
import { DialogProps, useDialog } from "./Controls/Dialog";
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
-import type NotebookManager from "./Notebook/NotebookManager";
-import { useNotebook } from "./Notebook/useNotebook";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel";
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
@@ -78,13 +68,7 @@ export default class Explorer {
// Tabs
public isTabsContentExpanded: ko.Observable;
- // Notebooks
- public notebookManager?: NotebookManager;
-
- private _isInitializingNotebooks: boolean;
-
private static readonly MaxNbDatabasesToAutoExpand = 5;
- public phoenixClient: PhoenixClient;
/**
* Resolves when the initial refreshAllDatabases (including collection loading) completes.
@@ -95,13 +79,6 @@ export default class Explorer {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree,
});
- this._isInitializingNotebooks = false;
-
- this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
- useNotebook.subscribe(
- () => this.refreshCommandBarButtons(),
- (state) => state.isNotebooksEnabledForAccount,
- );
this.queriesClient = new QueriesClient(this);
@@ -156,31 +133,8 @@ export default class Explorer {
startKey,
);
- useNotebook.subscribe(
- async () => this.initiateAndRefreshNotebookList(),
- (state) => [state.isNotebookEnabled, state.isRefreshed],
- shallow,
- );
-
this.resourceTree = new ResourceTreeAdapter(this);
- // Override notebook server parameters from URL parameters
- if (
- userContext.features.notebookServerUrl &&
- validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
- userContext.features.notebookServerToken
- ) {
- useNotebook.getState().setNotebookServerInfo({
- notebookServerEndpoint: userContext.features.notebookServerUrl,
- authToken: userContext.features.notebookServerToken,
- forwardingId: undefined,
- });
- }
-
- if (userContext.features.notebookBasePath) {
- useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
- }
-
if (isFabricMirrored()) {
useTabs.getState().closeReactTab(ReactTabKind.Home);
}
@@ -188,23 +142,6 @@ export default class Explorer {
this.refreshExplorer();
}
- public async initiateAndRefreshNotebookList(): Promise {
- if (!this.notebookManager) {
- const NotebookManager = (await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager"))
- .default;
- this.notebookManager = new NotebookManager();
- this.notebookManager.initialize({
- container: this,
- resourceTree: this.resourceTree,
- refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
- refreshNotebookList: () => this.refreshNotebookList(),
- });
- }
-
- this.refreshCommandBarButtons();
- this.refreshNotebookList();
- }
-
public openEnableSynapseLinkDialog(targetAccountOverride?: DataModels.AccountOverride): void {
const subscriptionId = targetAccountOverride?.subscriptionId ?? userContext.subscriptionId;
const resourceGroup = targetAccountOverride?.resourceGroup ?? userContext.resourceGroup;
@@ -227,7 +164,7 @@ export default class Explorer {
const clearInProgressMessage = logConsoleProgress(
"Enabling Azure Synapse Link for this account. This may take a few minutes before you can enable analytical store for this account.",
);
- useNotebook.getState().setIsSynapseLinkUpdating(true);
+ useCommandBar.getState().setIsSynapseLinkUpdating(true);
useDialog.getState().closeDialog();
try {
@@ -248,7 +185,7 @@ export default class Explorer {
logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`);
TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, {}, startTime);
} finally {
- useNotebook.getState().setIsSynapseLinkUpdating(false);
+ useCommandBar.getState().setIsSynapseLinkUpdating(false);
}
},
@@ -431,7 +368,6 @@ export default class Explorer {
await (userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases());
- await this.refreshNotebookList();
}
logConsoleInfo("Successfully refreshed databases");
@@ -442,108 +378,6 @@ export default class Explorer {
window.open(Constants.Urls.feedbackEmail, "_blank");
};
- public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise {
- if (!databaseAccount) {
- throw new Error("No database account specified");
- }
-
- if (this._isInitializingNotebooks) {
- return;
- }
- this._isInitializingNotebooks = true;
- this.refreshNotebookList();
- this._isInitializingNotebooks = false;
- }
-
- public async allocateContainer(): Promise {
- const notebookServerInfo = useNotebook.getState().notebookServerInfo;
- const isAllocating = useNotebook.getState().isAllocating;
- if (
- isAllocating === false &&
- (notebookServerInfo === undefined ||
- (notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined))
- ) {
- const connectionStatus: ContainerConnectionInfo = {
- status: ConnectionStatusType.Connecting,
- };
-
- useNotebook.getState().setConnectionInfo(connectionStatus);
-
- let connectionInfo;
- try {
- TelemetryProcessor.traceStart(Action.PhoenixConnection, {
- dataExplorerArea: Areas.Notebook,
- });
- useNotebook.getState().setIsAllocating(true);
- const provisionData: IProvisionData = {
- cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
- poolId: undefined,
- };
- connectionInfo = await this.phoenixClient.allocateContainer(provisionData);
- if (!connectionInfo?.data?.phoenixServiceUrl) {
- throw new Error(`PhoenixServiceUrl is invalid!`);
- }
- await this.setNotebookInfo(connectionInfo, connectionStatus);
- TelemetryProcessor.traceSuccess(Action.PhoenixConnection, {
- dataExplorerArea: Areas.Notebook,
- });
- } catch (error) {
- TelemetryProcessor.traceFailure(Action.PhoenixConnection, {
- dataExplorerArea: Areas.Notebook,
- status: error.status,
- error: getErrorMessage(error),
- errorStack: getErrorStack(error),
- });
- connectionStatus.status = ConnectionStatusType.Failed;
- useNotebook.getState().resetContainerConnection(connectionStatus);
- if (error?.status === HttpStatusCodes.Forbidden && error.message) {
- useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
- } else {
- useDialog
- .getState()
- .showOkModalDialog(
- "Connection Failed",
- "We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.",
- );
- }
- throw error;
- } finally {
- useNotebook.getState().setIsAllocating(false);
- this.refreshCommandBarButtons();
- this.refreshNotebookList();
- this._isInitializingNotebooks = false;
- }
- }
- }
-
- public async setNotebookInfo(
- connectionInfo: IResponse,
- connectionStatus: DataModels.ContainerConnectionInfo,
- ): Promise {
- const containerData = {
- forwardingId: connectionInfo.data.forwardingId,
- dbAccountName: userContext.databaseAccount.name,
- };
- await this.phoenixClient.initiateContainerHeartBeat(true, containerData);
-
- connectionStatus.status = ConnectionStatusType.Connected;
- useNotebook.getState().setConnectionInfo(connectionStatus);
-
- const noteBookServerInfo = {
- notebookServerEndpoint:
- (validateEndpoint(userContext.features.notebookServerUrl, allowedNotebookServerUrls) &&
- userContext.features.notebookServerUrl) ||
- connectionInfo.data.phoenixServiceUrl,
- authToken: userContext.features.notebookServerToken || connectionInfo.data.authToken,
- forwardingId: connectionInfo.data.forwardingId,
- };
- useNotebook.getState().setNotebookServerInfo(noteBookServerInfo);
-
- this.notebookManager?.notebookClient
- .getMemoryUsage()
- .then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
- }
-
private getDeltaDatabases(
updatedDatabaseList: DataModels.Database[],
databases: ViewModels.Database[],
@@ -637,11 +471,6 @@ export default class Explorer {
}
}
- private refreshNotebookList = (): Promise => {
- // Notebook authoring and listing have been removed.
- return Promise.resolve();
- };
-
public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
this.connectToNotebookTerminal(kind);
}
@@ -742,24 +571,6 @@ export default class Explorer {
.openSidePanel("Input parameters", );
}
- public getDownloadModalContent(fileName: string): JSX.Element {
- if (useNotebook.getState().isPhoenixNotebooks) {
- return (
- <>
- {Notebook.galleryNotebookDownloadContent1}
-
-
- {Notebook.galleryNotebookDownloadContent2}
-
- {Notebook.learnMore}
-
-
- >
- );
- }
- return Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook.
;
- }
-
public async refreshExplorer(): Promise {
// Start DatabaseLoad scenario before fetching databases
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
@@ -768,8 +579,7 @@ export default class Explorer {
// Run independent initialization tasks in parallel:
// - Database loading (ARM/SDK calls for databases + collections)
- // - Notebook enabled check (Phoenix + Portal backend — no dependency on databases)
- // - Feature registration check (ARM call — no dependency on databases or notebooks)
+ // - Feature registration check (ARM call — no dependency on databases)
const databasesTask =
userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo"
? (async () => {
@@ -786,37 +596,12 @@ export default class Explorer {
})()
: Promise.resolve();
- const notebooksTask = !isFabricNative()
- ? useNotebook.getState().refreshNotebooksEnabledStateForAccount()
- : Promise.resolve();
-
const featureRegistrationTask =
userContext.authType === AuthType.AAD && userContext.apiType === "SQL" && !isFabricNative()
? featureRegistered(userContext.subscriptionId, "ThroughputBucketing")
: Promise.resolve(false);
- const [, , throughputBucketsEnabled] = await Promise.all([databasesTask, notebooksTask, featureRegistrationTask]);
-
- // Notebook initialization depends on refreshNotebooksEnabledStateForAccount completing above
- // TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
- const isNotebookEnabled =
- configContext.platform !== Platform.Fabric &&
- (userContext.features.notebooksDownBanner ||
- useNotebook.getState().isPhoenixNotebooks ||
- useNotebook.getState().isPhoenixFeatures);
- useNotebook.getState().setIsNotebookEnabled(isNotebookEnabled);
- useNotebook
- .getState()
- .setIsShellEnabled(useNotebook.getState().isPhoenixFeatures && isPublicInternetAccessAllowed());
-
- TelemetryProcessor.trace(Action.NotebookEnabled, ActionModifiers.Mark, {
- isNotebookEnabled,
- dataExplorerArea: Constants.Areas.Notebook,
- });
-
- if (useNotebook.getState().isPhoenixNotebooks) {
- await this.initNotebooks(userContext.databaseAccount);
- }
+ const [, throughputBucketsEnabled] = await Promise.all([databasesTask, featureRegistrationTask]);
if (throughputBucketsEnabled) {
updateUserContext({ throughputBucketsEnabled });
diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx
index 4d2326f3f..17b89d650 100644
--- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx
+++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx
@@ -5,14 +5,12 @@
*/
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { makeStyles, useFluent } from "@fluentui/react-components";
-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 { ConnectionStatusType } from "../../../Common/Constants";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode";
@@ -28,6 +26,8 @@ export interface CommandBarStore {
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
isHidden: boolean;
setIsHidden: (isHidden: boolean) => void;
+ isSynapseLinkUpdating: boolean;
+ setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => void;
}
export const useCommandBar: UseStore = create((set) => ({
@@ -35,6 +35,8 @@ export const useCommandBar: UseStore = create((set) => ({
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
isHidden: false,
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
+ isSynapseLinkUpdating: false,
+ setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
}));
const useStyles = makeStyles({
@@ -69,26 +71,25 @@ export const CommandBar: React.FC = ({ container }: Props) => {
const selectedNodeState = useSelectedNode();
const buttons = useCommandBar((state) => state.contextButtons);
const isHidden = useCommandBar((state) => state.isHidden);
+ const isSynapseLinkUpdating = useCommandBar((state) => state.isSynapseLinkUpdating);
// targetDocument is used by referenced components
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { targetDocument } = useFluent();
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
const styles = useStyles();
- const { connectionInfo, isPhoenixNotebooks, isPhoenixFeatures } = useNotebook((state) => ({
- connectionInfo: state.connectionInfo,
- isPhoenixNotebooks: state.isPhoenixNotebooks,
- isPhoenixFeatures: state.isPhoenixFeatures,
- }));
-
// 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]);
+ return CommandBarComponentButtonFactory.createStaticCommandBarButtons(
+ container,
+ selectedNodeState,
+ isSynapseLinkUpdating,
+ );
+ }, [container, selectedNodeState, dataPlaneRbacEnabled, aadTokenUpdated, isSynapseLinkUpdating]);
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
const buttons =
@@ -134,11 +135,6 @@ export const CommandBar: React.FC = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, "var(--colorNeutralBackground1)");
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
- // Add connection status if needed (using the hook values we got at the top level)
- if ((isPhoenixNotebooks || isPhoenixFeatures) && connectionInfo?.status !== ConnectionStatusType.Connect) {
- uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
- }
-
const rootStyle = {
root: {
backgroundColor: "var(--colorNeutralBackground1)",
diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts
index c0d97bb02..0008f99a3 100644
--- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts
+++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts
@@ -4,7 +4,6 @@ import { DatabaseAccount } from "../../../Contracts/DataModels";
import { CollectionBase } from "../../../Contracts/ViewModels";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
-import { useNotebook } from "../../Notebook/useNotebook";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
@@ -37,6 +36,18 @@ describe("CommandBarComponentButtonFactory tests", () => {
expect(enableAzureSynapseLinkBtn).toBeDefined();
});
+ it("Button should be disabled while Synapse Link is updating", () => {
+ const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(
+ mockExplorer,
+ selectedNodeState,
+ true,
+ );
+ const enableAzureSynapseLinkBtn = buttons.find(
+ (button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel,
+ );
+ expect(enableAzureSynapseLinkBtn.disabled).toBe(true);
+ });
+
// TODO: Now that Tables API supports dataplane RBAC, calling createStaticCommandBarButtons will enable the
// Entra ID Login button, which causes this test to fail due to "Invalid hook call.". This seems to be
// unsupported in jest and needs to be tested with react-hooks-testing-library.
@@ -99,11 +110,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
});
- afterEach(() => {
- useNotebook.getState().setIsNotebookEnabled(false);
- useNotebook.getState().setIsNotebooksEnabledForAccount(false);
- });
-
it("Cassandra Api not available - button should be hidden", () => {
updateUserContext({
databaseAccount: {
@@ -127,7 +133,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
expect(openCassandraShellBtn).toBeUndefined();
});
- it("Notebooks is not enabled and is unavailable - button should be shown and disabled", () => {
+ it("Cloud Shell is unavailable - button should be hidden", () => {
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined();
diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx
index ada89d807..c174c8f3a 100644
--- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx
+++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx
@@ -30,7 +30,6 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "../../Controls/Dialog";
import Explorer from "../../Explorer";
-import { useNotebook } from "../../Notebook/useNotebook";
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
import { SettingsPane, useDataPlaneRbac } from "../../Panes/SettingsPane/SettingsPane";
@@ -43,6 +42,7 @@ let counter = 0;
export function createStaticCommandBarButtons(
container: Explorer,
selectedNodeState: SelectedNodeState,
+ isSynapseLinkUpdating = false,
): CommandButtonComponentProps[] {
if (userContext.authType === AuthType.ResourceToken) {
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
@@ -62,7 +62,7 @@ export function createStaticCommandBarButtons(
userContext.apiType !== "Tables" &&
userContext.apiType !== "Cassandra"
) {
- const addSynapseLink = createOpenSynapseLinkDialogButton(container);
+ const addSynapseLink = createOpenSynapseLinkDialogButton(container, isSynapseLinkUpdating);
if (addSynapseLink) {
addDivider();
buttons.push(addSynapseLink);
@@ -136,14 +136,13 @@ export function createContextCommandBarButtons(
const buttons: CommandButtonComponentProps[] = [];
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
- const label =
- useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell ? "Open Mongo Shell" : "New Shell";
+ const label = userContext.features.enableCloudShell ? "Open Mongo Shell" : "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
- if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) {
+ if (userContext.features.enableCloudShell) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else {
selectedCollection && selectedCollection.onNewMongoShellClick();
@@ -157,7 +156,7 @@ export function createContextCommandBarButtons(
}
if (
- (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) &&
+ userContext.features.enableCloudShell &&
!selectedNodeState.isDatabaseNodeOrNoneSelected() &&
userContext.apiType === "Cassandra"
) {
@@ -252,7 +251,10 @@ function areScriptsSupported(): boolean {
);
}
-function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
+function createOpenSynapseLinkDialogButton(
+ container: Explorer,
+ isSynapseLinkUpdating: boolean,
+): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
return undefined;
}
@@ -273,7 +275,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
onCommandClick: () => container.openEnableSynapseLinkDialog(),
commandButtonLabel: label,
hasPopup: false,
- disabled: useNotebook.getState().isSynapseLinkUpdating,
+ disabled: isSynapseLinkUpdating,
ariaLabel: label,
};
}
@@ -514,14 +516,9 @@ function createOpenTerminalButtonByKind(
}
};
const label = `Open ${terminalFriendlyName()} Shell`;
- const tooltip =
- "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
+ const tooltip = "Cloud Shell is not available for this account.";
const isNativeAuthDisabled = terminalKind === ViewModels.TerminalKind.VCoreMongo && isVCoreMongoNativeAuthDisabled();
- const disableButton =
- (!useNotebook.getState().isNotebooksEnabledForAccount &&
- !useNotebook.getState().isNotebookEnabled &&
- !userContext.features.enableCloudShell) ||
- isNativeAuthDisabled;
+ const disableButton = !userContext.features.enableCloudShell || isNativeAuthDisabled;
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
@@ -533,7 +530,7 @@ function createOpenTerminalButtonByKind(
});
return;
}
- if (useNotebook.getState().isNotebookEnabled || userContext.features.enableCloudShell) {
+ if (userContext.features.enableCloudShell) {
container.openNotebookTerminal(terminalKind);
}
},
diff --git a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
index 40628db28..b298be795 100644
--- a/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
+++ b/src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
@@ -17,9 +17,6 @@ import { configContext, Platform } from "../../../ConfigContext";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
-import Explorer from "../../Explorer";
-import { ConnectionStatus } from "./ConnectionStatusComponent";
-import { MemoryTracker } from "./MemoryTrackerComponent";
/**
* Convert our NavbarButtonConfig to UI Fabric buttons
@@ -258,20 +255,6 @@ export const createDivider = (key: string): ICommandBarItemProps => {
};
};
-export const createMemoryTracker = (key: string): ICommandBarItemProps => {
- return {
- key,
- onRender: () => ,
- };
-};
-
-export const createConnectionStatus = (container: Explorer, key: string): ICommandBarItemProps => {
- return {
- key,
- onRender: () => ,
- };
-};
-
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
const handlers: KeyboardHandlerMap = {};
diff --git a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less
deleted file mode 100644
index 0688e0baa..000000000
--- a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.less
+++ /dev/null
@@ -1,184 +0,0 @@
-@import "../../../../less/Common/Constants";
-
-.connectionStatusContainer {
- cursor: default;
- align-items: center;
- border: 1px;
- min-height: 44px;
-
- > span {
- padding-right: 12px;
- font-size: 12px;
- font-family: @DataExplorerFont;
- color: @DefaultFontColor;
- }
- &:focus{
- outline: 0px;
- }
-}
-.commandReactBtn {
- &:hover {
- background-color: rgb(238, 247, 255);
- color: rgb(32, 31, 30);
- cursor: pointer;
- }
- &:focus{
- outline: 1px dashed #605e5c;
- }
-}
-.connectedReactBtn {
- &:hover {
- background-color: rgb(238, 247, 255);
- color: rgb(32, 31, 30);
- cursor: pointer;
- }
- &:focus{
- outline: 0px;
- }
-}
-.connectIcon{
- margin: 0px 4px;
- height: 18px;
- width: 18px;
- color: rgb(0, 120, 212);
-}
- .status {
- position: relative;
- display: block;
- margin-right: 8px;
- width: 1em;
- height: 1em;
- font-size: 9px!important;
- padding: 0px!important;
- border-radius: 0.5em;
- }
-
- .status::before,
- .status::after {
- position: absolute;
- content: "";
- }
-
- .status::before {
- top: 0;
- left: 0;
- width: 1em;
- height: 1em;
- background-color: rgba(#fff, 0.1);
- border-radius: 100%;
- opacity: 1;
- transform: translate3d(0, 0, 0) scale(0);
- }
-
- .connected{
- background-color: green;
- box-shadow:
- 0 0 0 0em rgba(green, 0),
- 0em 0.05em 0.1em rgba(#000000, 0.2);
- transform: translate3d(0, 0, 0) scale(1);
- }
- .connecting{
- background-color:#ffbf00;
- box-shadow:
- 0 0 0 0em rgba(#ffbf00, 0),
- 0em 0.05em 0.1em rgba(#000000, 0.2);
- transform: translate3d(0, 0, 0) scale(1);
- }
- .failed{
- background-color:#bd1919;
- box-shadow:
- 0 0 0 0em rgba(#bd1919, 0),
- 0em 0.05em 0.1em rgba(#000000, 0.2);
- transform: translate3d(0, 0, 0) scale(1);
- }
-
- .status.connecting.is-animating {
- animation: status-outer-connecting 3000ms infinite;
- }
- .status.failed.is-animating {
- animation: status-outer-failed 3000ms infinite;
- }
- .status.connected.is-animating {
- animation: status-outer-connected 3000ms infinite;
- }
- @keyframes status-outer-connected {
-
- 0% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0em #008000, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
- }
- 20% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.6), 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
- }
- 40% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
- }
- 60% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
- }
- 80% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
- }
- 85% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
- }
- }
- @keyframes status-outer-failed {
-
- 0% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0em #bd1919, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
- }
- 20% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0em #c52d2d, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
- }
- 40% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0em #b47b7b, 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
- }
- 60% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
- }
- 80% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
- }
- 85% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
- }
- }
- @keyframes status-outer-connecting {
-
- 0% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0em #ffbf00, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
- }
- 20% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0em #f0dfad, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
- }
- 40% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0em rgba(198, 243, 198, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
- }
- 60% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0em rgba(213, 241, 213, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
- }
- 80% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
- }
- 85% {
- transform: translate3d(0, 0, 0) scale(1);
- box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
- }
- }
\ No newline at end of file
diff --git a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx b/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx
deleted file mode 100644
index 60499661f..000000000
--- a/src/Explorer/Menus/CommandBar/ConnectionStatusComponent.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import {
- FocusTrapCallout,
- FocusZone,
- FocusZoneTabbableElements,
- FontWeights,
- Icon,
- mergeStyleSets,
- ProgressIndicator,
- Stack,
- Text,
- TooltipHost,
-} from "@fluentui/react";
-import { useId } from "@fluentui/react-hooks";
-import { ActionButton, DefaultButton } from "@fluentui/react/lib/Button";
-import * as React from "react";
-import "../../../../less/hostedexplorer.less";
-import { ConnectionStatusType, ContainerStatusType, Notebook } from "../../../Common/Constants";
-import Explorer from "../../Explorer";
-import { useNotebook } from "../../Notebook/useNotebook";
-import "../CommandBar/ConnectionStatusComponent.less";
-interface Props {
- container: Explorer;
-}
-export const ConnectionStatus: React.FC = ({ container }: Props): JSX.Element => {
- const connectionInfo = useNotebook((state) => state.connectionInfo);
- const [second, setSecond] = React.useState("00");
- const [minute, setMinute] = React.useState("00");
- const [isActive, setIsActive] = React.useState(false);
- const [counter, setCounter] = React.useState(0);
- const [statusColor, setStatusColor] = React.useState("");
- const [toolTipContent, setToolTipContent] = React.useState("Connect to temporary workspace.");
- const [isBarDismissed, setIsBarDismissed] = React.useState(false);
- const buttonId = useId("callout-button");
- const containerInfo = useNotebook((state) => state.containerStatus);
-
- const styles = mergeStyleSets({
- callout: {
- width: 320,
- padding: "20px 24px",
- },
- title: {
- marginBottom: 12,
- fontWeight: FontWeights.semilight,
- },
- buttons: {
- display: "flex",
- justifyContent: "flex-end",
- marginTop: 20,
- },
- });
-
- React.useEffect(() => {
- let intervalId: NodeJS.Timeout;
-
- if (isActive) {
- intervalId = setInterval(() => {
- const secondCounter = counter % 60;
- const minuteCounter = Math.floor(counter / 60);
- const computedSecond: string = String(secondCounter).length === 1 ? `0${secondCounter}` : `${secondCounter}`;
- const computedMinute: string = String(minuteCounter).length === 1 ? `0${minuteCounter}` : `${minuteCounter}`;
-
- setSecond(computedSecond);
- setMinute(computedMinute);
-
- setCounter((counter) => counter + 1);
- }, 1000);
- }
- return () => clearInterval(intervalId);
- }, [isActive, counter]);
-
- React.useEffect(() => {
- if (connectionInfo?.status === ConnectionStatusType.Reconnect) {
- setToolTipContent("Click here to Reconnect to temporary workspace.");
- } else if (connectionInfo?.status === ConnectionStatusType.Failed) {
- setStatusColor("status failed is-animating");
- setToolTipContent("Click here to Reconnect to temporary workspace.");
- }
- }, [connectionInfo.status]);
-
- const stopTimer = () => {
- setIsActive(false);
- setCounter(0);
- setSecond("00");
- setMinute("00");
- };
-
- const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
- const totalGB = memoryUsageInfo ? memoryUsageInfo.totalKB / Notebook.memoryGuageToGB : 0;
- const usedGB = totalGB > 0 ? totalGB - memoryUsageInfo.freeKB / Notebook.memoryGuageToGB : 0;
-
- if (
- connectionInfo &&
- (connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.Reconnect)
- ) {
- return (
- container.allocateContainer()}>
-
-
-
- {connectionInfo.status}
-
-
-
- );
- }
-
- if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) {
- stopTimer();
- setIsActive(true);
- setStatusColor("status connecting is-animating");
- setToolTipContent("Connecting to temporary workspace.");
- } else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) {
- stopTimer();
- setStatusColor("status connected is-animating");
- setToolTipContent("Connected to temporary workspace.");
- } else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) {
- stopTimer();
- setStatusColor("status failed is-animating");
- setToolTipContent("Click here to Reconnect to temporary workspace.");
- }
- return (
- <>
-
- ) =>
- connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
- }
- >
-
-
-
- {connectionInfo.status}
-
- {connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
-
- )}
- {connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
- 0.8 ? "lowMemory" : ""}
- description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
- percentComplete={totalGB !== 0 ? usedGB / totalGB : 0}
- />
- )}
-
- {!isBarDismissed &&
- containerInfo.status &&
- containerInfo.status === ContainerStatusType.Active &&
- Math.round(containerInfo.durationLeftInMinutes) <= Notebook.remainingTimeForAlert ? (
- setIsBarDismissed(true)}
- setInitialFocus
- >
-
- Remaining Time
-
-
- This temporary workspace will get disconnected in {Math.round(containerInfo.durationLeftInMinutes)}{" "}
- minutes. To save your work permanently, save your notebooks to a GitHub repository or download the
- notebooks to your local machine before the session ends.
-
-
-
- setIsBarDismissed(true)}>Dimiss
-
-
-
- ) : undefined}
-
-
- >
- );
-};
diff --git a/src/Explorer/Menus/CommandBar/MemoryTrackerComponent.less b/src/Explorer/Menus/CommandBar/MemoryTrackerComponent.less
deleted file mode 100644
index 404025a91..000000000
--- a/src/Explorer/Menus/CommandBar/MemoryTrackerComponent.less
+++ /dev/null
@@ -1,24 +0,0 @@
-@import "../../../../less/Common/Constants";
-
-.memoryTrackerContainer {
- cursor: default;
- align-items: center;
- margin: 0 9px;
- border: 1px;
-
- > span {
- padding-right: 12px;
- font-size: 13px;
- font-family: @DataExplorerFont;
- color: @DefaultFontColor;
- }
-
- > .lowMemory {
- .ms-ProgressIndicator-progressBar {
- background-color: @SelectionHigh;
- }
- .ms-ProgressIndicator-itemDescription {
- color: @SelectionHigh;
- }
- }
-}
\ No newline at end of file
diff --git a/src/Explorer/Menus/CommandBar/MemoryTrackerComponent.tsx b/src/Explorer/Menus/CommandBar/MemoryTrackerComponent.tsx
deleted file mode 100644
index ac0621897..000000000
--- a/src/Explorer/Menus/CommandBar/MemoryTrackerComponent.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { ProgressIndicator, Spinner, SpinnerSize, Stack } from "@fluentui/react";
-import * as React from "react";
-import { useNotebook } from "../../Notebook/useNotebook";
-
-export const MemoryTracker: React.FC = (): JSX.Element => {
- const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
- if (!memoryUsageInfo) {
- return (
-
- Memory
-
-
- );
- }
-
- const totalGB = memoryUsageInfo.totalKB / 1048576;
- const usedGB = totalGB - memoryUsageInfo.freeKB / 1048576;
-
- return (
-
- Memory
- 0.8 ? "lowMemory" : ""}
- description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
- percentComplete={usedGB / totalGB}
- />
-
- );
-};
diff --git a/src/Explorer/Notebook/NotebookContainerClient.ts b/src/Explorer/Notebook/NotebookContainerClient.ts
deleted file mode 100644
index 76d4ba2b8..000000000
--- a/src/Explorer/Notebook/NotebookContainerClient.ts
+++ /dev/null
@@ -1,195 +0,0 @@
-/**
- * Notebook container related stuff
- */
-import { useDialog } from "Explorer/Controls/Dialog";
-import promiseRetry, { AbortError, Options } from "p-retry";
-import { PhoenixClient } from "Phoenix/PhoenixClient";
-import * as Constants from "../../Common/Constants";
-import { ConnectionStatusType, HttpHeaders, HttpStatusCodes, Notebook, PoolIdType } from "../../Common/Constants";
-import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
-import * as Logger from "../../Common/Logger";
-import * as DataModels from "../../Contracts/DataModels";
-import { IPhoenixServiceInfo, IProvisionData, IResponse } from "../../Contracts/DataModels";
-import { userContext } from "../../UserContext";
-import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
-import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
-import { useNotebook } from "./useNotebook";
-
-export class NotebookContainerClient {
- private clearReconnectionAttemptMessage? = () => {};
- private isResettingWorkspace: boolean;
- private phoenixClient: PhoenixClient;
- private retryOptions: Options;
- private scheduleTimerId: NodeJS.Timeout;
-
- constructor(private onConnectionLost: () => void) {
- this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
- this.retryOptions = {
- retries: Notebook.retryAttempts,
- maxTimeout: Notebook.retryAttemptDelayMs,
- minTimeout: Notebook.retryAttemptDelayMs,
- };
-
- this.initHeartbeat(Constants.Notebook.heartbeatDelayMs);
- }
-
- private initHeartbeat(delayMs: number): void {
- this.scheduleHeartbeat(delayMs);
-
- useNotebook.subscribe(
- () => this.scheduleHeartbeat(delayMs),
- (state) => state.notebookServerInfo,
- );
- }
-
- private scheduleHeartbeat(delayMs: number) {
- if (this.scheduleTimerId) {
- clearInterval(this.scheduleTimerId);
- }
-
- const notebookServerInfo = useNotebook.getState().notebookServerInfo;
- if (notebookServerInfo?.notebookServerEndpoint) {
- this.scheduleTimerId = setInterval(async () => {
- const notebookServerInfo = useNotebook.getState().notebookServerInfo;
- if (notebookServerInfo?.notebookServerEndpoint) {
- const memoryUsageInfo = await this.getMemoryUsage();
- useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo);
- }
- }, delayMs);
- }
- }
-
- public async getMemoryUsage(): Promise {
- const notebookServerInfo = useNotebook.getState().notebookServerInfo;
- if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
- const error = "No server endpoint detected";
- Logger.logError(error, "NotebookContainerClient/getMemoryUsage");
- return Promise.reject(error);
- }
-
- if (this.isResettingWorkspace) {
- return undefined;
- }
-
- const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
- try {
- const runMemoryAsync = async () => {
- return await this._getMemoryAsync(notebookServerEndpoint, authToken);
- };
- return await promiseRetry(runMemoryAsync, this.retryOptions);
- } catch (error) {
- Logger.logError(getErrorMessage(error), "NotebookContainerClient/getMemoryUsage");
- if (!this.clearReconnectionAttemptMessage) {
- this.clearReconnectionAttemptMessage = logConsoleProgress(
- "Connection lost with Notebook server. Attempting to reconnect...",
- );
- }
- this.onConnectionLost();
- return undefined;
- }
- }
-
- private async _getMemoryAsync(
- notebookServerEndpoint: string,
- authToken: string,
- ): Promise {
- if (this.shouldExecuteMemoryCall()) {
- const response = await fetch(`${notebookServerEndpoint}api/metrics/memory`, {
- method: "GET",
- headers: {
- Authorization: authToken,
- "content-type": "application/json",
- },
- });
- if (response.ok) {
- if (this.clearReconnectionAttemptMessage) {
- this.clearReconnectionAttemptMessage();
- this.clearReconnectionAttemptMessage = undefined;
- }
- const memoryUsageInfo = await response.json();
- if (memoryUsageInfo) {
- return {
- totalKB: memoryUsageInfo.total,
- freeKB: memoryUsageInfo.free,
- };
- }
- } else if (response.status === HttpStatusCodes.NotFound) {
- throw new AbortError(response.statusText);
- }
- throw new Error(response.statusText);
- } else {
- return undefined;
- }
- }
-
- private shouldExecuteMemoryCall(): boolean {
- return (
- useNotebook.getState().containerStatus?.status === Constants.ContainerStatusType.Active &&
- useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected
- );
- }
-
- public async resetWorkspace(): Promise> {
- this.isResettingWorkspace = true;
- let response: IResponse;
- try {
- response = await this._resetWorkspace();
- } catch (error) {
- Promise.reject(error);
- return response;
- }
- this.isResettingWorkspace = false;
- return response;
- }
-
- private async _resetWorkspace(): Promise> {
- const notebookServerInfo = useNotebook.getState().notebookServerInfo;
- if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
- const error = "No server endpoint detected";
- Logger.logError(error, "NotebookContainerClient/resetWorkspace");
- return Promise.reject(error);
- }
-
- try {
- if (useNotebook.getState().isPhoenixNotebooks) {
- const provisionData: IProvisionData = {
- cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
- poolId: PoolIdType.DefaultPoolId,
- };
- return await this.phoenixClient.resetContainer(provisionData);
- }
- return null;
- } catch (error) {
- Logger.logError(getErrorMessage(error), "NotebookContainerClient/resetWorkspace");
- if (error?.status === HttpStatusCodes.Forbidden && error.message) {
- useDialog.getState().showOkModalDialog("Connection Failed", `${error.message}`);
- } else {
- useDialog
- .getState()
- .showOkModalDialog(
- "Connection Failed",
- "We are unable to connect to the temporary workspace. Please try again in a few minutes. If the error persists, file a support ticket.",
- );
- }
- throw error;
- }
- }
-
- private getNotebookServerConfig(): { notebookServerEndpoint: string; authToken: string } {
- const notebookServerInfo = useNotebook.getState().notebookServerInfo;
- const authToken: string = notebookServerInfo.authToken ? `Token ${notebookServerInfo.authToken}` : undefined;
-
- return {
- notebookServerEndpoint: notebookServerInfo.notebookServerEndpoint,
- authToken,
- };
- }
-
- private getHeaders(): HeadersInit {
- const authorizationHeader = getAuthorizationHeader();
- return {
- [authorizationHeader.header]: authorizationHeader.token,
- [HttpHeaders.contentType]: "application/json",
- };
- }
-}
diff --git a/src/Explorer/Notebook/NotebookContentItem.ts b/src/Explorer/Notebook/NotebookContentItem.ts
deleted file mode 100644
index bb79485b8..000000000
--- a/src/Explorer/Notebook/NotebookContentItem.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-export interface NotebookContentItem {
- name: string;
- path: string;
- type: NotebookContentItemType;
- children?: NotebookContentItem[];
- parent?: NotebookContentItem;
- timestamp?: number;
-}
-
-export enum NotebookContentItemType {
- Notebook,
- File,
- Directory,
-}
diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx
deleted file mode 100644
index 175c76061..000000000
--- a/src/Explorer/Notebook/NotebookManager.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Contains all notebook related stuff meant to be dynamically loaded by explorer
- */
-
-import { JunoClient } from "../../Juno/JunoClient";
-import { userContext } from "../../UserContext";
-import Explorer from "../Explorer";
-import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
-import { NotebookContainerClient } from "./NotebookContainerClient";
-
-export interface NotebookManagerOptions {
- container: Explorer;
- resourceTree: ResourceTreeAdapter;
- refreshCommandBarButtons: () => void;
- refreshNotebookList: () => void;
-}
-
-export default class NotebookManager {
- private params: NotebookManagerOptions;
- public junoClient: JunoClient;
-
- public notebookClient: NotebookContainerClient;
-
- public initialize(params: NotebookManagerOptions): void {
- this.params = params;
- this.junoClient = new JunoClient();
-
- this.notebookClient = new NotebookContainerClient(() =>
- this.params.container.initNotebooks(userContext?.databaseAccount),
- );
- }
-}
diff --git a/src/Explorer/Notebook/NotebookUtil.test.ts b/src/Explorer/Notebook/NotebookUtil.test.ts
deleted file mode 100644
index 97ff4514a..000000000
--- a/src/Explorer/Notebook/NotebookUtil.test.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { NotebookUtil } from "./NotebookUtil";
-
-const fileName = "file";
-const notebookName = "file.ipynb";
-const folderPath = "folder";
-const filePath = `${folderPath}/${fileName}`;
-const notebookPath = `${folderPath}/${notebookName}`;
-
-describe("NotebookUtil", () => {
- describe("isNotebookFile", () => {
- it("works for jupyter file paths", () => {
- expect(NotebookUtil.isNotebookFile(filePath)).toBeFalsy();
- expect(NotebookUtil.isNotebookFile(notebookPath)).toBeTruthy();
- });
- });
-
- describe("getFilePath", () => {
- it("works for jupyter file paths", () => {
- expect(NotebookUtil.getFilePath(folderPath, fileName)).toEqual(filePath);
- });
- });
-
- describe("getParentPath", () => {
- it("works for jupyter file paths", () => {
- expect(NotebookUtil.getParentPath(filePath)).toEqual(folderPath);
- });
- });
-
- describe("getName", () => {
- it("works for jupyter file paths", () => {
- expect(NotebookUtil.getName(filePath)).toEqual(fileName);
- expect(NotebookUtil.getName(notebookPath)).toEqual(notebookName);
- });
- });
-
- describe("replaceName", () => {
- it("works for jupyter file paths", () => {
- expect(NotebookUtil.replaceName(filePath, "newName")).toEqual(filePath.replace(fileName, "newName"));
- expect(NotebookUtil.replaceName(notebookPath, "newName")).toEqual(notebookPath.replace(notebookName, "newName"));
- });
- });
-});
diff --git a/src/Explorer/Notebook/NotebookUtil.ts b/src/Explorer/Notebook/NotebookUtil.ts
deleted file mode 100644
index d8e752f3d..000000000
--- a/src/Explorer/Notebook/NotebookUtil.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import * as StringUtils from "../../Utils/StringUtils";
-import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
-
-// Must match rx-jupyter' FileType
-export type FileType = "directory" | "file" | "notebook";
-
-// Utilities for notebooks
-export class NotebookUtil {
- /**
- * It's a notebook file if the filename ends with .ipynb.
- */
- public static isNotebookFile(notebookPath: string): boolean {
- const fileName = NotebookUtil.getName(notebookPath);
- return !!fileName && StringUtils.endsWith(fileName, ".ipynb");
- }
-
- /**
- * Note: this does not connect the item to a parent in a tree.
- * @param name
- * @param path
- */
- public static createNotebookContentItem(name: string, path: string, type: FileType): NotebookContentItem {
- return {
- name,
- path,
- type: NotebookUtil.getType(type),
- timestamp: NotebookUtil.getCurrentTimestamp(),
- };
- }
-
- /**
- * Convert rx-jupyter type to our type
- * @param type
- */
- public static getType(type: FileType): NotebookContentItemType {
- switch (type) {
- case "directory":
- return NotebookContentItemType.Directory;
- case "notebook":
- return NotebookContentItemType.Notebook;
- case "file":
- return NotebookContentItemType.File;
- default:
- throw new Error(`Unknown file type: ${type}`);
- }
- }
-
- public static getCurrentTimestamp(): number {
- return new Date().getTime();
- }
-
- public static getFilePath(path: string, fileName: string): string {
- return `${path}/${fileName}`;
- }
-
- public static getParentPath(filepath: string): undefined | string {
- const basename = NotebookUtil.getName(filepath);
- if (basename) {
- const parentPath = filepath.split(basename).shift();
- if (parentPath) {
- return parentPath.replace(/\/$/, ""); // no trailling slash
- }
- }
-
- return undefined;
- }
-
- public static getName(path: string): undefined | string {
- return path.split("/").pop();
- }
-
- public static replaceName(path: string, newName: string): string {
- const contentName = path.split("/").pop();
- if (!contentName) {
- throw new Error(`Failed to extract name from path ${path}`);
- }
-
- const basePath = path.split(contentName).shift();
- return `${basePath}${newName}`;
- }
-}
diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts
deleted file mode 100644
index 608358cf7..000000000
--- a/src/Explorer/Notebook/useNotebook.ts
+++ /dev/null
@@ -1,248 +0,0 @@
-import { cloneDeep } from "lodash";
-import create, { UseStore } from "zustand";
-import { AuthType } from "../../AuthType";
-import * as Constants from "../../Common/Constants";
-import { ConnectionStatusType } from "../../Common/Constants";
-import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
-import * as Logger from "../../Common/Logger";
-import { configContext } from "../../ConfigContext";
-import * as DataModels from "../../Contracts/DataModels";
-import { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels";
-import { Action } from "../../Shared/Telemetry/TelemetryConstants";
-import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
-import { userContext } from "../../UserContext";
-import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
-import { useTabs } from "../../hooks/useTabs";
-import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
-
-interface NotebookState {
- isNotebookEnabled: boolean;
- isNotebooksEnabledForAccount: boolean;
- notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
- sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo;
- isSynapseLinkUpdating: boolean;
- memoryUsageInfo: DataModels.MemoryUsageInfo;
- isShellEnabled: boolean;
- notebookBasePath: string;
- isInitializingNotebooks: boolean;
- myNotebooksContentRoot: NotebookContentItem;
- galleryContentRoot: NotebookContentItem;
- connectionInfo: ContainerConnectionInfo;
- notebookFolderName: string;
- isAllocating: boolean;
- isRefreshed: boolean;
- containerStatus: ContainerInfo;
- isPhoenixNotebooks: boolean;
- isPhoenixFeatures: boolean;
- setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
- setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
- setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
- setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) => void;
- setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => void;
- setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void;
- setIsShellEnabled: (isShellEnabled: boolean) => void;
- setNotebookBasePath: (notebookBasePath: string) => void;
- setNotebookFolderName: (notebookFolderName: string) => void;
- refreshNotebooksEnabledStateForAccount: () => Promise;
- findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem;
- insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem) => void;
- updateNotebookItem: (item: NotebookContentItem) => void;
- deleteNotebookItem: (item: NotebookContentItem) => void;
- initializeNotebooksTree: () => Promise;
- setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void;
- setIsAllocating: (isAllocating: boolean) => void;
- resetContainerConnection: (connectionStatus: ContainerConnectionInfo) => void;
- setIsRefreshed: (isAllocating: boolean) => void;
- setContainerStatus: (containerStatus: ContainerInfo) => void;
- getPhoenixStatus: () => Promise;
- setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => void;
- setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
-}
-
-export const useNotebook: UseStore = 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,
- 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 => {
- 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();
- const startKey = TelemetryProcessor.traceStart(Action.RefreshNotebooksEnabled, {
- dataExplorerArea: "Notebook",
- });
- 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 });
- TelemetryProcessor.traceSuccess(Action.RefreshNotebooksEnabled, { isAccountInAllowedLocation }, startKey);
- } catch (error) {
- Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
- set({ isNotebooksEnabledForAccount: false });
- TelemetryProcessor.traceFailure(Action.RefreshNotebooksEnabled, { error: getErrorMessage(error) }, startKey);
- }
- },
- 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): void => {
- const root = cloneDeep(get().myNotebooksContentRoot);
- const parentItem = get().findItem(root, parent);
- item.parent = parentItem;
- if (parentItem.children) {
- parentItem.children.push(item);
- } else {
- parentItem.children = [item];
- }
- set({ myNotebooksContentRoot: root });
- },
- updateNotebookItem: (item: NotebookContentItem): void => {
- const root = 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;
- set({ myNotebooksContentRoot: root });
- },
- deleteNotebookItem: (item: NotebookContentItem): void => {
- const root = cloneDeep(get().myNotebooksContentRoot);
- const parentItem = get().findItem(root, item.parent);
- parentItem.children = parentItem.children.filter((child) => child.path !== item.path);
- set({ myNotebooksContentRoot: root });
- },
- initializeNotebooksTree: async (): Promise => {
- 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,
- };
-
- set({
- myNotebooksContentRoot,
- galleryContentRoot,
- });
- },
- 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) {
- // getDbAccountAllowedStatus has been deprecated; Phoenix features are no longer available.
- set({ isPhoenixNotebooks: false });
- set({ isPhoenixFeatures: false });
- }
- },
- setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),
- setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => set({ isPhoenixFeatures: isPhoenixFeatures }),
-}));
diff --git a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/__snapshots__/AddGlobalSecondaryIndexPanel.test.tsx.snap b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/__snapshots__/AddGlobalSecondaryIndexPanel.test.tsx.snap
index 2495d4165..fd58794e0 100644
--- a/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/__snapshots__/AddGlobalSecondaryIndexPanel.test.tsx.snap
+++ b/src/Explorer/Panes/AddGlobalSecondaryIndexPanel/__snapshots__/AddGlobalSecondaryIndexPanel.test.tsx.snap
@@ -145,25 +145,15 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
= ({ explorer }) => {
React.useEffect(() => {
subscriptions.push(
- {
- dispose: useNotebook.subscribe(
- () => setState({}),
- (state) => state.isNotebookEnabled,
- ),
- },
{ dispose: useSelectedNode.subscribe(() => setState({})) },
{
dispose: useCarousel.subscribe(
diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx
index ded16bf78..6bb014011 100644
--- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx
+++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx
@@ -35,15 +35,6 @@ import * as ViewModels from "../../../Contracts/ViewModels";
import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer";
-jest.mock("rx-jupyter", () => ({
- sessions: {
- create: jest.fn(),
- },
- contents: {
- JupyterContentProvider: jest.fn().mockImplementation(() => ({})),
- },
-}));
-
jest.mock("Common/dataAccess/queryDocuments", () => ({
queryDocuments: jest.fn(() => ({
// Omit headers, because we can't mock a private field and we don't need to test it
diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2Mongo.test.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2Mongo.test.tsx
index e0bacfa5a..7cee70afe 100644
--- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2Mongo.test.tsx
+++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2Mongo.test.tsx
@@ -19,15 +19,6 @@ import { act } from "react-dom/test-utils";
import * as ViewModels from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer";
-jest.mock("rx-jupyter", () => ({
- sessions: {
- create: jest.fn(),
- },
- contents: {
- JupyterContentProvider: jest.fn().mockImplementation(() => ({})),
- },
-}));
-
jest.requireActual("Explorer/Controls/Editor/EditorReact");
const PROPERTY_VALUE = "__SOME_PROPERTY_VALUE__";
diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts
index f78a95eac..78faf849c 100644
--- a/src/Explorer/Tree/Collection.ts
+++ b/src/Explorer/Tree/Collection.ts
@@ -5,7 +5,6 @@ import {
TriggerDefinition,
UserDefinedFunctionDefinition,
} from "@azure/cosmos";
-import { useNotebook } from "Explorer/Notebook/useNotebook";
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
diff --git a/src/Explorer/Tree/Database.test.tsx b/src/Explorer/Tree/Database.test.tsx
index 9fbeb56fe..4bd8f087a 100644
--- a/src/Explorer/Tree/Database.test.tsx
+++ b/src/Explorer/Tree/Database.test.tsx
@@ -6,15 +6,6 @@ import { updateUserContext, userContext } from "../../UserContext";
import Explorer from "../Explorer";
import Database from "./Database";
-jest.mock("rx-jupyter", () => ({
- sessions: {
- create: jest.fn(),
- },
- contents: {
- JupyterContentProvider: jest.fn().mockImplementation(() => ({})),
- },
-}));
-
const createMockContainer = (): Explorer => {
const mockContainer = new Explorer();
return mockContainer;
diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx
index dfcf53422..8ce6e2cb0 100644
--- a/src/Explorer/Tree/ResourceTree.tsx
+++ b/src/Explorer/Tree/ResourceTree.tsx
@@ -18,12 +18,8 @@ import { userContext } from "UserContext";
import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react";
import { useEffect, useMemo } from "react";
-import shallow from "zustand/shallow";
import { useDatabaseLoadScenario } from "../../Metrics/useMetricPhases";
import Explorer from "../Explorer";
-import { useNotebook } from "../Notebook/useNotebook";
-
-export const MyNotebooksTitle = "My Notebooks";
interface ResourceTreeProps {
explorer: Explorer;
@@ -40,13 +36,6 @@ export const ResourceTree: React.FC = ({ explorer }: Resource
const [openItems, setOpenItems] = React.useState([]);
const treeStyles = useTreeStyles();
- const { isNotebookEnabled } = useNotebook(
- (state) => ({
- isNotebookEnabled: state.isNotebookEnabled,
- }),
- shallow,
- );
-
// We intentionally avoid using a state selector here because we want to re-render the tree if the active tab changes.
const { refreshActiveTab } = useTabs();
@@ -65,24 +54,8 @@ export const ResourceTree: React.FC = ({ explorer }: Resource
const databaseTreeNodes = useMemo(() => {
return userContext.authType === AuthType.ResourceToken
? createResourceTokenTreeNodes(resourceTokenCollection)
- : createDatabaseTreeNodes(
- explorer,
- isNotebookEnabled,
- databases,
- refreshActiveTab,
- searchText,
- sortOrder,
- pinnedDatabaseIds,
- );
- }, [
- resourceTokenCollection,
- databases,
- isNotebookEnabled,
- refreshActiveTab,
- searchText,
- sortOrder,
- pinnedDatabaseIds,
- ]);
+ : createDatabaseTreeNodes(explorer, databases, refreshActiveTab, searchText, sortOrder, pinnedDatabaseIds);
+ }, [resourceTokenCollection, databases, refreshActiveTab, searchText, sortOrder, pinnedDatabaseIds]);
const headerNodes: TreeNode[] = isFabricMirrored()
? []
diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx
index e25b76a45..f2f2af369 100644
--- a/src/Explorer/Tree/ResourceTreeAdapter.tsx
+++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx
@@ -1,34 +1,20 @@
-import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
import { shouldShowScriptNodes } from "Explorer/Tree/treeNodeUtil";
import { getItemName } from "Utils/APITypeUtils";
import * as ko from "knockout";
import * as React from "react";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
-import DeleteIcon from "../../../images/delete.svg";
-import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
-import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
-import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
-import FileIcon from "../../../images/notebook/file-cosmos.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 { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
-import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
import { useTabs } from "../../hooks/useTabs";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
-import { useDialog } from "../Controls/Dialog";
import { LegacyTreeComponent, LegacyTreeNode } from "../Controls/TreeComponent/LegacyTreeComponent";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
-import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
-import { NotebookUtil } from "../Notebook/NotebookUtil";
-import { useNotebook } from "../Notebook/useNotebook";
import TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
@@ -37,16 +23,10 @@ import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction";
export class ResourceTreeAdapter implements ReactAdapter {
- public static readonly MyNotebooksTitle = "My Notebooks";
-
private static readonly DataTitle = "DATA";
- private static readonly NotebooksTitle = "NOTEBOOKS";
- private static readonly PseudoDirPath = "PsuedoDir";
public parameters: ko.Observable;
- public myNotebooksContentRoot: NotebookContentItem;
-
public constructor(private container: Explorer) {
this.parameters = ko.observable(Date.now());
@@ -55,56 +35,15 @@ export class ResourceTreeAdapter implements ReactAdapter {
() => this.triggerRender(),
(state) => state.activeTab,
);
- useNotebook.subscribe(
- () => this.triggerRender(),
- (state) => state.isNotebookEnabled,
- );
-
useDatabases.subscribe(() => this.triggerRender());
this.triggerRender();
}
- private traceMyNotebookTreeInfo() {
- const myNotebooksTree = this.myNotebooksContentRoot;
- if (myNotebooksTree.children) {
- // Count 1st generation children (tree is lazy-loaded)
- const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
- myNotebooksTree.children.forEach((treeNode) => {
- switch ((treeNode as NotebookContentItem).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 });
- }
- }
-
public renderComponent(): JSX.Element {
const dataRootNode = this.buildDataTree();
return ;
}
- public async initialize(): Promise {
- const refreshTasks: Promise[] = [];
-
- this.myNotebooksContentRoot = {
- name: useNotebook.getState().notebookFolderName,
- path: useNotebook.getState().notebookBasePath,
- type: NotebookContentItemType.Directory,
- };
-
- return Promise.all(refreshTasks);
- }
-
private buildDataTree(): LegacyTreeNode {
const databaseTreeNodes: LegacyTreeNode[] = useDatabases
.getState()
@@ -403,93 +342,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
return traverse(schema);
}
- private buildChildNodes(
- item: NotebookContentItem,
- onFileClick: (item: NotebookContentItem) => void,
- createDirectoryContextMenu: boolean,
- createFileContextMenu: boolean,
- ): LegacyTreeNode[] {
- if (!item || !item.children) {
- return [];
- } else {
- return item.children.map((item) => {
- const result =
- item.type === NotebookContentItemType.Directory
- ? this.buildNotebookDirectoryNode(item, onFileClick, createDirectoryContextMenu, createFileContextMenu)
- : this.buildNotebookFileNode(item, onFileClick, createFileContextMenu);
- result.timestamp = item.timestamp;
- return result;
- });
- }
- }
-
- private buildNotebookFileNode(
- item: NotebookContentItem,
- onFileClick: (item: NotebookContentItem) => void,
- createFileContextMenu: boolean,
- ): LegacyTreeNode {
- return {
- label: item.name,
- iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon,
- className: "notebookHeader",
- onClick: () => onFileClick(item),
- isSelected: () => {
- const activeTab = useTabs.getState().activeTab;
- return (
- activeTab &&
- activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
- /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
- NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
- */
- (activeTab as any).notebookPath() === item.path
- );
- },
- contextMenu: createFileContextMenu && this.createFileContextMenu(),
- data: item,
- };
- }
-
- private createFileContextMenu(): TreeNodeMenuItem[] {
- return [];
- }
-
- private createDirectoryContextMenu(): TreeNodeMenuItem[] {
- return [];
- }
-
- private buildNotebookDirectoryNode(
- item: NotebookContentItem,
- onFileClick: (item: NotebookContentItem) => void,
- createDirectoryContextMenu: boolean,
- createFileContextMenu: boolean,
- ): LegacyTreeNode {
- return {
- label: item.name,
- iconSrc: undefined,
- className: "notebookHeader",
- isAlphaSorted: true,
- isLeavesParentsSeparate: true,
- onClick: undefined,
- isSelected: () => {
- const activeTab = useTabs.getState().activeTab;
- return (
- activeTab &&
- activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
- /* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
- NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
- */
- (activeTab as any).notebookPath() === item.path
- );
- },
- contextMenu:
- createDirectoryContextMenu && item.path !== ResourceTreeAdapter.PseudoDirPath
- ? this.createDirectoryContextMenu()
- : undefined,
- data: item,
- children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu),
- };
- }
-
public triggerRender() {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
diff --git a/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap b/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap
index ead9bdb15..53c49c24f 100644
--- a/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap
+++ b/src/Explorer/Tree/__snapshots__/treeNodeUtil.test.ts.snap
@@ -363,7 +363,7 @@ exports[`createDatabaseTreeNodes generates the correct tree structure for the Ca
]
`;
-exports[`createDatabaseTreeNodes generates the correct tree structure for the Mongo API, with Notebooks and Phoenix features, on Emulator 1`] = `
+exports[`createDatabaseTreeNodes generates the correct tree structure for the Mongo API, on Emulator 1`] = `
[
{
"children": [
diff --git a/src/Explorer/Tree/treeNodeUtil.test.ts b/src/Explorer/Tree/treeNodeUtil.test.ts
index c64f9276e..f30c3e166 100644
--- a/src/Explorer/Tree/treeNodeUtil.test.ts
+++ b/src/Explorer/Tree/treeNodeUtil.test.ts
@@ -4,7 +4,6 @@ import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
-import { useNotebook } from "Explorer/Notebook/useNotebook";
import { DeleteDatabaseConfirmationPanel } from "Explorer/Panes/DeleteDatabaseConfirmationPanel";
import TabsBase from "Explorer/Tabs/TabsBase";
import StoredProcedure from "Explorer/Tree/StoredProcedure";
@@ -363,7 +362,7 @@ describe("createDatabaseTreeNodes", () => {
},
} as never,
});
- nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab, "");
+ nodes = createDatabaseTreeNodes(explorer, useDatabases.getState().databases, refreshActiveTab, "");
});
it("creates expected tree", () => {
@@ -371,11 +370,10 @@ describe("createDatabaseTreeNodes", () => {
});
});
- it.each<[string, Platform, boolean, Partial, Partial]>([
+ it.each<[string, Platform, Partial, Partial]>([
[
"the SQL API, on Fabric read-only (mirrored)",
Platform.Fabric,
- false,
{ capabilities: [], enableMultipleWriteLocations: true },
{
fabricContext: {
@@ -387,7 +385,6 @@ describe("createDatabaseTreeNodes", () => {
[
"the SQL API, on Fabric non read-only (native)",
Platform.Fabric,
- false,
{ capabilities: [], enableMultipleWriteLocations: true },
{
fabricContext: {
@@ -399,7 +396,6 @@ describe("createDatabaseTreeNodes", () => {
[
"the SQL API, on Portal",
Platform.Portal,
- false,
{ capabilities: [], enableMultipleWriteLocations: true },
{
fabricContext: undefined,
@@ -408,7 +404,6 @@ describe("createDatabaseTreeNodes", () => {
[
"the Cassandra API, serverless, on Hosted",
Platform.Hosted,
- false,
{
capabilities: [
{ name: CapabilityNames.EnableCassandra, description: "" },
@@ -418,45 +413,34 @@ describe("createDatabaseTreeNodes", () => {
{ fabricContext: undefined },
],
[
- "the Mongo API, with Notebooks and Phoenix features, on Emulator",
+ "the Mongo API, on Emulator",
Platform.Emulator,
- true,
{
capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }],
},
{ fabricContext: undefined },
],
- ])(
- "generates the correct tree structure for %s",
- (_, platform, isNotebookEnabled, dbAccountProperties, userContext) => {
- useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled });
- updateConfigContext({ platform });
- updateUserContext({
- ...userContext,
- databaseAccount: {
- properties: {
- enableMultipleWriteLocations: true,
- ...dbAccountProperties,
- },
- } as unknown as DataModels.DatabaseAccount,
- });
- const nodes = createDatabaseTreeNodes(
- explorer,
- isNotebookEnabled,
- useDatabases.getState().databases,
- refreshActiveTab,
- "",
- );
- expect(nodes).toMatchSnapshot();
- },
- );
+ ])("generates the correct tree structure for %s", (_, platform, dbAccountProperties, userContext) => {
+ updateConfigContext({ platform });
+ updateUserContext({
+ ...userContext,
+ databaseAccount: {
+ properties: {
+ enableMultipleWriteLocations: true,
+ ...dbAccountProperties,
+ },
+ } as unknown as DataModels.DatabaseAccount,
+ });
+ const nodes = createDatabaseTreeNodes(explorer, useDatabases.getState().databases, refreshActiveTab, "");
+ expect(nodes).toMatchSnapshot();
+ });
// The above tests focused on the tree structure. The below tests focus on some core behaviors of the nodes.
// They are not exhaustive, because exhaustive tests here require a lot of mocking and can become very brittle.
// The goal is to cover some key behaviors like loading child nodes, opening tabs/side panels, etc.
it("adds new collections to database as they appear", () => {
- const nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab, "");
+ const nodes = createDatabaseTreeNodes(explorer, useDatabases.getState().databases, refreshActiveTab, "");
const giganticDbNode = nodes.find((node) => node.label === giganticDb.id());
expect(giganticDbNode).toBeDefined();
expect(giganticDbNode.children.map((node) => node.label)).toStrictEqual(["schemaCollection", "load more"]);
@@ -488,7 +472,7 @@ describe("createDatabaseTreeNodes", () => {
},
} as unknown as DataModels.DatabaseAccount,
});
- nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab, "");
+ nodes = createDatabaseTreeNodes(explorer, useDatabases.getState().databases, refreshActiveTab, "");
standardDbNode = nodes.find((node) => node.label === standardDb.id());
sharedDbNode = nodes.find((node) => node.label === sharedDb.id());
giganticDbNode = nodes.find((node) => node.label === giganticDb.id());
@@ -643,7 +627,7 @@ describe("createDatabaseTreeNodes", () => {
setup();
// Rebuild the nodes after changing the user/config context.
- nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab, "");
+ nodes = createDatabaseTreeNodes(explorer, useDatabases.getState().databases, refreshActiveTab, "");
standardDbNode = nodes.find((node) => node.label === standardDb.id());
standardCollectionNode = standardDbNode.children.find((node) => node.label === standardCollection.id());
diff --git a/src/Explorer/Tree/treeNodeUtil.tsx b/src/Explorer/Tree/treeNodeUtil.tsx
index 224542755..d784080cb 100644
--- a/src/Explorer/Tree/treeNodeUtil.tsx
+++ b/src/Explorer/Tree/treeNodeUtil.tsx
@@ -137,7 +137,6 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBa
export const createDatabaseTreeNodes = (
container: Explorer,
- isNotebookEnabled: boolean,
databases: ViewModels.Database[],
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
searchText = "",
@@ -184,9 +183,7 @@ export const createDatabaseTreeNodes = (
database
.collections()
.forEach((collection: ViewModels.Collection) =>
- databaseNode.children.push(
- buildCollectionNode(database, collection, isNotebookEnabled, container, refreshActiveTab),
- ),
+ databaseNode.children.push(buildCollectionNode(database, collection, container, refreshActiveTab)),
);
if (database.collectionsContinuationToken) {
@@ -254,14 +251,13 @@ export const createDatabaseTreeNodes = (
export const buildCollectionNode = (
database: ViewModels.Database,
collection: ViewModels.Collection,
- isNotebookEnabled: boolean,
container: Explorer,
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
): TreeNode => {
let children: TreeNode[];
// Flat Tree for Fabric
if (!isFabricMirrored()) {
- children = buildCollectionNodeChildren(database, collection, isNotebookEnabled, container, refreshActiveTab);
+ children = buildCollectionNodeChildren(database, collection, container, refreshActiveTab);
}
const collectionNode: TreeNode = {
@@ -310,7 +306,6 @@ export const buildCollectionNode = (
const buildCollectionNodeChildren = (
database: ViewModels.Database,
collection: ViewModels.Collection,
- isNotebookEnabled: boolean,
container: Explorer,
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
): TreeNode[] => {
diff --git a/src/Explorer/useSelectedNode.ts b/src/Explorer/useSelectedNode.ts
index 4dd34a91e..cbade8ed9 100644
--- a/src/Explorer/useSelectedNode.ts
+++ b/src/Explorer/useSelectedNode.ts
@@ -1,5 +1,3 @@
-import { ConnectionStatusType } from "Common/Constants";
-import { useNotebook } from "Explorer/Notebook/useNotebook";
import create, { UseStore } from "zustand";
import * as ViewModels from "../Contracts/ViewModels";
import { useTabs } from "../hooks/useTabs";
@@ -13,7 +11,6 @@ export interface SelectedNodeState {
collectionId?: string,
subnodeKinds?: ViewModels.CollectionTabKind[],
) => boolean;
- isConnectedToContainer: () => boolean;
}
export const useSelectedNode: UseStore = create((set, get) => ({
@@ -61,7 +58,4 @@ export const useSelectedNode: UseStore = create((set, get) =>
subnodeKinds.includes(selectedSubnodeKind)
);
},
- isConnectedToContainer: (): boolean => {
- return useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected;
- },
}));
diff --git a/src/Main.tsx b/src/Main.tsx
index 6179711d6..e60a5e863 100644
--- a/src/Main.tsx
+++ b/src/Main.tsx
@@ -52,8 +52,6 @@ import { ErrorBoundary } from "./Explorer/ErrorBoundary";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter";
-import "./Explorer/Menus/CommandBar/ConnectionStatusComponent.less";
-import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
import "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import "./Explorer/Panes/PanelComponent.less";
diff --git a/src/Phoenix/PhoenixClient.ts b/src/Phoenix/PhoenixClient.ts
deleted file mode 100644
index f1d722e3b..000000000
--- a/src/Phoenix/PhoenixClient.ts
+++ /dev/null
@@ -1,256 +0,0 @@
-import { configContext } from "ConfigContext";
-import { useDialog } from "Explorer/Controls/Dialog";
-import { Action } from "Shared/Telemetry/TelemetryConstants";
-import { userContext } from "UserContext";
-import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils";
-import promiseRetry, { AbortError, Options } from "p-retry";
-import {
- Areas,
- ConnectionStatusType,
- ContainerStatusType,
- HttpHeaders,
- HttpStatusCodes,
- Notebook,
-} from "../Common/Constants";
-import { getErrorMessage } from "../Common/ErrorHandlingUtils";
-import * as Logger from "../Common/Logger";
-import {
- ContainerConnectionInfo,
- ContainerInfo,
- IContainerData,
- IMaxAllocationTimeExceeded,
- IPhoenixConnectionInfoResult,
- IPhoenixError,
- IPhoenixServiceInfo,
- IProvisionData,
- IResponse,
- PhoenixErrorType,
-} from "../Contracts/DataModels";
-import { useNotebook } from "../Explorer/Notebook/useNotebook";
-import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
-import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
-
-export class PhoenixClient {
- private armResourceId: string;
- private containerHealthHandler: NodeJS.Timeout;
- private retryOptions: Options = {
- retries: Notebook.retryAttempts,
- maxTimeout: Notebook.retryAttemptDelayMs,
- minTimeout: Notebook.retryAttemptDelayMs,
- };
- private abortController: AbortController;
- private abortSignal: AbortSignal;
-
- constructor(armResourceId: string) {
- this.armResourceId = armResourceId;
- }
-
- public async allocateContainer(provisionData: IProvisionData): Promise> {
- this.initializeCancelEventListener();
-
- return promiseRetry(() => this.executeContainerAssignmentOperation(provisionData, "allocate"), {
- retries: 4,
- maxTimeout: 20000,
- minTimeout: 20000,
- signal: this.abortSignal,
- });
- }
-
- public async resetContainer(provisionData: IProvisionData): Promise> {
- return this.executeContainerAssignmentOperation(provisionData, "reset");
- }
-
- private async executeContainerAssignmentOperation(
- provisionData: IProvisionData,
- operation: string,
- ): Promise> {
- let response;
- try {
- response = await fetch(`${this.getPhoenixControlPlanePathPrefix()}/containerconnections/multicontainer`, {
- method: operation === "allocate" ? "POST" : "PATCH",
- headers: PhoenixClient.getHeaders(),
- body: JSON.stringify(provisionData),
- });
- const responseJson = await response?.json();
- if (response.ok) {
- const phoenixConnectionInfoResult = responseJson as IPhoenixConnectionInfoResult[];
- if (
- !phoenixConnectionInfoResult ||
- phoenixConnectionInfoResult.length === 0 ||
- !phoenixConnectionInfoResult[0]
- ) {
- throw new Error("Received invalid phoenix connection response.");
- }
- return {
- status: response.status,
- data: phoenixConnectionInfoResult[0].phoenixServiceInfo,
- };
- }
- const phoenixError = responseJson as IPhoenixError;
- if (response.status === HttpStatusCodes.Forbidden) {
- if (phoenixError.message === "Sequence contains no elements") {
- throw Error("Phoenix container allocation failed, please try again later.");
- }
- throw new AbortError(this.ConvertToForbiddenErrorString(phoenixError));
- }
- throw new AbortError(phoenixError.message);
- } catch (error) {
- error.status = response?.status;
- throw error;
- }
- }
-
- public async initiateContainerHeartBeat(shouldUseNotebookStates: boolean, containerData: IContainerData) {
- if (this.containerHealthHandler) {
- clearTimeout(this.containerHealthHandler);
- }
- await this.getContainerHealth(shouldUseNotebookStates, Notebook.containerStatusHeartbeatDelayMs, containerData);
- }
-
- private scheduleContainerHeartbeat(
- shouldUseNotebookStates: boolean,
- delayMs: number,
- containerData: IContainerData,
- ): void {
- this.containerHealthHandler = setTimeout(async () => {
- await this.getContainerHealth(shouldUseNotebookStates, delayMs, containerData);
- }, delayMs);
- }
-
- private async getContainerStatusAsync(
- shouldUseNotebookStates: boolean,
- containerData: IContainerData,
- ): Promise {
- try {
- const runContainerStatusAsync = async () => {
- const response = await window.fetch(
- `${this.getPhoenixControlPlanePathPrefix()}/${containerData.forwardingId}`,
- {
- method: "GET",
- headers: PhoenixClient.getHeaders(),
- },
- );
- if (response.status === HttpStatusCodes.OK) {
- const containerStatus = await response.json();
- return {
- durationLeftInMinutes: containerStatus?.durationLeftInMinutes,
- phoenixServerInfo: containerStatus?.phoenixServerInfo,
- status: ContainerStatusType.Active,
- };
- } else if (response.status === HttpStatusCodes.NotFound) {
- const error = "Disconnected from compute workspace";
- Logger.logError(error, "");
- const connectionStatus: ContainerConnectionInfo = {
- status: ConnectionStatusType.Reconnect,
- };
- TelemetryProcessor.traceMark(Action.PhoenixHeartBeat, {
- dataExplorerArea: Areas.Notebook,
- message: getErrorMessage(error),
- });
- shouldUseNotebookStates ? useNotebook.getState().resetContainerConnection(connectionStatus) : undefined;
- shouldUseNotebookStates && useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
- shouldUseNotebookStates &&
- useDialog
- .getState()
- .showOkModalDialog(
- "Disconnected",
- "Disconnected from temporary workspace. Please click on connect button to connect to temporary workspace.",
- );
- throw new AbortError(response.statusText);
- } else if (response?.status === HttpStatusCodes.Forbidden) {
- const validationMessage = this.ConvertToForbiddenErrorString(await response.json());
- if (validationMessage) {
- useDialog.getState().showOkModalDialog("Connection Failed", `${validationMessage}`);
- }
- throw new AbortError(response.statusText);
- }
- throw new Error(response.statusText);
- };
- return await promiseRetry(runContainerStatusAsync, this.retryOptions);
- } catch (error) {
- TelemetryProcessor.traceFailure(Action.PhoenixHeartBeat, {
- dataExplorerArea: Areas.Notebook,
- });
- Logger.logError(getErrorMessage(error), "");
- const connectionStatus: ContainerConnectionInfo = {
- status: ConnectionStatusType.Failed,
- };
- shouldUseNotebookStates ? useNotebook.getState().resetContainerConnection(connectionStatus) : undefined;
- shouldUseNotebookStates && useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
- return {
- durationLeftInMinutes: undefined,
- phoenixServerInfo: undefined,
- status: ContainerStatusType.Disconnected,
- };
- }
- }
-
- private async getContainerHealth(shouldUseNotebookStates: boolean, delayMs: number, containerData: IContainerData) {
- const containerInfo = await this.getContainerStatusAsync(shouldUseNotebookStates, containerData);
- shouldUseNotebookStates ? useNotebook.getState().setContainerStatus(containerInfo) : undefined;
-
- const containerStatus = shouldUseNotebookStates ? useNotebook.getState().containerStatus?.status : undefined;
- if (containerStatus === ContainerStatusType.Active) {
- this.scheduleContainerHeartbeat(shouldUseNotebookStates, delayMs, containerData);
- }
- }
-
- private getPhoenixControlPlanePathPrefix(): string {
- if (!this.armResourceId) {
- throw new Error("The Phoenix client was not initialized properly: missing ARM resource id");
- }
-
- const toolsEndpoint =
- userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
-
- if (!validateEndpoint(toolsEndpoint, allowedJunoOrigins)) {
- const error = `${toolsEndpoint} not allowed as tools endpoint`;
- console.error(error);
- throw new Error(error);
- }
-
- return `${toolsEndpoint}/api/controlplane/toolscontainer/cosmosaccounts${this.armResourceId}`;
- }
-
- private static getHeaders(): HeadersInit {
- const authorizationHeader = getAuthorizationHeader();
- return {
- [authorizationHeader.header]: authorizationHeader.token,
- [HttpHeaders.contentType]: "application/json",
- };
- }
-
- private initializeCancelEventListener(): void {
- this.abortController = new AbortController();
- this.abortSignal = this.abortController.signal;
-
- document.addEventListener("keydown", (event: KeyboardEvent) => {
- if (event.ctrlKey && (event.key === "c" || event.key === "z")) {
- this.abortController.abort(new AbortError("Request canceled"));
- }
- });
- }
-
- public ConvertToForbiddenErrorString(jsonData: IPhoenixError): string {
- const errInfo = jsonData;
- switch (errInfo?.type) {
- case PhoenixErrorType.MaxAllocationTimeExceeded: {
- const maxAllocationTimeExceeded = errInfo as IMaxAllocationTimeExceeded;
- const allocateAfterTimestamp = new Date(maxAllocationTimeExceeded?.earliestAllocationTimestamp);
- allocateAfterTimestamp.setDate(allocateAfterTimestamp.getDate() + 1);
- return `${errInfo.message}` + ". Please try again after " + `${allocateAfterTimestamp.toLocaleString()}`;
- }
- case PhoenixErrorType.MaxDbAccountsPerUserExceeded:
- case PhoenixErrorType.MaxUsersPerDbAccountExceeded:
- case PhoenixErrorType.AllocationValidationResult:
- case PhoenixErrorType.RegionNotServicable:
- case PhoenixErrorType.UserMissingPermissionsError:
- case PhoenixErrorType.SubscriptionNotAllowed: {
- return `${errInfo.message}`;
- }
- default: {
- return undefined;
- }
- }
- }
-}
diff --git a/src/Utils/NotebookConfigurationUtils.ts b/src/Utils/NotebookConfigurationUtils.ts
deleted file mode 100644
index 31bfccfdd..000000000
--- a/src/Utils/NotebookConfigurationUtils.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { getErrorMessage } from "../Common/ErrorHandlingUtils";
-import * as Logger from "../Common/Logger";
-import * as DataModels from "../Contracts/DataModels";
-
-interface KernelConnectionMetadata {
- name: string;
- configurationEndpoints: DataModels.NotebookConfigurationEndpoints;
- notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo;
-}
-
-export const _configureServiceEndpoints = async (kernelMetadata: KernelConnectionMetadata): Promise => {
- if (!kernelMetadata) {
- // should never get into this state
- Logger.logWarning("kernel metadata is null or undefined", "NotebookConfigurationUtils/configureServiceEndpoints");
- return;
- }
-
- const notebookConnectionInfo = kernelMetadata.notebookConnectionInfo;
- const configurationEndpoints = kernelMetadata.configurationEndpoints;
- if (notebookConnectionInfo && configurationEndpoints) {
- try {
- const headers: HeadersInit = { "Content-Type": "application/json" };
- if (notebookConnectionInfo.authToken) {
- headers["Authorization"] = `token ${notebookConnectionInfo.authToken}`;
- }
- const response = await fetch(`${notebookConnectionInfo.notebookServerEndpoint}/api/configureEndpoints`, {
- method: "POST",
- headers,
- body: JSON.stringify(configurationEndpoints),
- });
- if (!response.ok) {
- const responseMessage = await response.json();
- Logger.logError(
- getErrorMessage(responseMessage),
- "NotebookConfigurationUtils/configureServiceEndpoints",
- response.status,
- );
- }
- } catch (error) {
- Logger.logError(getErrorMessage(error), "NotebookConfigurationUtils/configureServiceEndpoints");
- }
- }
-};
-
-export const configureServiceEndpoints = async (
- notebookPath: string,
- notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo,
- kernelName: string,
- clusterConnectionInfo: DataModels.SparkClusterConnectionInfo,
-): Promise => {
- if (!notebookPath || !notebookConnectionInfo || !kernelName) {
- Logger.logError(
- "Invalid or missing notebook connection info/path",
- "NotebookConfigurationUtils/configureServiceEndpoints",
- );
- return Promise.reject("Invalid or missing notebook connection info");
- }
-
- if (!clusterConnectionInfo || !clusterConnectionInfo.endpoints || clusterConnectionInfo.endpoints.length === 0) {
- Logger.logError(
- "Invalid or missing cluster connection info/endpoints",
- "NotebookConfigurationUtils/configureServiceEndpoints",
- );
- return Promise.reject("Invalid or missing cluster connection info");
- }
-
- const notebookEndpointInfo: DataModels.NotebookConfigurationEndpointInfo[] = clusterConnectionInfo.endpoints.map(
- (clusterEndpoint) => ({
- type: clusterEndpoint.kind.toLowerCase(),
- endpoint: clusterEndpoint && clusterEndpoint.endpoint,
- username: clusterConnectionInfo.userName,
- password: clusterConnectionInfo.password,
- token: "", // TODO. This was arcadiaToken() when our synapse/spark integration comes back
- }),
- );
- const configurationEndpoints: DataModels.NotebookConfigurationEndpoints = {
- path: notebookPath,
- endpoints: notebookEndpointInfo,
- };
- const kernelMetadata: KernelConnectionMetadata = {
- configurationEndpoints,
- notebookConnectionInfo,
- name: kernelName,
- };
-
- return await _configureServiceEndpoints(kernelMetadata);
-};
diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts
index 41bec9797..8b8c63a3a 100644
--- a/src/hooks/useKnockoutExplorer.ts
+++ b/src/hooks/useKnockoutExplorer.ts
@@ -83,12 +83,6 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
useEffect(() => {
const effect = async () => {
if (platform) {
- //Updating phoenix feature flags for MPAC based of config context
- if (configContext.isPhoenixEnabled === true) {
- userContext.features.phoenixNotebooks = true;
- userContext.features.phoenixFeatures = true;
- }
-
let explorer: Explorer;
try {
if (platform === Platform.Hosted) {
@@ -1010,15 +1004,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) {
userContext.features.partitionKeyDefault2 = true;
}
- if (inputs.flights.indexOf(Flights.PhoenixNotebooks) !== -1) {
- userContext.features.phoenixNotebooks = true;
- }
- if (inputs.flights.indexOf(Flights.PhoenixFeatures) !== -1) {
- userContext.features.phoenixFeatures = true;
- }
- if (inputs.flights.indexOf(Flights.NotebooksDownBanner) !== -1) {
- userContext.features.notebooksDownBanner = true;
- }
}
// Handle initial theme from portal
diff --git a/src/hooks/useNotebookSnapshotStore.ts b/src/hooks/useNotebookSnapshotStore.ts
deleted file mode 100644
index 841f6b5d1..000000000
--- a/src/hooks/useNotebookSnapshotStore.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import create, { UseStore } from "zustand";
-
-export interface NotebookSnapshotHooks {
- snapshot?: string;
- error?: string;
- setSnapshot: (imageSrc: string) => void;
- setError: (error: string) => void;
-}
-
-export const useNotebookSnapshotStore: UseStore = create((set) => ({
- snapshot: undefined,
- error: undefined,
- setSnapshot: (imageSrc: string) => set((state) => ({ ...state, snapshot: imageSrc })),
- setError: (error: string) => set((state) => ({ ...state, error })),
-}));
diff --git a/src/hooks/useTabs.ts b/src/hooks/useTabs.ts
index f276e1498..4444f5c8c 100644
--- a/src/hooks/useTabs.ts
+++ b/src/hooks/useTabs.ts
@@ -9,7 +9,6 @@ import {
} from "Shared/AppStatePersistenceUtility";
import create, { UseStore } from "zustand";
import * as ViewModels from "../Contracts/ViewModels";
-import { CollectionTabKind } from "../Contracts/ViewModels";
import TabsBase from "../Explorer/Tabs/TabsBase";
export interface TabsState {
@@ -27,7 +26,6 @@ export interface TabsState {
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void;
closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void;
closeTab: (tab: TabsBase) => void;
- closeAllNotebookTabs: (hardClose: boolean) => void;
openAndActivateReactTab: (tabKind: ReactTabKind) => void;
closeReactTab: (tabKind: ReactTabKind) => void;
setIsTabExecuting: (state: boolean) => void;
@@ -132,33 +130,6 @@ export const useTabs: UseStore = create((set, get) => ({
get().persistTabsState();
},
- closeAllNotebookTabs: (): 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: TabsBase) => {
- const tabKind: CollectionTabKind = tab.tabKind;
- if (tabKind && isNotebook(tabKind)) {
- tab.onCloseTabButtonClick();
- }
- });
-
- if (get().openedTabs.length === 0 && !isFabricMirrored()) {
- set({ activeTab: undefined, activeReactTab: undefined });
- }
- }
- },
openAndActivateReactTab: (tabKind: ReactTabKind) => {
if (get().openedReactTabs.indexOf(tabKind) === -1) {
set((state) => ({
diff --git a/tsconfig.strict.json b/tsconfig.strict.json
index 947b073e5..1a2ae5ece 100644
--- a/tsconfig.strict.json
+++ b/tsconfig.strict.json
@@ -44,7 +44,6 @@
"./src/Explorer/Graph/GraphExplorerComponent/GraphData.ts",
"./src/Explorer/LazyMonaco.ts",
"./src/Explorer/Menus/NotificationConsole/ConsoleData.tsx",
- "./src/Explorer/Notebook/NotebookContentItem.ts",
"./src/Explorer/OpenFullScreen.test.tsx",
"./src/Explorer/OpenFullScreen.tsx",
"./src/Explorer/Panes/PanelContainerComponent.test.tsx",
@@ -107,7 +106,6 @@
"./src/hooks/useConfig.ts",
"./src/hooks/useDirectories.tsx",
"./src/hooks/useGraphPhoto.tsx",
- "./src/hooks/useNotebookSnapshotStore.ts",
"./src/hooks/useNotificationConsole.ts",
"./src/hooks/useObservable.ts",
"./src/hooks/useSidePanel.ts",
@@ -137,4 +135,4 @@
"src/Shared/Telemetry/**/*",
"src/Utils/arm/**/*"
]
-}
+}
\ No newline at end of file
From d1c122d300fe4f10ee37791c32fc6f68756e83cf Mon Sep 17 00:00:00 2001
From: jawelton74 <103591340+jawelton74@users.noreply.github.com>
Date: Thu, 27 Aug 2026 08:40:59 -0700
Subject: [PATCH 03/16] Update to Node.js 22 (#2567)
* Update project to Node.js 22
Standardize local development and GitHub Actions on Node.js 22.x. Update Node typings to the latest release compatible with the project's TypeScript 4.9 compiler.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3d0e0c37-ce73-44b6-a068-e24e87cf8fbd
* Remove .nvmrc created by copilot.
---------
Copilot-Session: 3d0e0c37-ce73-44b6-a068-e24e87cf8fbd
---
.github/workflows/ci.yml | 30 +++++++++++++++---------------
.github/workflows/cleanup.yml | 4 ++--
README.md | 1 +
package-lock.json | 21 +++++++++++++--------
package.json | 5 ++++-
5 files changed, 35 insertions(+), 26 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d4e67e782..274d5b862 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,10 +20,10 @@ jobs:
if: github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v4
- - name: Use Node.js 18.x
+ - name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
- node-version: 18.x
+ node-version: 22.x
- run: npm ci
- run: node utils/codeMetrics.js
env:
@@ -33,10 +33,10 @@ jobs:
name: "Compile TypeScript"
steps:
- uses: actions/checkout@v4
- - name: Use Node.js 18.x
+ - name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
- node-version: 18.x
+ node-version: 22.x
- run: npm ci
- run: npm run compile
- run: npm run compile:strict
@@ -45,10 +45,10 @@ jobs:
name: "Check Format"
steps:
- uses: actions/checkout@v4
- - name: Use Node.js 18.x
+ - name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
- node-version: 18.x
+ node-version: 22.x
- run: npm ci
- run: npm run format:check
lint:
@@ -56,10 +56,10 @@ jobs:
name: "Lint"
steps:
- uses: actions/checkout@v4
- - name: Use Node.js 18.x
+ - name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
- node-version: 18.x
+ node-version: 22.x
- run: npm ci
- run: npm run lint
unittest:
@@ -67,10 +67,10 @@ jobs:
name: "Unit Tests"
steps:
- uses: actions/checkout@v4
- - name: Use Node.js 18.x
+ - name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
- node-version: 18.x
+ node-version: 22.x
- run: npm ci
- run: npm run test
build:
@@ -78,10 +78,10 @@ jobs:
name: "Build"
steps:
- uses: actions/checkout@v4
- - name: Use Node.js 18.x
+ - name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
- node-version: 18.x
+ node-version: 22.x
- run: npm ci
- run: npm run build:contracts
- name: Restore Build Cache
@@ -173,10 +173,10 @@ jobs:
shardTotal: [20]
steps:
- uses: actions/checkout@v4
- - name: Use Node.js 18.x
+ - name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
- node-version: 18.x
+ node-version: 22.x
- run: npm ci
- run: npx playwright install --with-deps
- name: "Az CLI login"
@@ -265,7 +265,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
- node-version: 18
+ node-version: 22.x
- name: Install dependencies
run: npm ci
diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml
index b045f3bd3..add9f15e1 100644
--- a/.github/workflows/cleanup.yml
+++ b/.github/workflows/cleanup.yml
@@ -31,9 +31,9 @@ jobs:
tenant-id: ${{ secrets.E2ETESTS_TENANT_ID }}
subscription-id: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
- - name: Use Node.js 20.x
+ - name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
- node-version: 20.x
+ node-version: 22.x
- run: npm ci
- run: node utils/cleanupDBs.js
\ No newline at end of file
diff --git a/README.md b/README.md
index 797ab6c2b..8b0a5c0fc 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,7 @@ UI for Azure Cosmos DB. Powers the [Azure Portal](https://portal.azure.com/), ht
## Getting Started
+- Install Node.js 22.x.
- `npm install`
- `npm run build`
diff --git a/package-lock.json b/package-lock.json
index 66ca2a646..01ec4ba8f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -121,7 +121,7 @@
"@types/hasher": "0.0.31",
"@types/jest": "29.5.12",
"@types/jquery": "3.5.29",
- "@types/node": "18.19.0",
+ "@types/node": "22.14.1",
"@types/post-robot": "10.0.1",
"@types/q": "1.5.1",
"@types/react": "17.0.44",
@@ -193,6 +193,9 @@
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.2.5",
"ws": "8.21.3"
+ },
+ "engines": {
+ "node": "22.x"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -8088,11 +8091,12 @@
}
},
"node_modules/@types/node": {
- "version": "18.19.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.0.tgz",
- "integrity": "sha512-667KNhaD7U29mT5wf+TZUnrzPrlL2GNQ5N0BMjO2oNULhBxX0/FKCkm6JMu0Jh7Z+1LwUlR21ekd7KhIboNFNw==",
+ "version": "22.14.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
+ "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
+ "license": "MIT",
"dependencies": {
- "undici-types": "~5.26.4"
+ "undici-types": "~6.21.0"
}
},
"node_modules/@types/node-fetch": {
@@ -25392,9 +25396,10 @@
"license": "MIT"
},
"node_modules/undici-types": {
- "version": "5.26.5",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
- "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "license": "MIT"
},
"node_modules/unherit": {
"version": "1.1.3",
diff --git a/package.json b/package.json
index 0bffefbbc..201da1576 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,9 @@
"version": "1.0.0",
"description": "Cosmos Explorer",
"main": "index.js",
+ "engines": {
+ "node": "22.x"
+ },
"dependencies": {
"@azure/arm-cosmosdb": "16.4.0",
"@azure/cosmos": "4.7.0",
@@ -131,7 +134,7 @@
"@types/hasher": "0.0.31",
"@types/jest": "29.5.12",
"@types/jquery": "3.5.29",
- "@types/node": "18.19.0",
+ "@types/node": "22.14.1",
"@types/post-robot": "10.0.1",
"@types/q": "1.5.1",
"@types/react": "17.0.44",
From f30a613ab3ce034e9f7491a4ed185fd4b0666f86 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 27 Aug 2026 12:25:57 -0700
Subject: [PATCH 04/16] Bump serialize-javascript, terser-webpack-plugin and
copy-webpack-plugin (#2525)
Bumps [serialize-javascript](https://github.com/yahoo/serialize-javascript) to 7.1.0 and updates ancestor dependencies [serialize-javascript](https://github.com/yahoo/serialize-javascript), [terser-webpack-plugin](https://github.com/webpack/minimizer-webpack-plugin) and [copy-webpack-plugin](https://github.com/webpack/copy-webpack-plugin). These dependencies need to be updated together.
Updates `serialize-javascript` from 6.0.2 to 7.1.0
- [Release notes](https://github.com/yahoo/serialize-javascript/releases)
- [Commits](https://github.com/yahoo/serialize-javascript/compare/v6.0.2...v7.1.0)
Updates `terser-webpack-plugin` from 5.3.16 to 5.6.1
- [Release notes](https://github.com/webpack/minimizer-webpack-plugin/releases)
- [Changelog](https://github.com/webpack/minimizer-webpack-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/minimizer-webpack-plugin/compare/v5.3.16...v5.6.1)
Updates `copy-webpack-plugin` from 11.0.0 to 14.0.0
- [Release notes](https://github.com/webpack/copy-webpack-plugin/releases)
- [Changelog](https://github.com/webpack/copy-webpack-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/copy-webpack-plugin/compare/v11.0.0...v14.0.0)
Updates `terser-webpack-plugin` from 5.3.9 to 5.6.1
- [Release notes](https://github.com/webpack/minimizer-webpack-plugin/releases)
- [Changelog](https://github.com/webpack/minimizer-webpack-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/webpack/minimizer-webpack-plugin/compare/v5.3.16...v5.6.1)
---
updated-dependencies:
- dependency-name: copy-webpack-plugin
dependency-version: 14.0.0
dependency-type: direct:production
- dependency-name: serialize-javascript
dependency-version: 7.0.6
dependency-type: indirect
- dependency-name: terser-webpack-plugin
dependency-version: 5.6.1
dependency-type: indirect
- dependency-name: terser-webpack-plugin
dependency-version: 5.6.1
dependency-type: direct:production
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 284 ++++++++++++++++++++++------------------------
package.json | 4 +-
2 files changed, 137 insertions(+), 151 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 01ec4ba8f..67e5ff124 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35,7 +35,7 @@
"canvas": "3.2.1",
"clean-webpack-plugin": "4.0.0",
"clipboard-copy": "4.0.1",
- "copy-webpack-plugin": "11.0.0",
+ "copy-webpack-plugin": "14.0.0",
"crossroads": "0.12.2",
"css-element-queries": "1.1.1",
"d3": "7.9.0",
@@ -91,7 +91,7 @@
"shell-quote": "1.10.0",
"styled-components": "5.0.1",
"swr": "0.4.0",
- "terser-webpack-plugin": "5.3.9",
+ "terser-webpack-plugin": "5.6.1",
"tinykeys": "2.1.0",
"underscore": "1.13.8",
"utility-types": "3.10.0",
@@ -8969,6 +8969,7 @@
},
"node_modules/ajv-keywords": {
"version": "3.5.2",
+ "dev": true,
"license": "MIT",
"peerDependencies": {
"ajv": "^6.9.1"
@@ -10663,18 +10664,19 @@
}
},
"node_modules/copy-webpack-plugin": {
- "version": "11.0.0",
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-14.0.0.tgz",
+ "integrity": "sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==",
"license": "MIT",
"dependencies": {
- "fast-glob": "^3.2.11",
"glob-parent": "^6.0.1",
- "globby": "^13.1.1",
"normalize-path": "^3.0.0",
- "schema-utils": "^4.0.0",
- "serialize-javascript": "^6.0.0"
+ "schema-utils": "^4.2.0",
+ "serialize-javascript": "^7.0.3",
+ "tinyglobby": "^0.2.12"
},
"engines": {
- "node": ">= 14.15.0"
+ "node": ">= 20.9.0"
},
"funding": {
"type": "opencollective",
@@ -10708,47 +10710,6 @@
"ajv": "^8.8.2"
}
},
- "node_modules/copy-webpack-plugin/node_modules/fast-glob": {
- "version": "3.3.2",
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.stat": "^2.0.2",
- "@nodelib/fs.walk": "^1.2.3",
- "glob-parent": "^5.1.2",
- "merge2": "^1.3.0",
- "micromatch": "^4.0.4"
- },
- "engines": {
- "node": ">=8.6.0"
- }
- },
- "node_modules/copy-webpack-plugin/node_modules/fast-glob/node_modules/glob-parent": {
- "version": "5.1.2",
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/copy-webpack-plugin/node_modules/globby": {
- "version": "13.2.2",
- "license": "MIT",
- "dependencies": {
- "dir-glob": "^3.0.1",
- "fast-glob": "^3.3.0",
- "ignore": "^5.2.4",
- "merge2": "^1.4.1",
- "slash": "^4.0.0"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": {
"version": "1.0.0",
"license": "MIT"
@@ -10770,16 +10731,6 @@
"url": "https://opencollective.com/webpack"
}
},
- "node_modules/copy-webpack-plugin/node_modules/slash": {
- "version": "4.0.0",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/core-js-compat": {
"version": "3.38.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.0.tgz",
@@ -14085,7 +14036,6 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
- "dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@@ -22626,13 +22576,6 @@
"node": ">=0.12"
}
},
- "node_modules/randombytes": {
- "version": "2.1.0",
- "license": "MIT",
- "dependencies": {
- "safe-buffer": "^5.1.0"
- }
- },
"node_modules/range-parser": {
"version": "1.2.1",
"dev": true,
@@ -23946,11 +23889,12 @@
}
},
"node_modules/serialize-javascript": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
- "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
- "dependencies": {
- "randombytes": "^2.1.0"
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.0.tgz",
+ "integrity": "sha512-RNEqWOyhhUQYN9V1GfHwu9AR/g+NTciH6Z5u3/no6X3/w+04J2lVDL+svFQVXgXrEGBMG2puMVN3gq2SNGuTGw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=20.0.0"
}
},
"node_modules/serve-index": {
@@ -24806,14 +24750,15 @@
}
},
"node_modules/terser-webpack-plugin": {
- "version": "5.3.9",
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz",
+ "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==",
"license": "MIT",
"dependencies": {
- "@jridgewell/trace-mapping": "^0.3.17",
+ "@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
- "schema-utils": "^3.1.1",
- "serialize-javascript": "^6.0.1",
- "terser": "^5.16.8"
+ "schema-utils": "^4.3.0",
+ "terser": "^5.31.1"
},
"engines": {
"node": ">= 10.13.0"
@@ -24826,17 +24771,72 @@
"webpack": "^5.1.0"
},
"peerDependenciesMeta": {
+ "@minify-html/node": {
+ "optional": true
+ },
"@swc/core": {
"optional": true
},
+ "@swc/css": {
+ "optional": true
+ },
+ "@swc/html": {
+ "optional": true
+ },
+ "clean-css": {
+ "optional": true
+ },
+ "cssnano": {
+ "optional": true
+ },
+ "csso": {
+ "optional": true
+ },
"esbuild": {
"optional": true
},
+ "html-minifier-terser": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
"uglify-js": {
"optional": true
}
}
},
+ "node_modules/terser-webpack-plugin/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3"
+ },
+ "peerDependencies": {
+ "ajv": "^8.8.2"
+ }
+ },
"node_modules/terser-webpack-plugin/node_modules/has-flag": {
"version": "4.0.0",
"license": "MIT",
@@ -24856,13 +24856,22 @@
"node": ">= 10.13.0"
}
},
+ "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
"node_modules/terser-webpack-plugin/node_modules/schema-utils": {
- "version": "3.3.0",
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
+ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
"license": "MIT",
"dependencies": {
- "@types/json-schema": "^7.0.8",
- "ajv": "^6.12.5",
- "ajv-keywords": "^3.5.2"
+ "@types/json-schema": "^7.0.9",
+ "ajv": "^8.9.0",
+ "ajv-formats": "^2.1.1",
+ "ajv-keywords": "^5.1.0"
},
"engines": {
"node": ">= 10.13.0"
@@ -24906,6 +24915,51 @@
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
"dev": true
},
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/tinykeys": {
"version": "2.1.0",
"license": "MIT"
@@ -26866,27 +26920,6 @@
"node": ">=4.0"
}
},
- "node_modules/webpack/node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/webpack/node_modules/jest-worker": {
- "version": "27.5.1",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
- "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
- "dependencies": {
- "@types/node": "*",
- "merge-stream": "^2.0.0",
- "supports-color": "^8.0.0"
- },
- "engines": {
- "node": ">= 10.13.0"
- }
- },
"node_modules/webpack/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -26910,53 +26943,6 @@
"url": "https://opencollective.com/webpack"
}
},
- "node_modules/webpack/node_modules/supports-color": {
- "version": "8.1.1",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
- },
- "node_modules/webpack/node_modules/terser-webpack-plugin": {
- "version": "5.3.16",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz",
- "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
- "dependencies": {
- "@jridgewell/trace-mapping": "^0.3.25",
- "jest-worker": "^27.4.5",
- "schema-utils": "^4.3.0",
- "serialize-javascript": "^6.0.2",
- "terser": "^5.31.1"
- },
- "engines": {
- "node": ">= 10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- },
- "peerDependencies": {
- "webpack": "^5.1.0"
- },
- "peerDependenciesMeta": {
- "@swc/core": {
- "optional": true
- },
- "esbuild": {
- "optional": true
- },
- "uglify-js": {
- "optional": true
- }
- }
- },
"node_modules/websocket-driver": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz",
diff --git a/package.json b/package.json
index 201da1576..832342c01 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
"canvas": "3.2.1",
"clean-webpack-plugin": "4.0.0",
"clipboard-copy": "4.0.1",
- "copy-webpack-plugin": "11.0.0",
+ "copy-webpack-plugin": "14.0.0",
"crossroads": "0.12.2",
"css-element-queries": "1.1.1",
"d3": "7.9.0",
@@ -89,7 +89,7 @@
"shell-quote": "1.10.0",
"styled-components": "5.0.1",
"swr": "0.4.0",
- "terser-webpack-plugin": "5.3.9",
+ "terser-webpack-plugin": "5.6.1",
"tinykeys": "2.1.0",
"underscore": "1.13.8",
"utility-types": "3.10.0",
From 49a2ad491aa0c4aadb8a482bae3dd73a5aa1e758 Mon Sep 17 00:00:00 2001
From: jawelton74 <103591340+jawelton74@users.noreply.github.com>
Date: Mon, 31 Aug 2026 06:30:06 -0700
Subject: [PATCH 05/16] Fix js-yaml Dependabot alert (#2571)
Update ESLint's transitive js-yaml copies to a patched version that addresses CVE-2026-59869.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e102d1bf-4540-4ab5-9dd2-f82fe0267005
---
package-lock.json | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 67e5ff124..f6ab234a8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2697,9 +2697,9 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/js-yaml": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
- "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
+ "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
"dependencies": {
"argparse": "^2.0.1"
},
@@ -13020,9 +13020,9 @@
}
},
"node_modules/eslint/node_modules/js-yaml": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
- "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
+ "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
"dependencies": {
"argparse": "^2.0.1"
},
From a825a7ddc53179e931168852c13e6ebbd2bbefe7 Mon Sep 17 00:00:00 2001
From: asier-isayas
Date: Mon, 31 Aug 2026 10:35:54 -0400
Subject: [PATCH 06/16] Support Sovereign/PPE endpoint zones for connection
string login + E2E test for SQL account with public network access disabled
(#2568)
* Support sovereign/PPE endpoint zones for connection string login
Move the hardcoded account endpoint suffixes out of Constants and into ConfigContext so connection string login works in sovereign clouds and PPE, and widen ConnectionStringParser to accept every configured zone.
Surface Portal Backend rejections in the hosted connect form: read the body off the Response that fetchEncryptedToken throws, and offer a firewall help link on a 403.
Add an E2E test covering SQL connection string login against an account with public network access disabled.
* Harden connection string DNS zone matching
Detect PPE accounts from Mongo and Cassandra connection strings, build their document endpoint from the matched zone, match the PPE suffix on a label boundary, and escape every regex metacharacter in config-supplied zones. Drop the sqlx.cosmosdb.azure.com zone, which is not a real SQL zone.
* Mock web-vitals globally in test setup
ScenarioMonitor subscribes to web-vitals when it is imported, so the onTTFB timer can throw during any suite that outlives it. Mocking it in setupTests.ts keeps that out of individual test files.
* Improve connection string login feedback
* Preserve connection restriction check order
* Flatten connection login error handling
---------
Co-authored-by: Asier Isayas
---
src/Common/Constants.ts | 11 -
src/ConfigContext.ts | 33 ++
src/HostedExplorer.tsx | 5 +-
.../Components/ConnectExplorer.test.tsx | 172 ++++++++++
.../Hosted/Components/ConnectExplorer.tsx | 98 ++++--
src/Platform/Hosted/ConnectScreen.less | 44 +--
.../Helpers/ConnectionStringParser.test.ts | 318 +++++++++++++++---
.../Hosted/Helpers/ConnectionStringParser.ts | 98 ++++--
src/setupTests.ts | 3 +
test/fx.ts | 4 +
test/sql/connectionStringLogin.spec.ts | 28 ++
11 files changed, 671 insertions(+), 143 deletions(-)
diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts
index c4d0e1cd5..5f52c1d05 100644
--- a/src/Common/Constants.ts
+++ b/src/Common/Constants.ts
@@ -1,14 +1,3 @@
-export class EndpointsRegex {
- public static readonly cassandra = [
- "AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
- "HostName=(.*).cassandra.cosmos.azure.com",
- ];
- public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
- public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
- public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
- public static readonly table = "TableEndpoint=https://(.*).table.cosmos(?:db)?.azure.com";
-}
-
export class ApiEndpoints {
public static runtimeProxy: string = "/api/RuntimeProxy";
public static guestRuntimeProxy: string = "/api/guest/RuntimeProxy";
diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts
index 76574a117..a14deb248 100644
--- a/src/ConfigContext.ts
+++ b/src/ConfigContext.ts
@@ -30,6 +30,13 @@ export interface ConfigContext {
allowedCassandraProxyEndpoints: ReadonlyArray;
allowedMongoProxyEndpoints: ReadonlyArray;
allowedParentFrameOrigins: ReadonlyArray;
+ SQL_DNS_ZONES: ReadonlyArray;
+ MONGO_DNS_ZONES: ReadonlyArray;
+ MONGO_COMPUTE_DNS_ZONES: ReadonlyArray;
+ CASSANDRA_DNS_ZONES: ReadonlyArray;
+ TABLE_DNS_ZONES: ReadonlyArray;
+ DOCUMENT_ENDPOINT_ZONES: ReadonlyArray;
+ GREMLIN_ENDPOINT_ZONES: ReadonlyArray;
gitSha?: string;
proxyPath?: string;
AAD_ENDPOINT: string;
@@ -81,6 +88,32 @@ let configContext: Readonly = {
`^https:\\/\\/explorer\\.cosmos\\.sovcloud-api\\.sg$`,
`^https:\\/\\/portal\\.sovcloud-azure\\.sg$`,
], // Webpack injects this at build time
+ SQL_DNS_ZONES: [
+ "documents.azure.com",
+ "sql.cosmosdb.azure.com",
+ "sql.cosmos.azure.com",
+ "sqlx.cosmos.azure.com",
+ "documents-staging.windows-ppe.net",
+ "sql.cosmosdb.windows-ppe.net",
+ "sql.cosmos.windows-ppe.net",
+ "sqlx.cosmos.windows-ppe.net",
+ ],
+ MONGO_DNS_ZONES: ["documents.azure.com", "documents-staging.windows-ppe.net"],
+ MONGO_COMPUTE_DNS_ZONES: ["mongo.cosmos.azure.com", "mongo.cosmos.windows-ppe.net"],
+ CASSANDRA_DNS_ZONES: [
+ "cassandra.cosmosdb.azure.com",
+ "cassandra.cosmos.azure.com",
+ "cassandra.cosmosdb.windows-ppe.net",
+ "cassandra.cosmos.windows-ppe.net",
+ ],
+ TABLE_DNS_ZONES: [
+ "table.cosmosdb.azure.com",
+ "table.cosmos.azure.com",
+ "table.cosmosdb.windows-ppe.net",
+ "table.cosmos.windows-ppe.net",
+ ],
+ DOCUMENT_ENDPOINT_ZONES: ["documents.azure.com", "documents-staging.windows-ppe.net"],
+ GREMLIN_ENDPOINT_ZONES: ["gremlin.cosmos.azure.com", "gremlin.cosmos.windows-ppe.net"],
gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/",
AAD_ENDPOINT: "https://login.microsoftonline.com/",
diff --git a/src/HostedExplorer.tsx b/src/HostedExplorer.tsx
index ca28376d6..642141002 100644
--- a/src/HostedExplorer.tsx
+++ b/src/HostedExplorer.tsx
@@ -1,6 +1,5 @@
import { initializeIcons } from "@fluentui/react";
import { useBoolean } from "@fluentui/react-hooks";
-import { getErrorMessage } from "Common/ErrorHandlingUtils";
import { AadAuthorizationFailure } from "Platform/Hosted/Components/AadAuthorizationFailure";
import * as React from "react";
import { render } from "react-dom";
@@ -83,9 +82,9 @@ const App: React.FunctionComponent = () => {
setEncryptedToken(token);
setAuthType(AuthType.ConnectionString);
})
- .catch((error) => {
+ .catch(async (error) => {
logError(
- `Failed to connect with connection string: ${getErrorMessage(error)}`,
+ `Failed to connect with connection string: ${await (error as Response).text()}`,
"HostedExplorer/connectWithConnectionString",
);
});
diff --git a/src/Platform/Hosted/Components/ConnectExplorer.test.tsx b/src/Platform/Hosted/Components/ConnectExplorer.test.tsx
index a98a89ccc..251502f27 100644
--- a/src/Platform/Hosted/Components/ConnectExplorer.test.tsx
+++ b/src/Platform/Hosted/Components/ConnectExplorer.test.tsx
@@ -1,11 +1,27 @@
jest.mock("../../../hooks/useDirectories");
+jest.mock("../../../Common/PortalBackendClient");
import "@testing-library/jest-dom";
import { fireEvent, render, screen } from "@testing-library/react";
import { extractFeatures } from "Platform/Hosted/extractFeatures";
import { updateUserContext, userContext } from "UserContext";
import React from "react";
+import { fetchEncryptedToken, isAccountRestrictedForConnectionStringLogin } from "../../../Common/PortalBackendClient";
import { ConnectExplorer } from "./ConnectExplorer";
+const mockFetchEncryptedToken = fetchEncryptedToken as jest.MockedFunction;
+const mockIsAccountRestricted = isAccountRestrictedForConnectionStringLogin as jest.MockedFunction<
+ typeof isAccountRestrictedForConnectionStringLogin
+>;
+
+// fetchEncryptedToken rejects with the raw Response.
+const rejectWithResponse = (status: number, body: string) =>
+ mockFetchEncryptedToken.mockRejectedValue({ status, text: async () => body } as Response);
+
+beforeEach(() => {
+ jest.resetAllMocks();
+ mockIsAccountRestricted.mockResolvedValue(false);
+});
+
it("shows the connect form", () => {
const connectionString = "fakeConnectionString";
const login = jest.fn();
@@ -63,3 +79,159 @@ it("hides the connection string link when feature.disableConnectionStringLogin i
updateUserContext({ features: oldFeatures });
});
+
+it("rejects an unrecognized connection string before token exchange", async () => {
+ render(
+ ,
+ );
+ fireEvent.click(screen.getByText("Connect to your account with connection string"));
+ fireEvent.click(screen.getByDisplayValue("Connect"));
+
+ expect(
+ await screen.findByText(
+ "We couldn't recognize this connection string. Verify that it is a valid Azure Cosmos DB connection string and try again.",
+ ),
+ ).toBeInTheDocument();
+ expect(mockIsAccountRestricted).toHaveBeenCalledWith("not-a-valid-connection-string");
+ expect(mockFetchEncryptedToken).not.toHaveBeenCalled();
+});
+
+it("shows that a connection is in progress", async () => {
+ let finishRestrictionCheck: (restricted: boolean) => void = () => undefined;
+ mockIsAccountRestricted.mockImplementation(
+ () =>
+ new Promise((resolve) => {
+ finishRestrictionCheck = resolve;
+ }),
+ );
+
+ render(
+ ,
+ );
+ fireEvent.click(screen.getByText("Connect to your account with connection string"));
+ fireEvent.click(screen.getByDisplayValue("Connect"));
+
+ const connectButton = screen.getByDisplayValue("Connecting...");
+ expect(connectButton).toBeDisabled();
+ expect(connectButton.closest("form")).toHaveAttribute("aria-busy", "true");
+
+ finishRestrictionCheck(false);
+ expect(await screen.findByDisplayValue("Connect")).toBeEnabled();
+});
+
+it("shows the error when the Portal Backend rejects the connection string", async () => {
+ // Mongo and Cassandra are the APIs that still exchange the connection string for an encrypted token.
+ const mongoConnectionString = "mongodb://test:key@test.documents.azure.com:10255";
+ rejectWithResponse(403, "Request originated from IP 1.2.3.4 through public internet.");
+
+ render(
+ ,
+ );
+ fireEvent.click(screen.getByText("Connect to your account with connection string"));
+ fireEvent.click(screen.getByDisplayValue("Connect"));
+
+ expect(
+ await screen.findByText(
+ "Couldn't authenticate with Cosmos DB: Request originated from IP 1.2.3.4 through public internet.",
+ ),
+ ).toBeInTheDocument();
+});
+
+it("shows a generic error when the Portal Backend fails without a message", async () => {
+ const mongoConnectionString = "mongodb://test:key@test.documents.azure.com:10255";
+ // A failure with an empty body leaves nothing worth rendering.
+ rejectWithResponse(500, "");
+
+ render(
+ ,
+ );
+ fireEvent.click(screen.getByText("Connect to your account with connection string"));
+ fireEvent.click(screen.getByDisplayValue("Connect"));
+
+ expect(
+ await screen.findByText("Failed to connect to the account. Please check the connection string and try again."),
+ ).toBeInTheDocument();
+});
+
+it("offers the firewall help link when the Portal Backend is blocked by the account firewall", async () => {
+ const mongoConnectionString = "mongodb://test:key@test.documents.azure.com:10255";
+ rejectWithResponse(403, "Request originated from IP 1.2.3.4 through public internet.");
+
+ render(
+ ,
+ );
+ fireEvent.click(screen.getByText("Connect to your account with connection string"));
+ fireEvent.click(screen.getByDisplayValue("Connect"));
+
+ expect(await screen.findByText("Allow access from Azure Portal")).toBeInTheDocument();
+});
+
+it("does not offer the firewall help link for a failure the firewall did not cause", async () => {
+ const mongoConnectionString = "mongodb://test:key@test.documents.azure.com:10255";
+ rejectWithResponse(401, "The connection string is invalid.");
+
+ render(
+ ,
+ );
+ fireEvent.click(screen.getByText("Connect to your account with connection string"));
+ fireEvent.click(screen.getByDisplayValue("Connect"));
+
+ expect(
+ await screen.findByText("Couldn't authenticate with Cosmos DB: The connection string is invalid."),
+ ).toBeInTheDocument();
+ expect(screen.queryByText("Allow access from Azure Portal")).toBeNull();
+});
diff --git a/src/Platform/Hosted/Components/ConnectExplorer.tsx b/src/Platform/Hosted/Components/ConnectExplorer.tsx
index 99ce638cd..8158b4baf 100644
--- a/src/Platform/Hosted/Components/ConnectExplorer.tsx
+++ b/src/Platform/Hosted/Components/ConnectExplorer.tsx
@@ -1,10 +1,11 @@
+import { FluentProvider, Link, MessageBar, MessageBarBody, webLightTheme } from "@fluentui/react-components";
import { useBoolean } from "@fluentui/react-hooks";
import { getErrorMessage } from "Common/ErrorHandlingUtils";
import { userContext } from "UserContext";
import * as React from "react";
import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg";
-import ErrorImage from "../../../../images/error.svg";
import { AuthType } from "../../../AuthType";
+import { HttpStatusCodes } from "../../../Common/Constants";
import { fetchEncryptedToken, isAccountRestrictedForConnectionStringLogin } from "../../../Common/PortalBackendClient";
import { AccessInputMetadata } from "../../../Contracts/DataModels";
import { parseConnectionString } from "../Helpers/ConnectionStringParser";
@@ -30,6 +31,8 @@ export const ConnectExplorer: React.FunctionComponent = ({
}: Props) => {
const [isFormVisible, { setTrue: showForm }] = useBoolean(false);
const [errorMessage, setErrorMessage] = React.useState("");
+ const [isBlockedByFirewall, setIsBlockedByFirewall] = React.useState(false);
+ const [isConnecting, setIsConnecting] = React.useState(false);
const enableConnectionStringLogin = !userContext.features.disableConnectionStringLogin;
return (
@@ -43,39 +46,70 @@ export const ConnectExplorer: React.FunctionComponent = ({
{isFormVisible && enableConnectionStringLogin ? (
+ {errorMessage.length > 0 && (
+
+
+
+ {errorMessage}
+ {isBlockedByFirewall && (
+
+ Allow access from Azure Portal
+
+ )}
+
+
+
+ )}
-
+
Sign In with Azure Account
diff --git a/src/Platform/Hosted/ConnectScreen.less b/src/Platform/Hosted/ConnectScreen.less
index d4123f23f..2be2cf128 100644
--- a/src/Platform/Hosted/ConnectScreen.less
+++ b/src/Platform/Hosted/ConnectScreen.less
@@ -55,41 +55,19 @@
.connectExplorerContainer .connectExplorer .connectExplorerContent .inputToken::placeholder {
font-style: italic;
}
-.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip {
- position: relative;
- display: inline-block;
- padding-left: 4px;
- vertical-align: top;
+.connectExplorerContainer .connectExplorer .connectErrorMessageBar {
+ /* Matches the width of the connection string input so the form stays balanced. */
+ width: 308px;
+ margin: 0px auto 8px auto;
+ text-align: left;
}
-.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip:hover .errorDetails {
- visibility: visible;
+.connectExplorerContainer .connectExplorer .connectErrorMessageBar .errorDetails {
+ /* Service messages are diagnostics containing unbroken URIs and ids. */
+ overflow-wrap: anywhere;
}
-.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip .errorDetails {
- bottom: 24px;
- width: 165px;
- visibility: hidden;
- background-color: #393939;
- color: #ffffff;
- position: absolute;
- z-index: 1;
- left: -10px;
- padding: 6px;
-}
-.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip .errorDetails:after {
- border-width: 10px 10px 0px 10px;
- bottom: -8px;
- content: "";
- position: absolute;
- right: 100%;
- border-style: solid;
- left: 12px;
- width: 0;
- height: 0;
- border-color: #3b3b3b transparent;
-}
-.connectExplorerContainer .connectExplorer .connectExplorerContent .errorDetailsInfoTooltip .errorImg {
- height: 14px;
- width: 14px;
+.connectExplorerContainer .connectExplorer .connectErrorMessageBar .errorHelpLink {
+ display: block;
+ margin-top: 8px;
}
.filterbtnstyle {
diff --git a/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts b/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts
index 823ebb115..f1cb417ba 100644
--- a/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts
+++ b/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts
@@ -1,19 +1,75 @@
+import { configContext, updateConfigContext } from "../../../ConfigContext";
import * as DataModels from "../../../Contracts/DataModels";
-import { parseConnectionString } from "./ConnectionStringParser";
+import {
+ buildEndpointsRegex,
+ dnsZoneAlternation,
+ parseConnectionString,
+ selectEndpointZone,
+} from "./ConnectionStringParser";
describe("ConnectionStringParser", () => {
const mockAccountName = "Test";
const mockMasterKey = "some-key";
- it("should parse a valid sql account connection string", () => {
- const metadata = parseConnectionString(
- `AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};`,
- );
+ // The shape of each api's connection string, parameterized by the dns zone the account sits in. What
+ // these tests are about is the zones, so keeping the shapes here stops them being restated once per zone.
+ const buildConnectionString = {
+ sql: (zone: string) => `AccountEndpoint=https://${mockAccountName}.${zone}:443/;AccountKey=${mockMasterKey};`,
+ mongo: (zone: string) => `mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.${zone}:10255`,
+ // A cassandra connection string can name the account host under either AccountEndpoint or HostName.
+ cassandra: (zone: string, hostKey = "AccountEndpoint") =>
+ `${hostKey}=${mockAccountName}.${zone};AccountKey=${mockMasterKey};`,
+ table: (zone: string) =>
+ `DefaultEndpointsProtocol=https;AccountName=${mockAccountName};AccountKey=${mockMasterKey};TableEndpoint=https://${mockAccountName}.${zone}:443/;`,
+ graph: (zone: string) =>
+ `AccountEndpoint=https://${mockAccountName}.${zone}:443/;AccountKey=${mockMasterKey};ApiKind=Gremlin;`,
+ };
- expect(metadata.accountName).toBe(mockAccountName);
- expect(metadata.apiKind).toBe(DataModels.ApiKind.SQL);
- expect(metadata.documentEndpoint).toBe(`https://${mockAccountName}.documents.azure.com:443/`);
- expect(metadata.apiEndpoint).toBeUndefined();
+ const connectionStringsByApiKind: Record = {
+ [DataModels.ApiKind.SQL]: buildConnectionString.sql("documents.azure.com"),
+ [DataModels.ApiKind.MongoDB]: buildConnectionString.mongo("documents.azure.com"),
+ [DataModels.ApiKind.MongoDBCompute]: buildConnectionString.mongo("mongo.cosmos.azure.com"),
+ [DataModels.ApiKind.Cassandra]: buildConnectionString.cassandra("cassandra.cosmosdb.azure.com"),
+ [DataModels.ApiKind.Table]: buildConnectionString.table("table.cosmosdb.azure.com"),
+ [DataModels.ApiKind.Graph]: buildConnectionString.graph("documents.azure.com"),
+ };
+
+ it("should parse a connection string for every api kind", () => {
+ Object.entries(connectionStringsByApiKind).forEach(([apiKind, connectionString]) => {
+ const metadata = parseConnectionString(connectionString);
+
+ expect(metadata.accountName).toBe(mockAccountName);
+ expect(metadata.apiKind).toBe(Number(apiKind));
+ });
+ });
+
+ // The parameterized tests below iterate the zone lists, so removing a zone would silently shrink the
+ // suite rather than fail it. Pin the expected contents so that stays visible in review.
+ it("should support the expected dns zones", () => {
+ expect(configContext.SQL_DNS_ZONES).toEqual([
+ "documents.azure.com",
+ "sql.cosmosdb.azure.com",
+ "sql.cosmos.azure.com",
+ "sqlx.cosmos.azure.com",
+ "documents-staging.windows-ppe.net",
+ "sql.cosmosdb.windows-ppe.net",
+ "sql.cosmos.windows-ppe.net",
+ "sqlx.cosmos.windows-ppe.net",
+ ]);
+ expect(configContext.MONGO_DNS_ZONES).toEqual(["documents.azure.com", "documents-staging.windows-ppe.net"]);
+ expect(configContext.MONGO_COMPUTE_DNS_ZONES).toEqual(["mongo.cosmos.azure.com", "mongo.cosmos.windows-ppe.net"]);
+ expect(configContext.CASSANDRA_DNS_ZONES).toEqual([
+ "cassandra.cosmosdb.azure.com",
+ "cassandra.cosmos.azure.com",
+ "cassandra.cosmosdb.windows-ppe.net",
+ "cassandra.cosmos.windows-ppe.net",
+ ]);
+ expect(configContext.TABLE_DNS_ZONES).toEqual([
+ "table.cosmosdb.azure.com",
+ "table.cosmos.azure.com",
+ "table.cosmosdb.windows-ppe.net",
+ "table.cosmos.windows-ppe.net",
+ ]);
});
it("should keep the document endpoint given by the connection string", () => {
@@ -26,28 +82,99 @@ describe("ConnectionStringParser", () => {
expect(metadata.documentEndpoint).toBe(`https://${mockAccountName}.documents.azure.com/`);
});
- it("should parse a valid mongo account connection string", () => {
- const metadata = parseConnectionString(
- `mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.documents.azure.com:10255`,
- );
+ it.each(configContext.SQL_DNS_ZONES)(
+ "should parse a sql account connection string using the %s zone",
+ (dnsZone: string) => {
+ const metadata = parseConnectionString(buildConnectionString.sql(dnsZone));
- expect(metadata.accountName).toBe(mockAccountName);
- expect(metadata.apiKind).toBe(DataModels.ApiKind.MongoDB);
- });
+ expect(metadata.accountName).toBe(mockAccountName);
+ expect(metadata.apiKind).toBe(DataModels.ApiKind.SQL);
+ expect(metadata.documentEndpoint).toBe(`https://${mockAccountName}.${dnsZone}:443/`);
+ expect(metadata.apiEndpoint).toBeUndefined();
+ },
+ );
- it("should parse a valid compute mongo account connection string", () => {
- const metadata = parseConnectionString(
- `mongodb://${mockAccountName}:${mockMasterKey}@${mockAccountName}.mongo.cosmos.azure.com:10255`,
- );
+ it.each(configContext.MONGO_DNS_ZONES)(
+ "should parse a mongo account connection string using the %s zone",
+ (dnsZone: string) => {
+ const metadata = parseConnectionString(buildConnectionString.mongo(dnsZone));
- expect(metadata.accountName).toBe(mockAccountName);
- expect(metadata.apiKind).toBe(DataModels.ApiKind.MongoDBCompute);
- });
+ expect(metadata.accountName).toBe(mockAccountName);
+ expect(metadata.apiKind).toBe(DataModels.ApiKind.MongoDB);
+ },
+ );
+
+ it.each(configContext.MONGO_COMPUTE_DNS_ZONES)(
+ "should parse a compute mongo account connection string using the %s zone",
+ (dnsZone: string) => {
+ const metadata = parseConnectionString(buildConnectionString.mongo(dnsZone));
+
+ expect(metadata.accountName).toBe(mockAccountName);
+ expect(metadata.apiKind).toBe(DataModels.ApiKind.MongoDBCompute);
+ },
+ );
+
+ it.each(
+ ["AccountEndpoint", "HostName"].flatMap((hostKey) =>
+ configContext.CASSANDRA_DNS_ZONES.map((dnsZone) => [hostKey, dnsZone]),
+ ),
+ )(
+ "should parse a cassandra account connection string using %s and the %s zone",
+ (hostKey: string, dnsZone: string) => {
+ const metadata = parseConnectionString(buildConnectionString.cassandra(dnsZone, hostKey));
+
+ expect(metadata.accountName).toBe(mockAccountName);
+ expect(metadata.apiKind).toBe(DataModels.ApiKind.Cassandra);
+ },
+ );
+
+ it.each(configContext.TABLE_DNS_ZONES)(
+ "should parse a table account connection string using the %s zone",
+ (dnsZone: string) => {
+ const metadata = parseConnectionString(buildConnectionString.table(dnsZone));
+
+ expect(metadata.accountName).toBe(mockAccountName);
+ expect(metadata.apiKind).toBe(DataModels.ApiKind.Table);
+ expect(metadata.apiEndpoint).toBeUndefined();
+ },
+ );
+
+ // A Mongo, Cassandra or Table connection string names the account in its own api's dns zone rather than
+ // giving the document endpoint, so the document endpoint that data plane operations go through is built
+ // from the account name, under a zone of the same kind as the one that matched.
+ const publicDocumentEndpoint = `https://${mockAccountName}.documents.azure.com:443/`;
+ const ppeDocumentEndpoint = `https://${mockAccountName}.documents-staging.windows-ppe.net:443/`;
+ const expectedDocumentEndpointByZone: Record = {
+ "documents.azure.com": publicDocumentEndpoint,
+ "documents-staging.windows-ppe.net": ppeDocumentEndpoint,
+ "mongo.cosmos.azure.com": publicDocumentEndpoint,
+ "mongo.cosmos.windows-ppe.net": ppeDocumentEndpoint,
+ "cassandra.cosmosdb.azure.com": publicDocumentEndpoint,
+ "cassandra.cosmos.azure.com": publicDocumentEndpoint,
+ "cassandra.cosmosdb.windows-ppe.net": ppeDocumentEndpoint,
+ "cassandra.cosmos.windows-ppe.net": ppeDocumentEndpoint,
+ "table.cosmosdb.azure.com": publicDocumentEndpoint,
+ "table.cosmos.azure.com": publicDocumentEndpoint,
+ "table.cosmosdb.windows-ppe.net": ppeDocumentEndpoint,
+ "table.cosmos.windows-ppe.net": ppeDocumentEndpoint,
+ };
+
+ it.each([
+ ...configContext.MONGO_DNS_ZONES.map((dnsZone) => [dnsZone, buildConnectionString.mongo(dnsZone)]),
+ ...configContext.MONGO_COMPUTE_DNS_ZONES.map((dnsZone) => [dnsZone, buildConnectionString.mongo(dnsZone)]),
+ ...configContext.CASSANDRA_DNS_ZONES.map((dnsZone) => [dnsZone, buildConnectionString.cassandra(dnsZone)]),
+ ...configContext.TABLE_DNS_ZONES.map((dnsZone) => [dnsZone, buildConnectionString.table(dnsZone)]),
+ ])(
+ "should construct the document endpoint for an account in the %s zone",
+ (dnsZone: string, connectionString: string) => {
+ const metadata = parseConnectionString(connectionString);
+
+ expect(metadata.documentEndpoint).toBe(expectedDocumentEndpointByZone[dnsZone]);
+ },
+ );
it("should parse a valid graph account connection string", () => {
- const metadata = parseConnectionString(
- `AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockMasterKey};ApiKind=Gremlin;`,
- );
+ const metadata = parseConnectionString(buildConnectionString.graph("documents.azure.com"));
expect(metadata.accountName).toBe(mockAccountName);
expect(metadata.apiKind).toBe(DataModels.ApiKind.Graph);
@@ -55,36 +182,41 @@ describe("ConnectionStringParser", () => {
expect(metadata.apiEndpoint).toBe(`${mockAccountName}.gremlin.cosmos.azure.com:443`);
});
- it("should parse a valid table account connection string", () => {
- const metadata = parseConnectionString(
- `DefaultEndpointsProtocol=https;AccountName=${mockAccountName};AccountKey=${mockMasterKey};TableEndpoint=https://${mockAccountName}.table.cosmosdb.azure.com:443/;`,
- );
+ it("should construct a PPE gremlin endpoint for a PPE graph account", () => {
+ const metadata = parseConnectionString(buildConnectionString.graph("documents-staging.windows-ppe.net"));
expect(metadata.accountName).toBe(mockAccountName);
- expect(metadata.apiKind).toBe(DataModels.ApiKind.Table);
- // Table data operations go through the document endpoint, which is constructed from the account name.
- expect(metadata.documentEndpoint).toBe(`https://${mockAccountName}.documents.azure.com:443/`);
- expect(metadata.apiEndpoint).toBeUndefined();
+ expect(metadata.apiKind).toBe(DataModels.ApiKind.Graph);
+ expect(metadata.documentEndpoint).toBe(`https://${mockAccountName}.documents-staging.windows-ppe.net:443/`);
+ // The constructed endpoint has to match the kind of zone the document endpoint we matched came from.
+ expect(metadata.apiEndpoint).toBe(`${mockAccountName}.gremlin.cosmos.windows-ppe.net:443`);
});
- it("should parse a valid table account connection string using the cosmos.azure.com zone", () => {
- const metadata = parseConnectionString(
- `DefaultEndpointsProtocol=https;AccountName=${mockAccountName};AccountKey=${mockMasterKey};TableEndpoint=https://${mockAccountName}.table.cosmos.azure.com:443/;`,
- );
+ it("should reject a connection string when no DNS zone matches the account", () => {
+ const originalZones = configContext.DOCUMENT_ENDPOINT_ZONES;
+ updateConfigContext({ DOCUMENT_ENDPOINT_ZONES: ["documents.azure.com"] });
- expect(metadata.accountName).toBe(mockAccountName);
- expect(metadata.apiKind).toBe(DataModels.ApiKind.Table);
- expect(metadata.documentEndpoint).toBe(`https://${mockAccountName}.documents.azure.com:443/`);
- expect(metadata.apiEndpoint).toBeUndefined();
+ try {
+ const metadata = parseConnectionString(buildConnectionString.table("table.cosmos.windows-ppe.net"));
+
+ // The account key travels to the constructed document endpoint, so a config carrying no PPE zone
+ // has to fail on a PPE account rather than fall back to a zone the account does not own.
+ expect(metadata).toBe(undefined);
+ } finally {
+ updateConfigContext({ DOCUMENT_ENDPOINT_ZONES: originalZones });
+ }
});
- it("should parse a valid cassandra account connection string", () => {
- const metadata = parseConnectionString(
- `AccountEndpoint=${mockAccountName}.cassandra.cosmosdb.azure.com;AccountKey=${mockMasterKey};`,
- );
-
- expect(metadata.accountName).toBe(mockAccountName);
- expect(metadata.apiKind).toBe(DataModels.ApiKind.Cassandra);
+ it.each([
+ buildConnectionString.sql("documents.azure.com.attacker.example"),
+ buildConnectionString.mongo("documents.azure.com.attacker.example"),
+ buildConnectionString.mongo("mongo.cosmos.azure.com.attacker.example"),
+ buildConnectionString.cassandra("cassandra.cosmosdb.azure.com.attacker.example"),
+ buildConnectionString.table("table.cosmosdb.azure.com.attacker.example"),
+ ])("should not accept a host that only begins with a known zone: %s", (connectionString: string) => {
+ // The zone list is what keeps the account key from being sent somewhere arbitrary, so a host that
+ // appends to an allowed zone must not pass as that zone.
+ expect(parseConnectionString(connectionString)).toBe(undefined);
});
it("should fail to parse an invalid connection string", () => {
@@ -98,4 +230,92 @@ describe("ConnectionStringParser", () => {
expect(metadata).toBe(undefined);
});
+
+ describe("dnsZoneAlternation", () => {
+ it("should escape the dots in a zone", () => {
+ expect(dnsZoneAlternation(["documents.azure.com"])).toBe("(documents\\.azure\\.com)(?=[:/\\s]|$)");
+ });
+
+ it("should join multiple zones into a single alternation", () => {
+ expect(dnsZoneAlternation(["a.example", "b.test"])).toBe("(a\\.example|b\\.test)(?=[:/\\s]|$)");
+ });
+
+ it("should not let the dots match arbitrary characters", () => {
+ // An unescaped dot would make the zone list match hosts that only resemble a real zone.
+ const regex = RegExp(dnsZoneAlternation(["documents.azure.com"]));
+
+ expect(regex.test("documents.azure.com")).toBe(true);
+ expect(regex.test("documentsXazure.com")).toBe(false);
+ });
+
+ it("should capture the zone that matched", () => {
+ const regex = RegExp(dnsZoneAlternation(["a.example", "b.test"]));
+
+ expect("account.b.test".match(regex)[1]).toBe("b.test");
+ });
+
+ it("should require the zone to run to the end of the host", () => {
+ const regex = RegExp(dnsZoneAlternation(["documents.azure.com"]));
+
+ expect(regex.test("account.documents.azure.com")).toBe(true);
+ expect(regex.test("account.documents.azure.com:443/")).toBe(true);
+ expect(regex.test("account.documents.azure.com/")).toBe(true);
+ // Without this the zone list stops being an allowlist, since anything can be appended to a zone.
+ expect(regex.test("account.documents.azure.com.attacker.example")).toBe(false);
+ });
+ });
+
+ describe("buildEndpointsRegex", () => {
+ it("should build a pattern for every api matched by dns zone", () => {
+ expect(Object.keys(buildEndpointsRegex())).toEqual(["sql", "mongo", "mongoCompute", "cassandra", "table"]);
+ });
+
+ it("should build a cassandra pattern for each supported key", () => {
+ const { cassandra } = buildEndpointsRegex();
+
+ expect(cassandra).toHaveLength(2);
+ expect(cassandra[0]).toContain("AccountEndpoint=");
+ expect(cassandra[1]).toContain("HostName=");
+ });
+
+ it("should build each pattern from its own zone list", () => {
+ // The patterns are near identical, so a zone list wired to the wrong api would be easy to miss in
+ // review and would let an account of one api be parsed as another.
+ const { sql, mongo, mongoCompute, cassandra, table } = buildEndpointsRegex();
+
+ expect(sql).toContain(dnsZoneAlternation(configContext.SQL_DNS_ZONES));
+ expect(mongo).toContain(dnsZoneAlternation(configContext.MONGO_DNS_ZONES));
+ expect(mongoCompute).toContain(dnsZoneAlternation(configContext.MONGO_COMPUTE_DNS_ZONES));
+ cassandra.forEach((pattern) => expect(pattern).toContain(dnsZoneAlternation(configContext.CASSANDRA_DNS_ZONES)));
+ expect(table).toContain(dnsZoneAlternation(configContext.TABLE_DNS_ZONES));
+ });
+ });
+
+ describe("selectEndpointZone", () => {
+ const nonPpeZone = "documents.azure.com";
+ const ppeZone = "documents-staging.windows-ppe.net";
+
+ it("should pick the ppe zone for a ppe account", () => {
+ expect(selectEndpointZone([nonPpeZone, ppeZone], true)).toBe(ppeZone);
+ });
+
+ it("should pick the non ppe zone for a non ppe account", () => {
+ expect(selectEndpointZone([nonPpeZone, ppeZone], false)).toBe(nonPpeZone);
+ });
+
+ it("should not depend on the order of the zones", () => {
+ expect(selectEndpointZone([ppeZone, nonPpeZone], true)).toBe(ppeZone);
+ expect(selectEndpointZone([ppeZone, nonPpeZone], false)).toBe(nonPpeZone);
+ });
+
+ it("should return undefined when no zone matches the kind of account", () => {
+ // Sovereign configs carry no ppe zone, and a ppe only config carries no non ppe zone.
+ expect(selectEndpointZone([nonPpeZone], true)).toBeUndefined();
+ expect(selectEndpointZone([ppeZone], false)).toBeUndefined();
+ });
+
+ it("should return undefined for an empty zone list", () => {
+ expect(selectEndpointZone([], false)).toBeUndefined();
+ });
+ });
});
diff --git a/src/Platform/Hosted/Helpers/ConnectionStringParser.ts b/src/Platform/Hosted/Helpers/ConnectionStringParser.ts
index 44be7e82a..9730052ea 100644
--- a/src/Platform/Hosted/Helpers/ConnectionStringParser.ts
+++ b/src/Platform/Hosted/Helpers/ConnectionStringParser.ts
@@ -1,44 +1,80 @@
-import * as Constants from "../../../Common/Constants";
+import { configContext } from "../../../ConfigContext";
import { AccessInputMetadata, ApiKind } from "../../../Contracts/DataModels";
-// Cosmos DB DNS zones used to construct endpoints client-side. These mirror what the Portal Backend's
-// accessinputmetadata API constructs from the account name when the connection string does not already
-// contain the endpoint.
-const DocumentEndpointZone = "documents.azure.com";
-const GremlinEndpointZone = "gremlin.cosmos.azure.com";
+const PpeDnsSuffix = "windows-ppe.net";
const DnsPort = "443";
+// Match on a label boundary so a zone like "notwindows-ppe.net" is not taken for a PPE zone.
+const isPpeZone = (zone: string): boolean => zone === PpeDnsSuffix || zone.endsWith(`.${PpeDnsSuffix}`);
+
+// Picks the DNS zone matching the kind of account the connection string came from, since a PPE
+// account's endpoints sit under PPE zones and every other account's do not.
+export const selectEndpointZone = (zones: ReadonlyArray, isPpeAccount: boolean): string | undefined =>
+ zones.find((zone) => isPpeZone(zone) === isPpeAccount);
+
+// Builds an alternation matching any of the given DNS zones, e.g. "(documents\.azure\.com|sql\.cosmos\.azure\.com)".
+// The group captures so callers can tell which zone matched, and with it whether the account is a PPE account.
+// The zone has to run to the end of the host, otherwise a host that merely starts with an allowed zone
+// would pass as that zone and the account key would travel to whatever was appended to it. Zones come
+// from config, so every regex metacharacter is escaped rather than just the dots.
+export const dnsZoneAlternation = (zones: ReadonlyArray): string =>
+ `(${zones.map((zone) => zone.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})(?=[:/\\s]|$)`;
+
+// The zone lists live in ConfigContext, which is populated asynchronously by initializeConfiguration,
+// so these are built per call rather than once at module load.
+export const buildEndpointsRegex = () => ({
+ sql: `AccountEndpoint=https://([^.]+)\\.${dnsZoneAlternation(configContext.SQL_DNS_ZONES)}`,
+ mongo: `mongodb://.*:(.*)@([^.]+)\\.${dnsZoneAlternation(configContext.MONGO_DNS_ZONES)}`,
+ mongoCompute: `mongodb://.*:(.*)@([^.]+)\\.${dnsZoneAlternation(configContext.MONGO_COMPUTE_DNS_ZONES)}`,
+ cassandra: ["AccountEndpoint", "HostName"].map(
+ (key) => `${key}=([^.]+)\\.${dnsZoneAlternation(configContext.CASSANDRA_DNS_ZONES)}`,
+ ),
+ table: `TableEndpoint=https://([^.]+)\\.${dnsZoneAlternation(configContext.TABLE_DNS_ZONES)}`,
+});
+
export function parseConnectionString(connectionString: string): AccessInputMetadata {
if (connectionString) {
try {
const accessInput = {} as AccessInputMetadata;
const connectionStringParts = connectionString.split(";");
+ const endpointsRegex = buildEndpointsRegex();
+ // Endpoints we build from the account name have to match the kind of zone the connection string
+ // actually matched, since PPE accounts and other accounts do not share zones.
+ let isPpeAccount = false;
connectionStringParts.forEach((connectionStringPart: string) => {
- if (RegExp(Constants.EndpointsRegex.sql).test(connectionStringPart)) {
- accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.sql)[1];
+ if (RegExp(endpointsRegex.sql).test(connectionStringPart)) {
+ const matches: string[] = connectionStringPart.match(endpointsRegex.sql);
+ accessInput.accountName = matches[1];
accessInput.apiKind = ApiKind.SQL;
// SQL and Gremlin connection strings carry the account's document endpoint, so take it as
// given instead of rebuilding it from the account name.
accessInput.documentEndpoint = connectionStringPart.substring(connectionStringPart.indexOf("=") + 1);
- } else if (RegExp(Constants.EndpointsRegex.mongo).test(connectionStringPart)) {
- const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongo);
+ isPpeAccount = isPpeZone(matches[2]);
+ } else if (RegExp(endpointsRegex.mongo).test(connectionStringPart)) {
+ const matches: string[] = connectionStringPart.match(endpointsRegex.mongo);
accessInput.accountName = matches && matches.length > 1 && matches[2];
accessInput.apiKind = ApiKind.MongoDB;
- } else if (RegExp(Constants.EndpointsRegex.mongoCompute).test(connectionStringPart)) {
- const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongoCompute);
+ isPpeAccount = isPpeZone(matches[3]);
+ } else if (RegExp(endpointsRegex.mongoCompute).test(connectionStringPart)) {
+ const matches: string[] = connectionStringPart.match(endpointsRegex.mongoCompute);
accessInput.accountName = matches && matches.length > 1 && matches[2];
accessInput.apiKind = ApiKind.MongoDBCompute;
- } else if (Constants.EndpointsRegex.cassandra.some((regex) => RegExp(regex).test(connectionStringPart))) {
- Constants.EndpointsRegex.cassandra.forEach((regex) => {
- if (RegExp(regex).test(connectionStringPart)) {
- accessInput.accountName = connectionStringPart.match(regex)[1];
+ isPpeAccount = isPpeZone(matches[3]);
+ } else if (endpointsRegex.cassandra.some((regex) => RegExp(regex).test(connectionStringPart))) {
+ endpointsRegex.cassandra.forEach((regex) => {
+ const matches: string[] = connectionStringPart.match(regex);
+ if (matches) {
+ accessInput.accountName = matches[1];
accessInput.apiKind = ApiKind.Cassandra;
+ isPpeAccount = isPpeZone(matches[2]);
}
});
- } else if (RegExp(Constants.EndpointsRegex.table).test(connectionStringPart)) {
- accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.table)[1];
+ } else if (RegExp(endpointsRegex.table).test(connectionStringPart)) {
+ const matches: string[] = connectionStringPart.match(endpointsRegex.table);
+ accessInput.accountName = matches[1];
accessInput.apiKind = ApiKind.Table;
+ isPpeAccount = isPpeZone(matches[2]);
} else if (connectionStringPart.indexOf("ApiKind=Gremlin") >= 0) {
accessInput.apiKind = ApiKind.Graph;
}
@@ -48,14 +84,28 @@ export function parseConnectionString(connectionString: string): AccessInputMeta
return undefined;
}
- // Table connection strings only carry the table endpoint, so the document endpoint that data plane
- // operations go through has to be derived from the account name. Gremlin accounts additionally
- // need the Gremlin endpoint, which is never part of the connection string.
+ // A Table, Mongo or Cassandra connection string names the account in its own api's dns zone rather
+ // than giving the document endpoint, so the document endpoint that data plane operations go through
+ // has to be built from the account name. SQL and Gremlin strings carry it and take it as given.
+ // Gremlin additionally needs the Gremlin endpoint, which is never part of the connection string.
if (accessInput.accountName) {
- if (accessInput.apiKind === ApiKind.Table) {
- accessInput.documentEndpoint = `https://${accessInput.accountName}.${DocumentEndpointZone}:${DnsPort}/`;
+ if (
+ accessInput.apiKind === ApiKind.Table ||
+ accessInput.apiKind === ApiKind.MongoDB ||
+ accessInput.apiKind === ApiKind.MongoDBCompute ||
+ accessInput.apiKind === ApiKind.Cassandra
+ ) {
+ const documentEndpointZone = selectEndpointZone(configContext.DOCUMENT_ENDPOINT_ZONES, isPpeAccount);
+ if (!documentEndpointZone) {
+ return undefined;
+ }
+ accessInput.documentEndpoint = `https://${accessInput.accountName}.${documentEndpointZone}:${DnsPort}/`;
} else if (accessInput.apiKind === ApiKind.Graph) {
- accessInput.apiEndpoint = `${accessInput.accountName}.${GremlinEndpointZone}:${DnsPort}`;
+ const gremlinEndpointZone = selectEndpointZone(configContext.GREMLIN_ENDPOINT_ZONES, isPpeAccount);
+ if (!gremlinEndpointZone) {
+ return undefined;
+ }
+ accessInput.apiEndpoint = `${accessInput.accountName}.${gremlinEndpointZone}:${DnsPort}`;
}
}
diff --git a/src/setupTests.ts b/src/setupTests.ts
index 720a385d8..9d2e6dfb3 100644
--- a/src/setupTests.ts
+++ b/src/setupTests.ts
@@ -6,6 +6,9 @@ import enableHooks from "jest-react-hooks-shallow";
import { TextDecoder, TextEncoder } from "util";
import i18n from "./i18n";
import enResources from "./Localization/en/Resources.json";
+
+jest.mock("web-vitals");
+
configure({ adapter: new Adapter() });
initializeIcons();
diff --git a/test/fx.ts b/test/fx.ts
index a3cad859a..f3fc2e679 100644
--- a/test/fx.ts
+++ b/test/fx.ts
@@ -49,6 +49,7 @@ export enum TestAccount {
SQLReadOnly = "SQLReadOnly",
SQLContainerCopyOnly = "SQLContainerCopyOnly",
SQLConnectionString = "SQLConnectionString",
+ SQLConnectionStringPublicNetworkAccessDisabled = "SQLConnectionStringPublicNetworkAccessDisabled",
TableConnectionString = "TableConnectionString",
GremlinConnectionString = "GremlinConnectionString",
}
@@ -83,6 +84,8 @@ export function getDefaultAccountName(accountType: TestAccount): string {
return `${accountNamePrefix}-de-test-sql-containercopy`;
case TestAccount.SQLConnectionString:
return `${accountNamePrefix}-de-test-sql-connstring-1`;
+ case TestAccount.SQLConnectionStringPublicNetworkAccessDisabled:
+ return `${accountNamePrefix}-de-test-sql-connstring-nopublic-1`;
case TestAccount.TableConnectionString:
return `${accountNamePrefix}-de-test-table-connstring-1`;
case TestAccount.GremlinConnectionString:
@@ -258,6 +261,7 @@ export async function getTestExplorerUrl(accountType: TestAccount, options?: Tes
break;
case TestAccount.SQLConnectionString:
+ case TestAccount.SQLConnectionStringPublicNetworkAccessDisabled:
case TestAccount.TableConnectionString:
case TestAccount.GremlinConnectionString:
// Connection string (account key) login navigates directly to hostedExplorer.html and doesn't
diff --git a/test/sql/connectionStringLogin.spec.ts b/test/sql/connectionStringLogin.spec.ts
index 8359e6dee..88293bd64 100644
--- a/test/sql/connectionStringLogin.spec.ts
+++ b/test/sql/connectionStringLogin.spec.ts
@@ -115,4 +115,32 @@ test.describe("SQL account using connection string login", () => {
await expect(page.locator("#connectExplorer")).toHaveCount(0);
await expect(page.locator(".errorDetails")).toHaveCount(0);
});
+
+ test("opens Data Explorer but loads no databases when the account rejects the client IP", async ({ page }) => {
+ // An account that refuses this client's IP.
+ const armClient = new CosmosDBManagementClient(getAzureCLICredentials(), subscriptionId);
+ const blockedAccountName = getAccountName(TestAccount.SQLConnectionStringPublicNetworkAccessDisabled);
+ const blockedAccount = await armClient.databaseAccounts.get(resourceGroupName, blockedAccountName);
+ const blockedKeys = await armClient.databaseAccounts.listKeys(resourceGroupName, blockedAccountName);
+
+ await loginWithConnectionString(
+ page,
+ `AccountEndpoint=${blockedAccount.documentEndpoint!};AccountKey=${blockedKeys.primaryMasterKey};`,
+ );
+
+ const explorer = await DataExplorer.waitForExplorer(page);
+
+ // Login is a client-side parse of the connection string, so nothing checks whether the account will
+ // accept requests from this IP before letting the user in.
+ await expect(page.locator("#connectExplorer")).toHaveCount(0);
+ await expect(page.locator(".errorDetails")).toHaveCount(0);
+
+ // The rejection surfaces once the tree tries to read the data plane, and only in the console.
+ const consoleMessages = await explorer.getNotificationConsoleMessages();
+ await expect(consoleMessages).toContainText("Error while refreshing databases", { timeout: ONE_MINUTE_MS });
+
+ // The tree is left with the static Home node and no database or container beneath it.
+ await expect(explorer.treeNode("Home").element).toBeAttached();
+ await expect(explorer.frame.locator("[data-test^='TreeNode:']")).toHaveCount(1);
+ });
});
From 325abbdff5a46290d7aa1a8bffc478eae705bf95 Mon Sep 17 00:00:00 2001
From: jawelton74 <103591340+jawelton74@users.noreply.github.com>
Date: Tue, 1 Sep 2026 08:40:33 -0700
Subject: [PATCH 07/16] Pin GitHub Actions to full-length commit SHAs (#2574)
Co-authored-by: Dan Fiedler
---
.github/dependabot.yml | 9 ++++++
.github/workflows/ci.yml | 54 +++++++++++++++++------------------
.github/workflows/cleanup.yml | 6 ++--
3 files changed, 39 insertions(+), 30 deletions(-)
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 60fa0ee1a..5103aa0e3 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -7,3 +7,12 @@ updates:
directory: "/"
schedule:
interval: "daily"
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ groups:
+ github-actions:
+ patterns: ["*"]
+ schedule:
+ interval: "weekly"
+ cooldown:
+ default-days: 7
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 274d5b862..8dc333990 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,9 +19,9 @@ jobs:
name: "Log Code Metrics"
if: github.ref == 'refs/heads/master'
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Use Node.js 22.x
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.x
- run: npm ci
@@ -32,9 +32,9 @@ jobs:
runs-on: ubuntu-latest
name: "Compile TypeScript"
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Use Node.js 22.x
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.x
- run: npm ci
@@ -44,9 +44,9 @@ jobs:
runs-on: ubuntu-latest
name: "Check Format"
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Use Node.js 22.x
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.x
- run: npm ci
@@ -55,9 +55,9 @@ jobs:
runs-on: ubuntu-latest
name: "Lint"
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Use Node.js 22.x
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.x
- run: npm ci
@@ -66,9 +66,9 @@ jobs:
runs-on: ubuntu-latest
name: "Unit Tests"
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Use Node.js 22.x
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.x
- run: npm ci
@@ -77,15 +77,15 @@ jobs:
runs-on: ubuntu-latest
name: "Build"
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Use Node.js 22.x
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.x
- run: npm ci
- run: npm run build:contracts
- name: Restore Build Cache
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .cache
key: ${{ runner.os }}-build-cache
@@ -94,12 +94,12 @@ jobs:
NODE_OPTIONS: "--max-old-space-size=4096"
- run: cp -r ./Contracts ./dist/contracts
- run: cp -r ./configs ./dist/configs
- - uses: actions/upload-artifact@v4
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: dist
path: dist/
- name: "Az CLI login"
- uses: azure/login@v1
+ uses: azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
with:
client-id: ${{ secrets.E2ETESTS_CLIENT_ID }}
tenant-id: ${{ secrets.E2ETESTS_TENANT_ID }}
@@ -118,7 +118,7 @@ jobs:
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
steps:
- name: Download Dist Folder
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: dist
- run: cp ./configs/prod.json config.json
@@ -126,7 +126,7 @@ jobs:
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}"
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE"
- run: dotnet nuget remove source "ADO"
- - uses: actions/upload-artifact@v4
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
name: Upload package to Artifacts
with:
name: prod-package
@@ -142,7 +142,7 @@ jobs:
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
steps:
- name: Download Dist Folder
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: dist
- run: cp ./configs/mpac.json config.json
@@ -151,7 +151,7 @@ jobs:
- run: dotnet pack DataExplorer.proj /p:PackageVersion="2.0.0-github-${GITHUB_SHA}"
- run: dotnet nuget push "bin/Release/*.nupkg" --skip-duplicate --api-key Az --source="$NUGET_SOURCE"
- run: dotnet nuget remove source "ADO"
- - uses: actions/upload-artifact@v4
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
name: Upload package to Artifacts
with:
name: mpac-package
@@ -172,22 +172,22 @@ jobs:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
shardTotal: [20]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Use Node.js 22.x
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.x
- run: npm ci
- run: npx playwright install --with-deps
- name: "Az CLI login"
- uses: Azure/login@v2
+ uses: Azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2.3.1
with:
client-id: ${{ secrets.E2ETESTS_CLIENT_ID }}
tenant-id: ${{ secrets.E2ETESTS_TENANT_ID }}
subscription-id: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
# We can't use MSAL within playwright so we acquire tokens prior to running the tests
- name: "Acquire RBAC tokens for test accounts"
- uses: azure/cli@v2
+ uses: azure/cli@9f7ce6f37c31b777ec6c6b6d1dfe7db79f497956 # v2.2.0
env:
DE_ACCOUNT_PREFIX: ${{ secrets.E2ETESTS_ACCOUNT_PREFIX }}
DE_TEST_RESOURCE_GROUP: ${{ secrets.E2ETESTS_RESOURCEGROUP_NAME }}
@@ -231,7 +231,7 @@ jobs:
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
- name: "Re-auth for upload (refresh OIDC token)"
if: ${{ !cancelled() }}
- uses: Azure/login@v2
+ uses: Azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2.3.1
with:
client-id: ${{ secrets.E2ETESTS_CLIENT_ID }}
tenant-id: ${{ secrets.E2ETESTS_TENANT_ID }}
@@ -262,15 +262,15 @@ jobs:
pull-requests: write
id-token: write
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-node@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.x
- name: Install dependencies
run: npm ci
- name: "Az CLI login"
- uses: Azure/login@v2
+ uses: Azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2.3.1
with:
client-id: ${{ secrets.E2ETESTS_CLIENT_ID }}
tenant-id: ${{ secrets.E2ETESTS_TENANT_ID }}
diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml
index add9f15e1..5382dc11e 100644
--- a/.github/workflows/cleanup.yml
+++ b/.github/workflows/cleanup.yml
@@ -23,16 +23,16 @@ jobs:
AZURE_SUBSCRIPTION_ID: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
E2ETESTS_RESOURCEGROUP_NAME : ${{ secrets.E2ETESTS_RESOURCEGROUP_NAME }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: "Az CLI login"
- uses: azure/login@v2
+ uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2.3.1
with:
client-id: ${{ secrets.E2ETESTS_CLIENT_ID }}
tenant-id: ${{ secrets.E2ETESTS_TENANT_ID }}
subscription-id: ${{ secrets.E2ETESTS_SUBSCRIPTION_ID }}
- name: Use Node.js 22.x
- uses: actions/setup-node@v4
+ uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.x
- run: npm ci
From 5080e516d7d245bbfa68ca194bc1647ebbe8981a Mon Sep 17 00:00:00 2001
From: jawelton74 <103591340+jawelton74@users.noreply.github.com>
Date: Wed, 2 Sep 2026 13:28:31 -0700
Subject: [PATCH 08/16] Update preview site URL (#2572)
* Update all preview website URL references to the new URL.
* Remove az cli config.
* Fix formatting.
* Fix new preview URL in ConfigContext
---
.github/PULL_REQUEST_TEMPLATE.md | 2 +-
preview/.azure/config | 7 -------
preview/README.md | 4 ++--
preview/config.json | 2 +-
preview/index.js | 2 +-
src/ConfigContext.ts | 2 +-
src/Utils/EndpointUtils.ts | 4 +++-
web.config | 2 +-
8 files changed, 10 insertions(+), 15 deletions(-)
delete mode 100644 preview/.azure/config
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 7ff47e44c..f294ca39f 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1 +1 @@
-[Preview this branch](https://dataexplorer-preview.azurewebsites.net/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)
+[Preview this branch](https://dataexplorer-preview.portal.cosmos.azure.com/pull/EDIT_THIS_NUMBER_IN_THE_PR_DESCRIPTION?feature.someFeatureFlagYouMightNeed=true)
diff --git a/preview/.azure/config b/preview/.azure/config
deleted file mode 100644
index 4e174b741..000000000
--- a/preview/.azure/config
+++ /dev/null
@@ -1,7 +0,0 @@
-[defaults]
-group = dataexplorer-preview
-sku = P1v2
-appserviceplan = dataexplorer-preview
-location = westus2
-web = dataexplorer-preview
-
diff --git a/preview/README.md b/preview/README.md
index 59a84a2d7..86858539e 100644
--- a/preview/README.md
+++ b/preview/README.md
@@ -4,8 +4,8 @@ Cosmos Explorer Preview makes it possible to try a working version of any commit
Initial support is for Hosted (Connection string only) or the Azure Portal. Examples:
-Connection string URLs: https://dataexplorer-preview.azurewebsites.net/commit/COMMIT_SHA/hostedExplorer.html
-Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://dataexplorer-preview.azurewebsites.net/commit/COMMIT_SHA/explorer.html#home
+Connection string URLs: https://dataexplorer-preview.portal.cosmos.azure.com/commit/COMMIT_SHA/hostedExplorer.html
+Portal URLs: https://ms.portal.azure.com/?dataExplorerSource=https://dataexplorer-preview.portal.cosmos.azure.com/commit/COMMIT_SHA/explorer.html#home
In both cases replace `COMMIT_SHA` with the commit you want to view. It must have already completed its build on GitHub Actions.
diff --git a/preview/config.json b/preview/config.json
index a67bcd2ef..0332eae1f 100644
--- a/preview/config.json
+++ b/preview/config.json
@@ -1,4 +1,4 @@
{
"PROXY_PATH": "/proxy",
- "msalRedirectURI": "https://dataexplorer-preview.azurewebsites.net/"
+ "msalRedirectURI": "https://dataexplorer-preview.portal.cosmos.azure.com/"
}
diff --git a/preview/index.js b/preview/index.js
index be44c63ed..7a994a7e6 100644
--- a/preview/index.js
+++ b/preview/index.js
@@ -4,7 +4,7 @@ const port = process.env.PORT || 3000;
const fetch = require("node-fetch");
const backendEndpoint = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
-const previewSiteEndpoint = "https://dataexplorer-preview.azurewebsites.net";
+const previewSiteEndpoint = "https://dataexplorer-preview.portal.cosmos.azure.com";
const previewStorageWebsiteEndpoint = "_REPLACE_STORAGE_WEBSITE_ENDPOINT_";
const githubApiUrl = "https://api.github.com/repos/Azure/cosmos-explorer";
const azurePortalMpacEndpoint = "https://ms.portal.azure.com/";
diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts
index a14deb248..34eaeed07 100644
--- a/src/ConfigContext.ts
+++ b/src/ConfigContext.ts
@@ -80,7 +80,7 @@ let configContext: Readonly = {
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
`^https:\\/\\/.*\\.powerbi\\.com$`,
- `^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
+ `^https:\\/\\/dataexplorer-preview\\.portal\\.cosmos\\.azure\\.com$`,
`^https:\\/\\/explorer\\.cosmos\\.sovcloud-api\\.fr$`,
`^https:\\/\\/portal\\.sovcloud-azure\\.fr$`,
`^https:\\/\\/explorer\\.cosmos\\.sovcloud-api\\.de$`,
diff --git a/src/Utils/EndpointUtils.ts b/src/Utils/EndpointUtils.ts
index 9f716fcb2..e230a0469 100644
--- a/src/Utils/EndpointUtils.ts
+++ b/src/Utils/EndpointUtils.ts
@@ -88,7 +88,9 @@ export const allowedHostedExplorerEndpoints: ReadonlyArray = [
...(process.env.NODE_ENV === "development" ? ["https://localhost:12900"] : []),
];
-export const allowedMsalRedirectEndpoints: ReadonlyArray = ["https://dataexplorer-preview.azurewebsites.net/"];
+export const allowedMsalRedirectEndpoints: ReadonlyArray = [
+ "https://dataexplorer-preview.portal.cosmos.azure.com/",
+];
export const allowedJunoOrigins: ReadonlyArray = [
JunoEndpoints.Test,
diff --git a/web.config b/web.config
index e987fa939..2bea8b149 100644
--- a/web.config
+++ b/web.config
@@ -30,7 +30,7 @@
-
+
From 1f04f0ae7662aed13941f5b65dc59532aeeea7d4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 3 Sep 2026 17:01:01 -0700
Subject: [PATCH 09/16] Bump fast-uri from 3.1.5 to 3.1.7 (#2582)
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.5 to 3.1.7.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.5...v3.1.7)
---
updated-dependencies:
- dependency-name: fast-uri
dependency-version: 3.1.7
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index f6ab234a8..b7e080993 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13493,9 +13493,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
- "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
+ "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
"funding": [
{
"type": "github",
From f032a181070cc37b34fd7d3bbbfc85aaa187c7d1 Mon Sep 17 00:00:00 2001
From: olprod
Date: Thu, 10 Sep 2026 10:10:44 -0700
Subject: [PATCH 10/16] Localized file check-in by OneLocBuild Task: Build
definition ID 13114: Build ID 2577277 (#2488)
* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2550970
* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2552493
* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2553780
* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2555570
* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2557396
* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2559084
* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2566214
* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2567719
* Localized file check-in by OneLocBuild Task: Build definition ID 13114: Build ID 2569196
---------
Co-authored-by: sunghyunkang1111 <114709653+sunghyunkang1111@users.noreply.github.com>
---
src/Localization/cs/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/de/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/es/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/fr/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/hu/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/id/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/it/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/ja/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/ko/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/nl/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/pl/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/pt-BR/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/pt-PT/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/ru/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/sv/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/tr/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/zh-Hans/Resources.json | 25 ++++++++++++++++++++++---
src/Localization/zh-Hant/Resources.json | 25 ++++++++++++++++++++++---
18 files changed, 396 insertions(+), 54 deletions(-)
diff --git a/src/Localization/cs/Resources.json b/src/Localization/cs/Resources.json
index 5797c9366..a8685b5cd 100644
--- a/src/Localization/cs/Resources.json
+++ b/src/Localization/cs/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "Odstranit uživatelem definovanou funkci"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "Duplikovat kartu",
+ "closeTab": "Zavřít kartu"
+ },
"documents": {
"newItem": "Nová položka",
"newDocument": "Nový dokument",
@@ -760,11 +764,10 @@
"settings": "Nastavení",
"indexingPolicy": "Zásada indexování",
"partitionKeys": "Klíče oddílu",
- "partitionKeysPreview": "Klíče oddílů (Preview)",
"computedProperties": "Vypočítané vlastnosti",
"containerPolicies": "Zásady kontejneru",
"throughputBuckets": "Kbelíky propustnosti",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "Globální sekundární index",
"maskingPolicyPreview": "Zásady maskování"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "Změnit: {{partitionKeyName}}",
+ "confirmCancel1": "Chystáte se zrušit následující úlohu kopírování.",
+ "confirmCancel2": "Zrušením se úloha okamžitě zastaví.",
+ "confirmComplete1": "Chystáte se dokončit následující úlohu kopírování.",
+ "confrimComplete2": "Po dokončení a po zpracování všech čekajících dokumentů se průběžné kopírování dat zastaví. Pokud chcete zachovat integritu dat, doporučujeme před dokončením úlohy zastavit aktualizace zdrojového kontejneru.",
"currentPartitionKey": "Aktuální {{partitionKeyName}}",
"partitioning": "Dělení na oddíly",
"hierarchical": "Hierarchický",
@@ -1033,6 +1040,7 @@
"description": "Úlohy online kopírování kontejnerů umožňují kopírovat data ze zdrojového kontejneru do cílového kontejneru rozhraní API NoSQL služby Cosmos DB pomocí kanálu změn [Všechny verze a odstranění](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview). Díky tomu můžou aktualizace ve zdroji pokračovat i během kopírování dat. Na konci je potřeba krátký výpadek, aby bylo možné bezpečně přepnout klientské aplikace na cílový kontejner. Další informace o [úlohách online kopírování](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
+ "migrationTypeTitle": "Typ migrace",
"selectContainers": {
"description": "Vyberte zdrojový a cílový kontejner, do kterého chcete kopírovat.",
"sourceContainerSubHeading": "Zdrojový kontejner",
@@ -1117,7 +1125,8 @@
"buttonText": "Povolit online kopírování",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Ověřuje se režim kanálu změn Všechny verze a odstranění (Preview)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Povoluje se režim kanálu změn Všechny verze a odstranění (Preview)...",
- "enablingOnlineCopySpinnerLabel": "Povoluje se online kopírování pro účet {{accountName}}..."
+ "enablingOnlineCopySpinnerLabel": "Povoluje se online kopírování pro účet {{accountName}}...",
+ "onlineMigrationPrerequisitesMessage": "Před pokračováním musí být povoleny požadavky na online migraci."
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "Potvrdit",
"cancelButtonText": "Zrušit"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "Úloha přenosu dat {{jobName}} byla zrušena",
+ "completedConsoleMessage": "Úloha přenosu dat {{jobName}} byla dokončena",
+ "defaultErrorMessage": "Operaci se nepovedlo dokončit.",
+ "errorConsoleMessage": "Úloha přenosu dat {{jobName}} selhala: {{errorMessage}}",
+ "pauseConsoleMessage": "Úloha přenosu dat {{jobName}} byla pozastavena",
+ "retryConsoleMessage": "Probíhá úloha přenosu dat {{jobName}}, celkový počet: {{totalCount}}, počet zpracovaných: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/de/Resources.json b/src/Localization/de/Resources.json
index 23ebc9bb6..1a6e02b47 100644
--- a/src/Localization/de/Resources.json
+++ b/src/Localization/de/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "Benutzerdefinierte Funktion löschen"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "Doppelte Registerkarte",
+ "closeTab": "Registerkarte schließen"
+ },
"documents": {
"newItem": "Neues Element",
"newDocument": "Neues Dokument",
@@ -760,11 +764,10 @@
"settings": "Einstellungen",
"indexingPolicy": "Indizierungsrichtlinie",
"partitionKeys": "Partitionsschlüssel",
- "partitionKeysPreview": "Partitionsschlüssel (Vorschau)",
"computedProperties": "Berechnete Eigenschaften",
"containerPolicies": "Containerrichtlinien",
"throughputBuckets": "Durchsatzbuckets",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "Globaler sekundärer Index",
"maskingPolicyPreview": "Maskierungsrichtlinie"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "„{{partitionKeyName}}“ ändern",
+ "confirmCancel1": "Sie sind im Begriff, den folgenden Kopierauftrag abzubrechen.",
+ "confirmCancel2": "Durch das Abbrechen wird der Auftrag sofort beendet.",
+ "confirmComplete1": "Sie sind im Begriff, den folgenden Kopierauftrag abzuschließen.",
+ "confrimComplete2": "Nach Abschluss des Vorgangs wird die kontinuierliche Datenkopie beendet, nachdem alle ausstehenden Dokumente verarbeitet wurden. Um die Datenintegrität aufrechtzuerhalten, wird empfohlen, Aktualisierungen des Quellcontainers vor Abschluss des Auftrags zu beenden.",
"currentPartitionKey": "Aktueller {{partitionKeyName}}",
"partitioning": "Partitionierung",
"hierarchical": "Hierarchisch",
@@ -1033,6 +1040,7 @@
"description": "Mit Onlinecontainerkopieraufträgen können Sie Daten mithilfe des Änderungsfeeds [Alle Versionen und Löschvorgänge](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview) aus einem Quellcontainer in einen Cosmos DB NoSQL-API-Zielcontainer kopieren. Dadurch können Updates in der Quelle fortgesetzt werden, während Daten kopiert werden. Am Ende ist eine kurze Downtime erforderlich, um sicher über Clientanwendungen zum Zielcontainer zu wechseln. Weitere Informationen zu [Onlinekopieraufträgen](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
+ "migrationTypeTitle": "Migrationstyp",
"selectContainers": {
"description": "Wählen Sie einen Quellcontainer und einen Zielcontainer aus, in den kopiert werden soll.",
"sourceContainerSubHeading": "Quellcontainer",
@@ -1117,7 +1125,8 @@
"buttonText": "Onlinekopie aktivieren",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Änderungsfeedmodus „Alle Versionen und Löschvorgänge“ (Vorschau) wird validiert ...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Änderungsfeedmodus „Alle Versionen und Löschvorgänge“ (Vorschau) wird aktiviert",
- "enablingOnlineCopySpinnerLabel": "Onlinekopie wird für Ihr Konto „{{accountName}}“ aktiviert ..."
+ "enablingOnlineCopySpinnerLabel": "Onlinekopie wird für Ihr Konto „{{accountName}}“ aktiviert ...",
+ "onlineMigrationPrerequisitesMessage": "Die Voraussetzungen für die Onlinemigration müssen aktiviert sein, bevor Sie fortfahren können."
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "Bestätigen",
"cancelButtonText": "Abbrechen"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "Datenübertragungsauftrag „{{jobName}}“ abgebrochen",
+ "completedConsoleMessage": "Datenübertragungsauftrag „{{jobName}}“ abgeschlossen",
+ "defaultErrorMessage": "Vorgang konnte nicht abgeschlossen werden",
+ "errorConsoleMessage": "Fehler beim Datenübertragungsauftrag „{{jobName}}“: {{errorMessage}}",
+ "pauseConsoleMessage": "Datenübertragungsauftrag „{{jobName}}“ angehalten",
+ "retryConsoleMessage": "Der Datenübertragungsauftrag „{{jobName}}“ ist in Arbeit, Gesamtanzahl: {{totalCount}}, verarbeitete Anzahl: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/es/Resources.json b/src/Localization/es/Resources.json
index 6015a13c2..5c14c164a 100644
--- a/src/Localization/es/Resources.json
+++ b/src/Localization/es/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "Eliminar función definida por el usuario"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "Duplicar pestaña",
+ "closeTab": "Cerrar pestaña"
+ },
"documents": {
"newItem": "Nuevo elemento",
"newDocument": "Nuevo documento",
@@ -760,11 +764,10 @@
"settings": "Configuración",
"indexingPolicy": "Directiva de indexación",
"partitionKeys": "Claves de partición",
- "partitionKeysPreview": "Claves de partición (versión preliminar)",
"computedProperties": "Propiedades calculadas",
"containerPolicies": "Directivas de contenedor",
"throughputBuckets": "Depósitos de rendimiento",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "Índice secundario global",
"maskingPolicyPreview": "Directiva de enmascaramiento"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "Cambiar {{partitionKeyName}}",
+ "confirmCancel1": "Está a punto de cancelar el siguiente trabajo de copia.",
+ "confirmCancel2": "La cancelación detendrá el trabajo inmediatamente.",
+ "confirmComplete1": "Está a punto de completar el siguiente trabajo de copia.",
+ "confrimComplete2": "Una vez completada, la copia continua de datos se detendrá después de que se procesen los documentos pendientes. Para mantener la integridad de los datos, se recomienda detener las actualizaciones del contenedor de origen antes de completar el trabajo.",
"currentPartitionKey": "Actual {{partitionKeyName}}",
"partitioning": "Creación de particiones",
"hierarchical": "Jerárquico",
@@ -1033,6 +1040,7 @@
"description": "Los trabajos de copia de contenedores en línea permiten copiar datos de un contenedor de origen a un contenedor de destino de la API NoSQL de Cosmos DB mediante la fuente de cambios [Todas las versiones y eliminación](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview). Esto permite que las actualizaciones continúen en el origen mientras se copian los datos. Se requiere un breve tiempo de inactividad al final para cambiar de forma segura las aplicaciones cliente al contenedor de destino. Obtenga más información sobre [trabajos de copia online](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
+ "migrationTypeTitle": "Tipo de migración",
"selectContainers": {
"description": "Seleccione un contenedor de origen y un contenedor de destino en el que copiar.",
"sourceContainerSubHeading": "Contenedor de origen",
@@ -1117,7 +1125,8 @@
"buttonText": "Habilitar copia en línea",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Validando todas las versiones y elimina el modo de fuente de cambios (versión preliminar)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Habilitando todas las versiones y elimina el modo de fuente de cambios (versión preliminar)...",
- "enablingOnlineCopySpinnerLabel": "Habilitando la copia en línea en su cuenta \"{{accountName}}\"..."
+ "enablingOnlineCopySpinnerLabel": "Habilitando la copia en línea en su cuenta \"{{accountName}}\"...",
+ "onlineMigrationPrerequisitesMessage": "Los requisitos previos de migración en línea deben estar habilitados antes de continuar."
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "Confirmar",
"cancelButtonText": "Cancelar"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "Trabajo de transferencia de datos \"{{jobName}}\" cancelado",
+ "completedConsoleMessage": "Trabajo de transferencia de datos \"{{jobName}}\" completado",
+ "defaultErrorMessage": "No se ha podido completar la operación",
+ "errorConsoleMessage": "El trabajo de transferencia de datos \"{{jobName}}\" ha fallado: {{errorMessage}}",
+ "pauseConsoleMessage": "Trabajo de transferencia de datos \"{{jobName}}\" pausado",
+ "retryConsoleMessage": "Trabajo de transferencia de datos \"{{jobName}}\" en curso, recuento total: {{totalCount}}, recuento procesado: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/fr/Resources.json b/src/Localization/fr/Resources.json
index d28813c23..610e3c58c 100644
--- a/src/Localization/fr/Resources.json
+++ b/src/Localization/fr/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "Supprimer une fonction définie par l’utilisateur"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "Dupliquer l’onglet",
+ "closeTab": "Fermer un onglet"
+ },
"documents": {
"newItem": "Nouvel élément",
"newDocument": "Nouveau document",
@@ -760,11 +764,10 @@
"settings": "Paramètres",
"indexingPolicy": "Politique d'indexation",
"partitionKeys": "Clés de partition",
- "partitionKeysPreview": "Clés de partition (aperçu)",
"computedProperties": "Propriétés calculées",
"containerPolicies": "Stratégies relatives aux conteneurs",
"throughputBuckets": "Bacs de débit",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "Index secondaire global",
"maskingPolicyPreview": "Stratégie de masquage"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "Modifier {{partitionKeyName}}",
+ "confirmCancel1": "Vous êtes sur le point d’annuler le travail de copie suivant.",
+ "confirmCancel2": "L’annulation arrêtera immédiatement le travail.",
+ "confirmComplete1": "Vous êtes sur le point d’achever le travail de copie suivant.",
+ "confrimComplete2": "Une fois l’opération terminée, la copie continue des données s’arrête après le traitement de tous les documents en attente. Pour préserver l’intégrité des données, nous vous recommandons d’arrêter les mises à jour du conteneur source avant d’effectuer le travail.",
"currentPartitionKey": "{{partitionKeyName}} actuel(le)",
"partitioning": "Partitionnement",
"hierarchical": "Hiérarchique",
@@ -1033,6 +1040,7 @@
"description": "Les travaux de copie en ligne de conteneur vous permettent de copier des données d’un conteneur source vers un conteneur cible d’API NoSQL de Cosmos DB à l’aide du flux de modification [All Versions and Delete](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview). Cela vous permet de continuer à mettre à jour la source pendant la copie des données. Un bref temps d’arrêt est nécessaire à la fin pour basculer en toute sécurité les applications clientes vers le conteneur cible. En savoir plus sur les [travaux de copie en ligne](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
+ "migrationTypeTitle": "Type de migration",
"selectContainers": {
"description": "Veuillez sélectionner un conteneur source et un conteneur de destination vers lequel effectuer la copie.",
"sourceContainerSubHeading": "Conteneur source",
@@ -1117,7 +1125,8 @@
"buttonText": "Activer la copie en ligne",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Validation en cours du Mode de flux de modification de toutes les versions et suppressions (préversion)",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Activation en cours du Mode de flux de modification de toutes les versions et suppressions (préversion)...",
- "enablingOnlineCopySpinnerLabel": "Activation de la copie en ligne sur votre compte « {{accountName}} » ..."
+ "enablingOnlineCopySpinnerLabel": "Activation de la copie en ligne sur votre compte « {{accountName}} » ...",
+ "onlineMigrationPrerequisitesMessage": "Vous devez activer les conditions préalables à la migration en ligne avant de continuer."
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "Confirmer",
"cancelButtonText": "Annuler"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "Travail de transfert de données « {{jobName}} » annulé",
+ "completedConsoleMessage": "Travail de transfert de données « {{jobName}} » terminé",
+ "defaultErrorMessage": "L'opération n'a pas pu être terminée.",
+ "errorConsoleMessage": "Nous n’avons pas pu effectuer le travail de transfert de données « {{jobName}} » : {{errorMessage}}",
+ "pauseConsoleMessage": "Travail de transfert de données « {{jobName}} » en pause",
+ "retryConsoleMessage": "Travail de transfert de données « {{jobName}} » en cours, nombre total : {{totalCount}}, nombre traité : {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/hu/Resources.json b/src/Localization/hu/Resources.json
index 621fe16bc..bc83cd668 100644
--- a/src/Localization/hu/Resources.json
+++ b/src/Localization/hu/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "Felhasználó által definiált függvény törlése"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "Lap duplikálása",
+ "closeTab": "Lap bezárása"
+ },
"documents": {
"newItem": "Új elem",
"newDocument": "Új dokumentum",
@@ -760,11 +764,10 @@
"settings": "Beállítások",
"indexingPolicy": "Indexelési házirend",
"partitionKeys": "Partíciókulcsok",
- "partitionKeysPreview": "Partíciókulcsok (előzetes verzió)",
"computedProperties": "Számított tulajdonságok",
"containerPolicies": "Tárolószabályzatok",
"throughputBuckets": "Átviteli sebesség gyűjtői",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "Globális másodlagos index",
"maskingPolicyPreview": "Maszkolási szabályzat"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "{{partitionKeyName}} módosítása",
+ "confirmCancel1": "A következő másolási feladat megszakítására készül.",
+ "confirmCancel2": "A megszakítás azonnal leállítja a feladatot.",
+ "confirmComplete1": "A következő másolási feladat végrehajtására készül.",
+ "confrimComplete2": "Ha elkészült, a függőben lévő dokumentumok feldolgozása után a folyamatos adatmásolás leáll. Az adatintegritás megőrzése érdekében javasoljuk, hogy a feladat végrehajtása előtt állítsa le a forrástároló frissítéseit.",
"currentPartitionKey": "Aktuális {{partitionKeyName}}",
"partitioning": "Particionálás",
"hierarchical": "Hierarchikus",
@@ -1033,6 +1040,7 @@
"description": "Az online tárolómásolási feladatok lehetővé teszik, hogy adatokat másoljon egy forrástárolóból egy cél Cosmos DB NoSQL API-tárolóba a [Minden verzió és törlés](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview) változáscsatorna használatával. Ez lehetővé teszi, hogy a frissítések az adatok másolása közben is folytatódjanak a forráson. A végén rövid leállásra van szükség ahhoz, hogy biztonságosan átválthassa az ügyfélalkalmazásokat a céltárolóra. További információ az [online másolási feladatokról](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
+ "migrationTypeTitle": "Áttelepítés típusa",
"selectContainers": {
"description": "Válasszon ki egy forrástárolót és egy céltárolót, amelybe másolni szeretne.",
"sourceContainerSubHeading": "Forrástároló",
@@ -1117,7 +1125,8 @@
"buttonText": "Online másolás engedélyezése",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Az összes verzió és a törlések változáscsatorna-módjának ellenőrzése",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Az összes verzió és a törlések változáscsatorna-módjának engedélyezése...",
- "enablingOnlineCopySpinnerLabel": "Az online másolás engedélyezése a(z) „{{accountName}}” fiókban ..."
+ "enablingOnlineCopySpinnerLabel": "Az online másolás engedélyezése a(z) „{{accountName}}” fiókban ...",
+ "onlineMigrationPrerequisitesMessage": "A folytatás előtt engedélyezni kell az online áttelepítés előfeltételeit."
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "Megerősítés",
"cancelButtonText": "Mégse"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "A(z) {{jobName}} adatátviteli feladat megszakítva",
+ "completedConsoleMessage": "A(z) {{jobName}} adatátviteli feladat kész",
+ "defaultErrorMessage": "A művelet nem fejezhető be",
+ "errorConsoleMessage": "A(z) {{jobName}} adatátviteli feladat meghiúsult: {{errorMessage}}",
+ "pauseConsoleMessage": "A(z) {{jobName}} adatátviteli feladat szüneteltetve",
+ "retryConsoleMessage": "Folyamatban van a(z) {{jobName}} adatátviteli feladat; teljes szám: {{totalCount}}, feldolgozottak száma: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/id/Resources.json b/src/Localization/id/Resources.json
index bb381aac6..5bb704da8 100644
--- a/src/Localization/id/Resources.json
+++ b/src/Localization/id/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "Hapus Fungsi yang Ditentukan Pengguna"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "Duplikasikan tab",
+ "closeTab": "Tutup tab"
+ },
"documents": {
"newItem": "Item Baru",
"newDocument": "Dokumen Baru",
@@ -760,11 +764,10 @@
"settings": "Pengaturan",
"indexingPolicy": "Kebijakan Pengindeksan",
"partitionKeys": "Kunci Partisi",
- "partitionKeysPreview": "Kunci Partisi (pratinjau)",
"computedProperties": "Properti Terkomputasi",
"containerPolicies": "Kebijakan Kontainer",
"throughputBuckets": "Wadah Throughput",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "Indeks Sekunder Global",
"maskingPolicyPreview": "Kebijakan Masking"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "Ubah {{partitionKeyName}}",
+ "confirmCancel1": "Anda akan segera membatalkan tugas penyalinan berikut.",
+ "confirmCancel2": "Membatalkan akan segera menghentikan tugas.",
+ "confirmComplete1": "Anda akan segera menyelesaikan tugas penyalinan berikut.",
+ "confrimComplete2": "Setelah selesai, penyalinan data berkelanjutan akan berhenti setelah semua dokumen yang tertunda diproses. Untuk menjaga integritas data, sebaiknya hentikan pembaruan ke kontainer sumber sebelum menyelesaikan tugas.",
"currentPartitionKey": "{{partitionKeyName}} saat Ini",
"partitioning": "Pemartisian",
"hierarchical": "Hierarkis",
@@ -1033,6 +1040,7 @@
"description": "Pekerjaan penyalinan kontainer online memungkinkan Anda menyalin data dari kontainer sumber ke kontainer API Cosmos DB NoSQL tujuan menggunakan umpan perubahan [Semua Versi dan Hapus](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview). Hal ini memungkinkan pembaruan untuk melanjutkan sumber saat data disalin. Waktu henti singkat diperlukan di akhir untuk mengalihkan aplikasi klien ke kontainer tujuan dengan aman. Pelajari selengkapnya tentang [pekerjaan penyalinan online](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
+ "migrationTypeTitle": "Jenis migrasi",
"selectContainers": {
"description": "Pilih kontainer sumber dan kontainer tujuan yang akan digunakan untuk penyalinan.",
"sourceContainerSubHeading": "Kontainer sumber",
@@ -1117,7 +1125,8 @@
"buttonText": "Aktifkan Penyalinan Online",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Memvalidasi Semua versi dan penghapusan mengubah mode umpan (pratinjau)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Mengaktifkan Semua versi dan menghapus mengubah mode umpan (pratinjau)...",
- "enablingOnlineCopySpinnerLabel": "Mengaktifkan penyalinan online di akun \"{{accountName}}\"..."
+ "enablingOnlineCopySpinnerLabel": "Mengaktifkan penyalinan online di akun \"{{accountName}}\"...",
+ "onlineMigrationPrerequisitesMessage": "Prasyarat migrasi online harus diaktifkan sebelum melanjutkan."
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "Konfirmasikan",
"cancelButtonText": "Batal"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "Tugas transfer data \"{{jobName}}\" dibatalkan",
+ "completedConsoleMessage": "Tugas transfer data \"{{jobName}}\" selesai",
+ "defaultErrorMessage": "Operasi tidak dapat diselesaikan",
+ "errorConsoleMessage": "Tugas transfer data \"{{jobName}}\" gagal: {{errorMessage}}",
+ "pauseConsoleMessage": "Tugas transfer data \"{{jobName}}\" dijeda",
+ "retryConsoleMessage": "Tugas transfer data \"{{jobName}}\" sedang berlangsung, jumlah total: {{totalCount}}, jumlah diproses: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/it/Resources.json b/src/Localization/it/Resources.json
index 83d0725e4..c050e50a5 100644
--- a/src/Localization/it/Resources.json
+++ b/src/Localization/it/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "Elimina funzione definita dall'utente"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "Duplica scheda",
+ "closeTab": "Chiudi scheda"
+ },
"documents": {
"newItem": "Nuovo elemento",
"newDocument": "Nuovo documento",
@@ -760,11 +764,10 @@
"settings": "Impostazioni",
"indexingPolicy": "Criteri di indicizzazione",
"partitionKeys": "Chiavi di partizione",
- "partitionKeysPreview": "Chiavi di partizione (anteprima)",
"computedProperties": "Proprietà calcolate",
"containerPolicies": "Criteri contenitore",
"throughputBuckets": "Bucket di velocità effettiva",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "Indice secondario globale",
"maskingPolicyPreview": "Criteri di maschera"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "Modifica {{partitionKeyName}}",
+ "confirmCancel1": "Si sta per annullare il processo di copia seguente.",
+ "confirmCancel2": "L'annullamento arresterà immediatamente il processo.",
+ "confirmComplete1": "Si sta per completare il processo di copia seguente.",
+ "confrimComplete2": "Al termine, la copia continua dei dati verrà interrotta dopo l'elaborazione di eventuali documenti in sospeso. Per mantenere l'integrità dei dati, è consigliabile arrestare gli aggiornamenti al contenitore di origine prima di completare il processo.",
"currentPartitionKey": "{{partitionKeyName}} corrente",
"partitioning": "Partizionamento",
"hierarchical": "Gerarchico",
@@ -1033,6 +1040,7 @@
"description": "I processi di copia dei contenitori online consentono di copiare i dati da un contenitore di origine a un contenitore di destinazione dell'API NoSQL di Cosmos DB usando il feed di modifiche [Tutte le versioni ed eliminazioni](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview). In questo modo gli aggiornamenti possono continuare nell'origine durante la copia dei dati. Alla fine è necessario un breve periodo di inattività per passare in modo sicuro le applicazioni client al contenitore di destinazione. Altre informazioni sui [processi di copia online](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
+ "migrationTypeTitle": "Tipo di migrazione",
"selectContainers": {
"description": "Selezionare un contenitore di origine e un contenitore di destinazione in cui copiare.",
"sourceContainerSubHeading": "Contenitore di origine",
@@ -1117,7 +1125,8 @@
"buttonText": "Abilita copia online",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Convalida della modalità feed di modifiche per tutte le versioni ed eliminazioni (anteprima)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Abilitazione della modalità feed di modifiche per tutte le versioni ed eliminazioni (anteprima)...",
- "enablingOnlineCopySpinnerLabel": "Abilitazione della copia online nell'account \"{{accountName}}\"..."
+ "enablingOnlineCopySpinnerLabel": "Abilitazione della copia online nell'account \"{{accountName}}\"...",
+ "onlineMigrationPrerequisitesMessage": "Prima di procedere, è necessario abilitare i prerequisiti per la migrazione online."
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "Conferma",
"cancelButtonText": "Annulla"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "Processo di trasferimento dati \"{{jobName}}\" annullato",
+ "completedConsoleMessage": "Processo di trasferimento dati \"{{jobName}}\" completato",
+ "defaultErrorMessage": "Non è stato possibile completare l'operazione",
+ "errorConsoleMessage": "Processo di trasferimento dati \"{{jobName}}\" non riuscito: {{errorMessage}}",
+ "pauseConsoleMessage": "Processo di trasferimento dati \"{{jobName}}\" sospeso",
+ "retryConsoleMessage": "Processo di trasferimento dati \"{{jobName}}\" in corso, conteggio totale: {{totalCount}}, conteggio elaborato: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/ja/Resources.json b/src/Localization/ja/Resources.json
index c2dec9990..55ac6a21d 100644
--- a/src/Localization/ja/Resources.json
+++ b/src/Localization/ja/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "ユーザー定義関数の削除"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "タブの複製",
+ "closeTab": "タブを閉じる"
+ },
"documents": {
"newItem": "新しい項目",
"newDocument": "新規ドキュメント",
@@ -760,11 +764,10 @@
"settings": "設定",
"indexingPolicy": "インデックス作成ポリシー",
"partitionKeys": "パーティション キー",
- "partitionKeysPreview": "パーティション キー (プレビュー)",
"computedProperties": "計算されたプロパティ",
"containerPolicies": "コンテナー ポリシー",
"throughputBuckets": "スループット バケット",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "グローバル セカンダリ インデックス",
"maskingPolicyPreview": "マスキング ポリシー"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "{{partitionKeyName}} の変更",
+ "confirmCancel1": "次のコピー ジョブを取り消します。",
+ "confirmCancel2": "取り消すと、ジョブはすぐに停止します。",
+ "confirmComplete1": "次のコピー ジョブを完了します。",
+ "confrimComplete2": "完了すると、保留中のドキュメントが処理された後、継続的なデータ コピーが停止します。データの整合性を維持するには、ジョブを完了する前にソース コンテナーの更新を停止することをお勧めします。",
"currentPartitionKey": "現在の {{partitionKeyName}}",
"partitioning": "パーティション分割",
"hierarchical": "階層",
@@ -1033,6 +1040,7 @@
"description": "オンライン コンテナー コピー ジョブでは、[All Versions and Delete](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview) 変更フィードを使用して、ソース コンテナーからコピー先の Cosmos DB NoSQL API コンテナーにデータをコピーできます。これにより、データのコピー中もソースで更新を継続できます。クライアント アプリケーションをコピー先コンテナーに安全に切り替えるには、最後に短時間のダウンタイムが必要です。[online copy jobs](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started) についての詳細をご覧ください。"
}
},
+ "migrationTypeTitle": "移行の種類",
"selectContainers": {
"description": "コピー元のコンテナーとコピー先のコンテナーを選択してください。",
"sourceContainerSubHeading": "ソース コンテナー",
@@ -1117,7 +1125,8 @@
"buttonText": "オンライン コピーを有効にする",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "すべてのバージョンを検証し、変更フィード モードを削除します (プレビュー)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "すべてのバージョンおよび削除の変更フィード モード (プレビュー) を有効にしています",
- "enablingOnlineCopySpinnerLabel": "\"{{accountName}}\" アカウントでオンライン コピーを有効にしています..."
+ "enablingOnlineCopySpinnerLabel": "\"{{accountName}}\" アカウントでオンライン コピーを有効にしています...",
+ "onlineMigrationPrerequisitesMessage": "続行する前に、オンライン移行の前提条件を有効にする必要があります。"
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "確認",
"cancelButtonText": "キャンセル"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "データ転送ジョブ \"{{jobName}}\" が取り消されました",
+ "completedConsoleMessage": "データ転送ジョブ \"{{jobName}}\" が完了しました",
+ "defaultErrorMessage": "操作を完了できませんでした",
+ "errorConsoleMessage": "データ転送ジョブ \"{{jobName}}\" に失敗しました: {{errorMessage}}",
+ "pauseConsoleMessage": "データ転送ジョブ \"{{jobName}}\" が一時停止されました",
+ "retryConsoleMessage": "データ転送ジョブ \"{{jobName}}\" が進行中です。合計数: {{totalCount}}、処理された数: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/ko/Resources.json b/src/Localization/ko/Resources.json
index ed774fe50..ceede69f6 100644
--- a/src/Localization/ko/Resources.json
+++ b/src/Localization/ko/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "사용자 정의 함수 삭제"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "중복 탭",
+ "closeTab": "탭 닫기"
+ },
"documents": {
"newItem": "새 항목",
"newDocument": "새 문서",
@@ -760,11 +764,10 @@
"settings": "설정",
"indexingPolicy": "인덱싱 정책",
"partitionKeys": "파티션 키",
- "partitionKeysPreview": "파티션 키(미리 보기)",
"computedProperties": "계산된 속성",
"containerPolicies": "컨테이너 정책",
"throughputBuckets": "처리량 버킷",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "전역 보조 인덱스",
"maskingPolicyPreview": "마스킹 정책"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "{{partitionKeyName}} 변경",
+ "confirmCancel1": "다음 복사 작업을 취소하려고 합니다.",
+ "confirmCancel2": "취소하면 작업이 즉시 중단됩니다.",
+ "confirmComplete1": "다음 복사 작업을 완료하려고 합니다.",
+ "confrimComplete2": "완료되면 보류 중인 문서가 모두 처리된 후 연속 데이터 복사가 중지됩니다. 데이터 무결성을 유지하려면 작업을 완료하기 전에 원본 컨테이너에 대한 업데이트를 중지하는 것이 좋습니다.",
"currentPartitionKey": "현재 {{partitionKeyName}}",
"partitioning": "분할",
"hierarchical": "계층적",
@@ -1033,6 +1040,7 @@
"description": "온라인 컨테이너 복사 작업을 사용하면 [모든 버전 및 삭제](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview) 변경 피드를 통해 원본 컨테이너의 데이터를 대상 Cosmos DB NoSQL API 컨테이너로 복사할 수 있습니다. 이렇게 하면 데이터가 복사되는 동안에도 원본에서 업데이트를 계속할 수 있습니다. 클라이언트 애플리케이션을 대상 컨테이너로 안전하게 전환하려면 마지막에 잠시 가동 중지 시간이 필요합니다. [온라인 복사 작업](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)에서 자세히 알아보세요."
}
},
+ "migrationTypeTitle": "마이그레이션 유형",
"selectContainers": {
"description": "복사할 원본 컨테이너와 대상 컨테이너를 선택하세요.",
"sourceContainerSubHeading": "원본 컨테이너",
@@ -1117,7 +1125,8 @@
"buttonText": "온라인 복사 사용",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "모든 버전 및 삭제 변경 피드 모드(미리 보기) 유효성 검사 중...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "모든 버전 및 삭제 변경 피드 모드(미리 보기) 사용 설정 중...",
- "enablingOnlineCopySpinnerLabel": "'{{accountName}}' 계정에서 온라인 복사를 사용하도록 설정하는 중..."
+ "enablingOnlineCopySpinnerLabel": "'{{accountName}}' 계정에서 온라인 복사를 사용하도록 설정하는 중...",
+ "onlineMigrationPrerequisitesMessage": "계속하기 전에 온라인 마이그레이션 사전 요구 사항을 사용하도록 설정해야 합니다."
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "확인",
"cancelButtonText": "취소"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "데이터 전송 작업 \"{{jobName}}\"이(가) 취소되었습니다.",
+ "completedConsoleMessage": "데이터 전송 작업 \"{{jobName}}\"이(가) 완료되었습니다.",
+ "defaultErrorMessage": "작업을 완료할 수 없음",
+ "errorConsoleMessage": "데이터 전송 작업 \"{{jobName}}\" 실패: {{errorMessage}}",
+ "pauseConsoleMessage": "데이터 전송 작업 \"{{jobName}}\"이(가) 일시 중지되었습니다.",
+ "retryConsoleMessage": "데이터 전송 작업 \"{{jobName}}\"이 진행 중입니다. 총 개수: {{totalCount}}, 처리된 개수: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/nl/Resources.json b/src/Localization/nl/Resources.json
index 3f1125b6e..4e0ff3622 100644
--- a/src/Localization/nl/Resources.json
+++ b/src/Localization/nl/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "Door de gebruiker gedefinieerde functie verwijderen"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "Tabblad Dupliceren",
+ "closeTab": "Tabblad sluiten"
+ },
"documents": {
"newItem": "Nieuw item",
"newDocument": "Nieuw document",
@@ -760,11 +764,10 @@
"settings": "Instellingen",
"indexingPolicy": "Indexeringsbeleid",
"partitionKeys": "Partitiesleutels",
- "partitionKeysPreview": "Partitiesleutels (preview)",
"computedProperties": "Berekende eigenschappen",
"containerPolicies": "Containerbeleidsregels",
"throughputBuckets": "Doorvoerbuckets",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "Globale secundaire index",
"maskingPolicyPreview": "Beleid voor maskering"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "{{partitionKeyName}} wijzigen",
+ "confirmCancel1": "U staat op het punt de volgende kopieertaak te annuleren.",
+ "confirmCancel2": "Annuleren stopt de taak onmiddellijk.",
+ "confirmComplete1": "U staat op het punt de volgende kopieertaak te voltooien.",
+ "confrimComplete2": "Zodra dit is voltooid, stopt het continu kopiëren van gegevens nadat eventuele documenten in behandeling zijn verwerkt. Om de gegevensintegriteit te behouden, raden we u aan updates voor de broncontainer te stoppen voordat u de taak voltooit.",
"currentPartitionKey": "Huidige {{partitionKeyName}}",
"partitioning": "Partitionering",
"hierarchical": "Hiërarchisch",
@@ -1033,6 +1040,7 @@
"description": "Met online containerkopietaken kunt u gegevens kopiëren van een broncontainer naar een bestemming Cosmos DB NoSQL API-container met behulp van de wijzigingenfeed [Alle versies en verwijderen](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview) change feed. Hierdoor kunnen updates worden voortgezet op de bron terwijl gegevens worden gekopieerd. Aan het einde is een korte downtime vereist om veilig over te schakelen van clienttoepassingen naar de doelcontainer. Meer informatie over [online kopieertaken](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
+ "migrationTypeTitle": "Migratietype",
"selectContainers": {
"description": "Selecteer een broncontainer en een doelcontainer waarnaar u wilt kopiëren.",
"sourceContainerSubHeading": "Broncontainer",
@@ -1117,7 +1125,8 @@
"buttonText": "Online kopiëren inschakelen",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Modus Alle versies en verwijderingen voor wijzigingenfeed (preview) valideren...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Inschakelen van Alle versies en verwijderen van de wijzigingenfeed (preview)",
- "enablingOnlineCopySpinnerLabel": "Online kopiëren inschakelen voor uw {{accountName}}-account... ..."
+ "enablingOnlineCopySpinnerLabel": "Online kopiëren inschakelen voor uw {{accountName}}-account... ...",
+ "onlineMigrationPrerequisitesMessage": "Vereisten voor online migratie moeten zijn ingeschakeld voordat u verdergaat."
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "Bevestigen",
"cancelButtonText": "Annuleren"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "Gegevensoverdrachtstaak \"{{jobName}}\" geannuleerd",
+ "completedConsoleMessage": "Gegevensoverdrachtstaak \"{{jobName}}\" voltooid",
+ "defaultErrorMessage": "De bewerking kan niet worden voltooid",
+ "errorConsoleMessage": "Gegevensoverdrachtstaak \"{{jobName}}\" mislukt: {{errorMessage}}",
+ "pauseConsoleMessage": "Gegevensoverdrachtstaak \"{{jobName}}\" onderbroken",
+ "retryConsoleMessage": "Gegevensoverdrachtstaak \"{{jobName}}\" wordt uitgevoerd, totaal aantal: {{totalCount}}, verwerkt aantal: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/pl/Resources.json b/src/Localization/pl/Resources.json
index b5d502ff3..4f60ce2e7 100644
--- a/src/Localization/pl/Resources.json
+++ b/src/Localization/pl/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "Usuń funkcję zdefiniowaną przez użytkownika"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "Duplikuj kartę",
+ "closeTab": "Zamknij kartę"
+ },
"documents": {
"newItem": "Nowy element",
"newDocument": "Nowy dokument",
@@ -760,11 +764,10 @@
"settings": "Ustawienia",
"indexingPolicy": "Zasady indeksowania",
"partitionKeys": "Klucze partycji",
- "partitionKeysPreview": "Klucze partycji (wersja zapoznawcza)",
"computedProperties": "Właściwości obliczane",
"containerPolicies": "Zasady kontenera",
"throughputBuckets": "Zasobniki przepływności",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "Globalny indeks pomocniczy",
"maskingPolicyPreview": "Zasady maskowania"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "Zmień element {{partitionKeyName}}",
+ "confirmCancel1": "Zamierzasz anulować następujące zadanie kopiowania.",
+ "confirmCancel2": "Anulowanie spowoduje natychmiastowe zatrzymanie zadania.",
+ "confirmComplete1": "Zamierzasz ukończyć następujące zadanie kopiowania.",
+ "confrimComplete2": "Po zakończeniu ciągłe kopiowanie danych zostanie zatrzymane po przetworzeniu wszystkich oczekujących dokumentów. Aby zachować integralność danych, zalecamy zatrzymanie aktualizacji kontenera źródłowego przed ukończeniem zadania.",
"currentPartitionKey": "Bieżący {{partitionKeyName}}",
"partitioning": "Partycjonowanie",
"hierarchical": "Hierarchiczne",
@@ -1033,6 +1040,7 @@
"description": "Zadania kopiowania kontenerów online umożliwiają kopiowanie danych z kontenera źródłowego do docelowego kontenera interfejsu API NoSQL usługi Cosmos DB przy użyciu zestawienia zmian [Wszystkie wersje i usuwanie](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview). Umożliwia to kontynuowanie aktualizacji w źródle podczas kopiowania danych. Na końcu jest wymagany krótki przestój, aby bezpiecznie przełączyć aplikacje klienckie do kontenera docelowego. Dowiedz się więcej o [zadaniach kopiowania online](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
+ "migrationTypeTitle": "Typ migracji",
"selectContainers": {
"description": "Wybierz kontener źródłowy i kontener docelowy do skopiowania.",
"sourceContainerSubHeading": "Kontener źródłowy",
@@ -1117,7 +1125,8 @@
"buttonText": "Włącz kopiowanie online",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Trwa weryfikacja wszystkich wersji i usunięć w trybie zestawienia zmian (wersja zapoznawcza)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Trwa włączanie wszystkich wersji i usunięć w trybie zestawienia zmian (wersja zapoznawcza)...",
- "enablingOnlineCopySpinnerLabel": "Trwa włączanie kopii online na koncie „{{accountName}}”..."
+ "enablingOnlineCopySpinnerLabel": "Trwa włączanie kopii online na koncie „{{accountName}}”...",
+ "onlineMigrationPrerequisitesMessage": "Przed kontynuowaniem należy włączyć wymagania wstępne migracji w trybie online."
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "Potwierdź",
"cancelButtonText": "Anuluj"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "Anulowano zadanie transferu danych „{{jobName}}”",
+ "completedConsoleMessage": "Ukończono zadanie transferu danych „{{jobName}}”",
+ "defaultErrorMessage": "Nie można ukończyć operacji",
+ "errorConsoleMessage": "Zadanie transferu danych „{{jobName}}” nie powiodło się: {{errorMessage}}",
+ "pauseConsoleMessage": "Wstrzymano zadanie transferu danych „{{jobName}}”",
+ "retryConsoleMessage": "Zadanie transferu danych „{{jobName}}” jest w toku, łączna liczba: {{totalCount}}, liczba przetworzonych danych: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/pt-BR/Resources.json b/src/Localization/pt-BR/Resources.json
index 23bc14da7..651304c45 100644
--- a/src/Localization/pt-BR/Resources.json
+++ b/src/Localization/pt-BR/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "Excluir Função Definida pelo Usuário"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "Guia Duplicata",
+ "closeTab": "Fechar guia"
+ },
"documents": {
"newItem": "Novo Item",
"newDocument": "Novo Documento",
@@ -760,11 +764,10 @@
"settings": "Configurações",
"indexingPolicy": "Política de Indexação",
"partitionKeys": "Chaves de Partição",
- "partitionKeysPreview": "Chaves de Partição (versão prévia)",
"computedProperties": "Propriedades Calculadas",
"containerPolicies": "Políticas de Contêiner",
"throughputBuckets": "Buckets de Taxa de Transferência",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "Índice Secundário Global",
"maskingPolicyPreview": "Política de Mascaramento"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "Alterar {{partitionKeyName}}",
+ "confirmCancel1": "Você está prestes a cancelar o trabalho de cópia a seguir.",
+ "confirmCancel2": "O cancelamento interromperá o trabalho imediatamente.",
+ "confirmComplete1": "Você está prestes a concluir o seguinte trabalho de cópia.",
+ "confrimComplete2": "Após a conclusão, a cópia de dados contínua será interrompida depois que todos os documentos pendentes forem processados. Para manter a integridade dos dados, recomendamos interromper as atualizações para o contêiner de origem antes de concluir o trabalho.",
"currentPartitionKey": "{{partitionKeyName}} atual",
"partitioning": "Particionamento",
"hierarchical": "Hierárquico",
@@ -1033,6 +1040,7 @@
"description": "Os trabalhos de cópia online de contêiner permitem copiar dados de um contêiner de origem para um contêiner de destino da API NoSQL do Cosmos DB usando o feed de alterações [Todas as Versões e Excluir](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview). Isso permite que as atualizações continuem na fonte enquanto os dados são copiados. Um breve tempo de inatividade é necessário no final para alternar aplicativos cliente com segurança para o contêiner de destino. Saiba mais sobre [trabalhos de cópia online](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
+ "migrationTypeTitle": "Tipo de migração",
"selectContainers": {
"description": "Selecione um contêiner de origem e um contêiner de destino para o qual copiar.",
"sourceContainerSubHeading": "Contêiner de origem",
@@ -1117,7 +1125,8 @@
"buttonText": "Habilitar Cópia Online",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Validando Todas as versões e exclui o modo de feed de alterações (versão prévia)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Habilitando Todas as versões e excluindo o modo de feed de alterações (versão prévia)...",
- "enablingOnlineCopySpinnerLabel": "Habilitando a cópia online em sua conta \"{{accountName}}\"..."
+ "enablingOnlineCopySpinnerLabel": "Habilitando a cópia online em sua conta \"{{accountName}}\"...",
+ "onlineMigrationPrerequisitesMessage": "Os pré-requisitos de migração online devem ser habilitados antes de continuar."
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "Confirmar",
"cancelButtonText": "Cancelar"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "Trabalho de transferência de dados \"{{jobName}}\" cancelado",
+ "completedConsoleMessage": "Trabalho de transferência de dados \"{{jobName}}\" concluído",
+ "defaultErrorMessage": "A operação não pôde ser concluída",
+ "errorConsoleMessage": "Falha no trabalho de transferência de dados \"{{jobName}}\": {{errorMessage}}",
+ "pauseConsoleMessage": "Trabalho de transferência de dados \"{{jobName}}\" em pausa",
+ "retryConsoleMessage": "Trabalho de transferência de dados \"{{jobName}}\" em andamento, contagem total: {{totalCount}}, contagem processada: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/pt-PT/Resources.json b/src/Localization/pt-PT/Resources.json
index dad4222dc..b4fdbdec0 100644
--- a/src/Localization/pt-PT/Resources.json
+++ b/src/Localization/pt-PT/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "Eliminar Função Definida pelo Utilizador"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "Duplicar separador",
+ "closeTab": "Fechar separador"
+ },
"documents": {
"newItem": "Novo Item",
"newDocument": "Novo Documento",
@@ -760,11 +764,10 @@
"settings": "Definições",
"indexingPolicy": "Política de Indexação",
"partitionKeys": "Chaves de Partição",
- "partitionKeysPreview": "Chaves de Partição (pré-visualização)",
"computedProperties": "Propriedades Calculadas",
"containerPolicies": "Políticas de Contentor",
"throughputBuckets": "Registos de Débito",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "Índice secundário global",
"maskingPolicyPreview": "Política de Mascaramento"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "Alterar {{partitionKeyName}}",
+ "confirmCancel1": "Está prestes a cancelar a seguinte tarefa de cópia.",
+ "confirmCancel2": "O cancelamento interromperá a tarefa imediatamente.",
+ "confirmComplete1": "Está prestes a concluir a seguinte tarefa de cópia.",
+ "confrimComplete2": "Quando estiver concluído, a cópia contínua de dados irá parar após serem processados quaisquer documentos pendentes. Para manter a integridade dos dados, recomendamos que interrompa as atualizações no contentor de origem antes de concluir a tarefa.",
"currentPartitionKey": "{{partitionKeyName}} atual",
"partitioning": "Criação de Partições",
"hierarchical": "Hierárquico",
@@ -1033,6 +1040,7 @@
"description": "As tarefas de cópia de contentores online permitem-lhe copiar dados de um contentor de origem para um contentor de destino da API NoSQL do Cosmos DB usando o feed de alterações [All Versions and Delete](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview). Isto permite que as atualizações continuem na origem enquanto os dados são copiados. É necessário um breve período de inatividade no final para mudar em segurança as aplicações cliente para o contentor de destino. Saiba mais sobre [online copy jobs](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
+ "migrationTypeTitle": "Tipo de migração",
"selectContainers": {
"description": "Selecione um contentor de origem e um contentor de destino para o qual copiar.",
"sourceContainerSubHeading": "Contentor de origem",
@@ -1117,7 +1125,8 @@
"buttonText": "Ativar Cópia Online",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "A validar o modo de feed de alterações para todas as versões e eliminações (pré-visualização)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "A ativar o modo de feed de alterações para todas as versões e eliminações (pré-visualização)...",
- "enablingOnlineCopySpinnerLabel": "A ativar a cópia online na sua conta \"{{accountName}}\"..."
+ "enablingOnlineCopySpinnerLabel": "A ativar a cópia online na sua conta \"{{accountName}}\"...",
+ "onlineMigrationPrerequisitesMessage": "Os pré-requisitos da migração online têm de estar ativados antes de continuar."
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "Confirmar",
"cancelButtonText": "Cancelar"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "Tarefa de transferência de dados \"{{jobName}}\" cancelada",
+ "completedConsoleMessage": "Tarefa de transferência de dados \"{{jobName}}\" concluída",
+ "defaultErrorMessage": "Não foi possível concluir a operação",
+ "errorConsoleMessage": "Tarefa de transferência de dados \"{{jobName}}\" falhou: {{errorMessage}}",
+ "pauseConsoleMessage": "Tarefa de transferência de dados \"{{jobName}}\" em pausa",
+ "retryConsoleMessage": "Tarefa de transferência de dados \"{{jobName}}\" em curso, contagem total: {{totalCount}}, contagem processada: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/ru/Resources.json b/src/Localization/ru/Resources.json
index 6f7ba298e..8804680d7 100644
--- a/src/Localization/ru/Resources.json
+++ b/src/Localization/ru/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "Удалить пользовательскую функцию"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "Дублировать вкладку",
+ "closeTab": "Закрыть вкладку"
+ },
"documents": {
"newItem": "Новый элемент",
"newDocument": "Новый документ",
@@ -760,11 +764,10 @@
"settings": "Параметры",
"indexingPolicy": "Политика индексации",
"partitionKeys": "Ключи разделов",
- "partitionKeysPreview": "Ключи разделов (предварительная версия)",
"computedProperties": "Вычисленные свойства",
"containerPolicies": "Правила перевозки контейнеров",
"throughputBuckets": "Группы пропускной способности",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "Глобальный вторичный индекс",
"maskingPolicyPreview": "Политика маскирования"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "Изменить {{partitionKeyName}}",
+ "confirmCancel1": "Вы собираетесь отменить следующее задание копирования.",
+ "confirmCancel2": "Отмена немедленно остановит выполнение задания.",
+ "confirmComplete1": "Вы собираетесь выполнить следующее задание копирования.",
+ "confrimComplete2": "По выполнении непрерывное копирование данных остановится после обработки всех ожидающих документов. Для обеспечения целостности данных мы рекомендуем остановить обновление исходного контейнера до выполнения задачи.",
"currentPartitionKey": "Текущий {{partitionKeyName}}",
"partitioning": "Секционирование",
"hierarchical": "Иерархический",
@@ -1033,6 +1040,7 @@
"description": "Онлайн-задания копирования контейнеров позволяют копировать данные из исходного контейнера в целевой контейнер API NoSQL Cosmos DB с помощью канала изменений [Все версии и удаление](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview). Это позволяет продолжать обновление в источнике во время копирования данных. В конце потребуется кратковременный простой, чтобы безопасно переключить клиентские приложения на целевой контейнер. Подробнее об [заданиях онлайн-копирования](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
+ "migrationTypeTitle": "Тип миграции",
"selectContainers": {
"description": "Выберите исходный и целевой контейнер для копирования.",
"sourceContainerSubHeading": "Исходный контейнер",
@@ -1117,7 +1125,8 @@
"buttonText": "Включить онлайн-копирование",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Проверка режима канала изменений для всех версий и удалений (предварительная версия)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Включение режима канала изменений для всех версий и удалений (предварительная версия)...",
- "enablingOnlineCopySpinnerLabel": "Включение онлайн-копирования для вашей учетной записи \"{{accountName}}\"..."
+ "enablingOnlineCopySpinnerLabel": "Включение онлайн-копирования для вашей учетной записи \"{{accountName}}\"...",
+ "onlineMigrationPrerequisitesMessage": "Перед продолжением необходимо включить предварительные условия для веб-миграции."
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "Подтвердить",
"cancelButtonText": "Отмена"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "Задание передачи данных \"{{jobName}}\" отменено",
+ "completedConsoleMessage": "Задание передачи данных \"{{jobName}}\" выполнено",
+ "defaultErrorMessage": "Не удалось завершить операцию",
+ "errorConsoleMessage": "Сбой задания передачи данных \"{{jobName}}\": {{errorMessage}}",
+ "pauseConsoleMessage": "Задание передачи данных \"{{jobName}}\" приостановлено",
+ "retryConsoleMessage": "Задание передачи данных \"{{jobName}}\" выполняется. Всего: {{totalCount}}, обработано: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/sv/Resources.json b/src/Localization/sv/Resources.json
index f7a27bff1..46bde741c 100644
--- a/src/Localization/sv/Resources.json
+++ b/src/Localization/sv/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "Ta bort användardefinierad funktion"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "Dubblettflik",
+ "closeTab": "Stäng flik"
+ },
"documents": {
"newItem": "Nytt objekt",
"newDocument": "Nytt dokument",
@@ -760,11 +764,10 @@
"settings": "Inställningar",
"indexingPolicy": "Indexeringspolicy",
"partitionKeys": "Partitionsnycklar",
- "partitionKeysPreview": "Partitionsnycklar (förhandsversion)",
"computedProperties": "Beräknade egenskaper",
"containerPolicies": "Containerprinciper",
"throughputBuckets": "Dataflödesbuckets",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "Globalt sekundärt index",
"maskingPolicyPreview": "Maskeringsprincip"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "Ändra {{partitionKeyName}}",
+ "confirmCancel1": "Du håller på att avbryta följande kopieringsjobb.",
+ "confirmCancel2": "Om du avbryter stoppas jobbet omedelbart.",
+ "confirmComplete1": "Du är på väg att slutföra följande kopieringsjobb.",
+ "confrimComplete2": "När detta är klart stoppas kontinuerlig datakopiering när eventuella väntande dokument har bearbetats. För att bevara dataintegriteten rekommenderar vi att du stoppar uppdateringar av källcontainern innan du slutför jobbet.",
"currentPartitionKey": "Nuvarande {{partitionKeyName}}",
"partitioning": "Partitionering",
"hierarchical": "Hierarkiskt",
@@ -1033,6 +1040,7 @@
"description": "Med online-containerkopieringsjobb kan du kopiera data från en källcontainer till ett mål Cosmos DB NoSQL API-containern med ändringsflödet [Alla versioner och ta bort](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview). Detta gör att uppdateringar kan fortsätta på källan medan data kopieras. En kort stilleståndstid krävs i slutet för att på ett säkert sätt växla över klientprogram till målcontainern. Läs mer om [onlinekopieringsjobb](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)."
}
},
+ "migrationTypeTitle": "Migreringstyp",
"selectContainers": {
"description": "Välj en källcontainer och en målcontainer att kopiera till.",
"sourceContainerSubHeading": "Källcontainer",
@@ -1117,7 +1125,8 @@
"buttonText": "Aktivera onlinekopiering",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Verifierar alla versioner och tar bort ändringsflödesläge (förhandsversion)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Aktiverar alla versioner och tar bort ändringsflödesläge (förhandsversion)...",
- "enablingOnlineCopySpinnerLabel": "Aktiverar onlinekopiering på ditt {{accountName}}-konto..."
+ "enablingOnlineCopySpinnerLabel": "Aktiverar onlinekopiering på ditt {{accountName}}-konto...",
+ "onlineMigrationPrerequisitesMessage": "Förutsättningarna för onlinemigrering måste vara aktiverade innan du fortsätter."
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "Bekräfta",
"cancelButtonText": "Avbryt"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "Dataöverföringsjobbet {{jobName}} avbröts",
+ "completedConsoleMessage": "Dataöverföringsjobbet {{jobName}} slutfördes",
+ "defaultErrorMessage": "Det gick inte att slutföra åtgärden",
+ "errorConsoleMessage": "Dataöverföringsjobbet {{jobName}} misslyckades: {{errorMessage}}",
+ "pauseConsoleMessage": "Dataöverföringsjobbet {{jobName}} pausades",
+ "retryConsoleMessage": "Dataöverföringsjobbet {{jobName}} pågår, totalt antal: {{totalCount}}, bearbetat antal: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/tr/Resources.json b/src/Localization/tr/Resources.json
index 3baa34132..bb38de8df 100644
--- a/src/Localization/tr/Resources.json
+++ b/src/Localization/tr/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "Kullanıcı Tanımlı İşlevi Sil"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "Sekmeyi çoğalt",
+ "closeTab": "Sekmeyi kapat"
+ },
"documents": {
"newItem": "Yeni Öğe",
"newDocument": "Yeni Belge",
@@ -760,11 +764,10 @@
"settings": "Ayarlar",
"indexingPolicy": "Dizin Oluşturma İlkesi",
"partitionKeys": "Bölüm Anahtarları",
- "partitionKeysPreview": "Bölüm Anahtarları (önizleme)",
"computedProperties": "Hesaplanan Özellikler",
"containerPolicies": "Kapsayıcı İlkeleri",
"throughputBuckets": "Aktarım Hızı Demetleri",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "Global İkincil Endeks",
"maskingPolicyPreview": "Veri Maskeleme İlkesi"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "{{partitionKeyName}} Değiştir",
+ "confirmCancel1": "Aşağıdaki kopyalama işini iptal etmek için varsınız.",
+ "confirmCancel2": "İptal etme işi hemen durdurur.",
+ "confirmComplete1": "Aşağıdaki kopyalama işini tamamlamak üzeresiniz.",
+ "confrimComplete2": "Tamamlandığında, bekleyen belgeler işlendiğinde sürekli veri kopyalama durur. Veri bütünlüğünü korumak için işi tamamlamadan önce kaynak kapsayıcıdaki güncelleştirmeleri durdurmanızı öneririz.",
"currentPartitionKey": "Geçerli {{partitionKeyName}}",
"partitioning": "Bölümleme",
"hierarchical": "Hiyerarşik",
@@ -1033,6 +1040,7 @@
"description": "Çevrimiçi kapsayıcı kopyalama işleri, [Tüm Sürümler ve Sil](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview) değişiklik akışını kullanarak kaynak kapsayıcıdan hedef Cosmos DB NoSQL API kapsayıcısına veri kopyalamanızı sağlar. Bu, veriler kopyalanırken kaynakta güncelleştirmelerin devam etmesini sağlar. İstemci uygulamalarını güvenli bir şekilde hedef kapsayıcıya geçirmek için sonunda kısa bir kesinti süresi gerekir. [Çevrimiçi kopyalama işleri](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started) hakkında daha fazla bilgi edinin."
}
},
+ "migrationTypeTitle": "Geçiş türü",
"selectContainers": {
"description": "Lütfen kopyalamak için bir kaynak kapsayıcı ve bir hedef kapsayıcı seçin.",
"sourceContainerSubHeading": "Kaynak kapsayıcı",
@@ -1117,7 +1125,8 @@
"buttonText": "Çevrimiçi Kopyalamayı Etkinleştir",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "Tüm sürümler ve silmeler için değişiklik akışı modu doğrulanıyor (önizleme)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "Tüm sürümler ve silmeler için değişiklik akışı modu etkinleştiriliyor (önizleme)...",
- "enablingOnlineCopySpinnerLabel": "\"{{accountName}}\" hesabınızda çevrimiçi kopyalama etkinleştiriliyor ..."
+ "enablingOnlineCopySpinnerLabel": "\"{{accountName}}\" hesabınızda çevrimiçi kopyalama etkinleştiriliyor ...",
+ "onlineMigrationPrerequisitesMessage": "Devam etmeden önce çevrimiçi geçiş önkoşullarının etkinleştirilmiş olması gerekir."
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "Onayla",
"cancelButtonText": "İptal"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "\"{{jobName}}\" veri aktarım işi iptal edildi",
+ "completedConsoleMessage": "\"{{jobName}}\" veri aktarım işi tamamlandı",
+ "defaultErrorMessage": "İşlem tamamlanamadı",
+ "errorConsoleMessage": "\"{{jobName}}\" veri aktarım işi başarısız oldu: {{errorMessage}}",
+ "pauseConsoleMessage": "\"{{jobName}}\" veri aktarım işi duraklatıldı",
+ "retryConsoleMessage": "Veri aktarım işi \"{{jobName}}\" sürüyor, toplam sayı: {{totalCount}}, işlenen sayı: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/zh-Hans/Resources.json b/src/Localization/zh-Hans/Resources.json
index 389e0b179..ed29243a0 100644
--- a/src/Localization/zh-Hans/Resources.json
+++ b/src/Localization/zh-Hans/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "删除用户定义函数"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "复制选项卡",
+ "closeTab": "关闭选项卡"
+ },
"documents": {
"newItem": "新建项目",
"newDocument": "新建文档",
@@ -760,11 +764,10 @@
"settings": "设置",
"indexingPolicy": "索引策略",
"partitionKeys": "分区键",
- "partitionKeysPreview": "分区键(预览版)",
"computedProperties": "计算属性",
"containerPolicies": "容器策略",
"throughputBuckets": "吞吐量存储桶",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "全局辅助索引",
"maskingPolicyPreview": "掩码策略"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "更改 {{partitionKeyName}}",
+ "confirmCancel1": "即将取消以下复制作业。",
+ "confirmCancel2": "取消会立即停止该作业。",
+ "confirmComplete1": "即将完成以下复制作业。",
+ "confrimComplete2": "完成后,连续数据复制将在处理完所有待处理文档后停止。为保持数据完整性,建议在完成作业前先停止对源容器的更新。",
"currentPartitionKey": "当前 {{partitionKeyName}}",
"partitioning": "分区",
"hierarchical": "分层",
@@ -1033,6 +1040,7 @@
"description": "借助联机容器复制作业,可以使用 [所有版本和删除](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview) 更改源将数据从源容器复制到目标 Cosmos DB NoSQL API 容器。此操作允许在复制数据时在源上继续更新。最后,需要短暂停机才能安全地将客户端应用程序切换到目标容器。详细了解 [联机复制作业](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)。"
}
},
+ "migrationTypeTitle": "迁移类型",
"selectContainers": {
"description": "请选择源容器和要复制到的目标容器。",
"sourceContainerSubHeading": "源容器",
@@ -1117,7 +1125,8 @@
"buttonText": "启用联机复制",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "正在验证所有版本和删除更改源模式(预览)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "正在启用所有版本和删除更改源模式(预览)...",
- "enablingOnlineCopySpinnerLabel": "正在“{{accountName}}”帐户上启用联机复制..."
+ "enablingOnlineCopySpinnerLabel": "正在“{{accountName}}”帐户上启用联机复制...",
+ "onlineMigrationPrerequisitesMessage": "必须先启用联机迁移先决条件,然后才能继续。"
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "确认",
"cancelButtonText": "取消"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "数据传输作业“{{jobName}}”已取消",
+ "completedConsoleMessage": "数据传输作业“{{jobName}}”已完成",
+ "defaultErrorMessage": "无法完成操作",
+ "errorConsoleMessage": "数据传输作业“{{jobName}}”失败: {{errorMessage}}",
+ "pauseConsoleMessage": "数据传输作业“{{jobName}}”已暂停",
+ "retryConsoleMessage": "数据传输作业“{{jobName}}”正在进行中,总计数: {{totalCount}},已处理计数: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Localization/zh-Hant/Resources.json b/src/Localization/zh-Hant/Resources.json
index 2a93a3ffc..32780cc62 100644
--- a/src/Localization/zh-Hant/Resources.json
+++ b/src/Localization/zh-Hant/Resources.json
@@ -316,6 +316,10 @@
"deleteUdf": "刪除使用者定義的函式"
},
"tabs": {
+ "tabMenu": {
+ "duplicateTab": "重複的索引標籤",
+ "closeTab": "關閉索引標籤"
+ },
"documents": {
"newItem": "新增項目",
"newDocument": "新增文件",
@@ -760,11 +764,10 @@
"settings": "設定",
"indexingPolicy": "編製索引原則",
"partitionKeys": "資料分割索引鍵",
- "partitionKeysPreview": "磁碟分割索引鍵 (預覽)",
"computedProperties": "計算屬性",
"containerPolicies": "容器原則",
"throughputBuckets": "輸送量貯體",
- "globalSecondaryIndexPreview": "Global Secondary Index",
+ "globalSecondaryIndexPreview": "全域從屬索引",
"maskingPolicyPreview": "遮罩原則"
},
"mongoNotifications": {
@@ -895,6 +898,10 @@
},
"partitionKeyEditor": {
"changePartitionKey": "變更 {{partitionKeyName}}",
+ "confirmCancel1": "您即將取消下列複製工作。",
+ "confirmCancel2": "取消將立即停止作業。",
+ "confirmComplete1": "您即將完成下列複製作業。",
+ "confrimComplete2": "完成後,持續資料複製會在處理完任何擱置的文件後停止。為了維持資料完整性,建議您在完成作業前先停止對來源容器的更新。",
"currentPartitionKey": "目前的 {{partitionKeyName}}",
"partitioning": "資料分割",
"hierarchical": "階層式",
@@ -1033,6 +1040,7 @@
"description": "線上容器複製工作可讓您使用 [所有版本和刪除](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview) 變更摘要,將資料從來源容器複製到目的地 Cosmos DB NoSQL API 容器。這可讓您在複製資料的同時,繼續對來源進行更新。最後需要短暫停機,才能安全地將用戶端應用程式切換到目的地容器。深入了解 [線上複製工作](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started)。"
}
},
+ "migrationTypeTitle": "移轉類型",
"selectContainers": {
"description": "請選取來源容器和要複製到的目的地容器。",
"sourceContainerSubHeading": "來源容器",
@@ -1117,7 +1125,8 @@
"buttonText": "啟用線上複製",
"validateAllVersionsAndDeletesChangeFeedSpinnerLabel": "驗證所有版本與刪除的變更摘要模式 (預覽)...",
"enablingAllVersionsAndDeletesChangeFeedSpinnerLabel": "啟用所有版本與刪除的變更摘要模式 (預覽)...",
- "enablingOnlineCopySpinnerLabel": "正在啟用您「{{accountName}}」帳戶的線上複製..."
+ "enablingOnlineCopySpinnerLabel": "正在啟用您「{{accountName}}」帳戶的線上複製...",
+ "onlineMigrationPrerequisitesMessage": "必須先啟用線上移轉必要條件,才能繼續。"
},
"monitorJobs": {
"columns": {
@@ -1152,6 +1161,16 @@
"confirmButtonText": "確認",
"cancelButtonText": "取消"
}
+ },
+ "dataTransfers": {
+ "polling": {
+ "cancelConsoleMessage": "資料傳輸作業「{{jobName}}」已取消",
+ "completedConsoleMessage": "資料傳輸作業「{{jobName}}」已完成",
+ "defaultErrorMessage": "作業無法完成",
+ "errorConsoleMessage": "資料傳輸作業「{{jobName}}」已失敗: {{errorMessage}}",
+ "pauseConsoleMessage": "資料傳輸作業「{{jobName}}」已暫停",
+ "retryConsoleMessage": "資料傳輸作業「{{jobName}}」進行中,總計數: {{totalCount}},已處理計數: {{processedCount}}"
+ }
}
}
}
\ No newline at end of file
From fc03bb342324f41624e4357c18003bb60367ee36 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 10 Sep 2026 14:20:42 -0700
Subject: [PATCH 11/16] Bump @xmldom/xmldom from 0.8.13 to 0.8.15 (#2584)
Bumps [@xmldom/xmldom](https://github.com/xmldom/xmldom) from 0.8.13 to 0.8.15.
- [Release notes](https://github.com/xmldom/xmldom/releases)
- [Changelog](https://github.com/xmldom/xmldom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/xmldom/xmldom/compare/0.8.13...0.8.15)
---
updated-dependencies:
- dependency-name: "@xmldom/xmldom"
dependency-version: 0.8.15
dependency-type: direct:production
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 8 ++++----
package.json | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index b7e080993..a6801adf5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,7 +26,7 @@
"@types/lodash": "4.14.171",
"@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.6.13",
- "@xmldom/xmldom": "0.8.13",
+ "@xmldom/xmldom": "0.8.15",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0",
"allotment": "1.20.2",
@@ -8784,9 +8784,9 @@
}
},
"node_modules/@xmldom/xmldom": {
- "version": "0.8.13",
- "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
- "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
+ "version": "0.8.15",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz",
+ "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
diff --git a/package.json b/package.json
index 832342c01..610b13075 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
"@types/lodash": "4.14.171",
"@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.6.13",
- "@xmldom/xmldom": "0.8.13",
+ "@xmldom/xmldom": "0.8.15",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0",
"allotment": "1.20.2",
From b16229d1f839090e867e4b6e4f0fa899fb49a9f1 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 11 Sep 2026 11:16:12 -0700
Subject: [PATCH 12/16] Bump browserslist from 4.28.1 to 4.28.9 (#2585)
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.28.1 to 4.28.9.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.28.1...4.28.9)
---
updated-dependencies:
- dependency-name: browserslist
dependency-version: 4.28.8
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-lock.json | 62 ++++++++++++++++++++++++++++-------------------
1 file changed, 37 insertions(+), 25 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index a6801adf5..a0eae3e61 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9815,11 +9815,15 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.9.19",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
- "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
+ "version": "2.11.22",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.22.tgz",
+ "integrity": "sha512-pWc4w51fBFd7mav43/zKRC+RI6f4yfzQoVlfvE8dECePyfkn1bzLp01Fj0QACcyCZyFhiEMyD2qScfKRWgWibA==",
+ "license": "Apache-2.0",
"bin": {
- "baseline-browser-mapping": "dist/cli.js"
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
}
},
"node_modules/batch": {
@@ -10000,9 +10004,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.28.1",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
- "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "version": "4.28.9",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz",
+ "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==",
"funding": [
{
"type": "opencollective",
@@ -10017,12 +10021,13 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
- "baseline-browser-mapping": "^2.9.0",
- "caniuse-lite": "^1.0.30001759",
- "electron-to-chromium": "^1.5.263",
- "node-releases": "^2.0.27",
- "update-browserslist-db": "^1.2.0"
+ "baseline-browser-mapping": "^2.11.20",
+ "caniuse-lite": "^1.0.30001810",
+ "electron-to-chromium": "^1.5.420",
+ "node-releases": "^2.0.54",
+ "update-browserslist-db": "^1.3.2"
},
"bin": {
"browserslist": "cli.js"
@@ -10170,9 +10175,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001766",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz",
- "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==",
+ "version": "1.0.30001810",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
+ "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
"funding": [
{
"type": "opencollective",
@@ -10186,7 +10191,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
- ]
+ ],
+ "license": "CC-BY-4.0"
},
"node_modules/canvas": {
"version": "3.2.1",
@@ -12185,9 +12191,10 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.282",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.282.tgz",
- "integrity": "sha512-FCPkJtpst28UmFzd903iU7PdeVTfY0KAeJy+Lk0GLZRwgwYHn/irRcaCbQQOmr5Vytc/7rcavsYLvTM8RiHYhQ=="
+ "version": "1.5.426",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.426.tgz",
+ "integrity": "sha512-2Gcq6inCQs/AqfHP3f5ftCzk+pqeW2VlA1LgmPqEj2hfBO8HiZRspNYkD4HzFBe4u6lGqca4BspFr6Ix4Q400g==",
+ "license": "ISC"
},
"node_modules/emitter-listener": {
"version": "1.1.2",
@@ -21240,9 +21247,13 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.27",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
- "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="
+ "version": "2.0.55",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz",
+ "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
},
"node_modules/normalize-path": {
"version": "3.0.0",
@@ -25576,9 +25587,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
- "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz",
+ "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==",
"funding": [
{
"type": "opencollective",
@@ -25593,6 +25604,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
From 11959bb7b0d05d78dd97b61d80e6abed8a9ae612 Mon Sep 17 00:00:00 2001
From: Laurent Nguyen
Date: Wed, 16 Sep 2026 14:30:19 +0200
Subject: [PATCH 13/16] Enhance logging and error handling in sdk read
collections api (#2602)
* Enhance logging and error handling in readCollections function
* Add defensive measure to avoid breaking runtime.
---------
Co-authored-by: Laurent Nguyen
---
src/Common/dataAccess/readCollections.test.ts | 97 +++++++++++++++----
src/Common/dataAccess/readCollections.ts | 13 ++-
2 files changed, 91 insertions(+), 19 deletions(-)
diff --git a/src/Common/dataAccess/readCollections.test.ts b/src/Common/dataAccess/readCollections.test.ts
index 50348b574..d3998ecfc 100644
--- a/src/Common/dataAccess/readCollections.test.ts
+++ b/src/Common/dataAccess/readCollections.test.ts
@@ -1,13 +1,39 @@
jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient");
+jest.mock("../Logger");
+jest.mock("../ErrorHandlingUtils", () => ({ handleError: jest.fn() }));
+jest.mock("../../Utils/NotificationConsoleUtils");
+import * as Logger from "Common/Logger";
import { AuthType } from "../../AuthType";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { updateUserContext } from "../../UserContext";
+import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { armRequest } from "../../Utils/arm/request";
import { client } from "../CosmosClient";
+import { handleError } from "../ErrorHandlingUtils";
import { readCollections } from "./readCollections";
describe("readCollections", () => {
+ const clearMessage = jest.fn();
+ const fetchAll = jest.fn();
+ const readAll = jest.fn(() => ({ fetchAll }));
+ const database = jest.fn(() => ({ containers: { readAll } }));
+ const diagnostics = {
+ clientSideRequestStatistics: {
+ requestDurationInMs: 123,
+ locationEndpointsContacted: ["https://test.documents.azure.com"],
+ retryDiagnostics: { failedAttempts: [{ statusCode: 429 }] },
+ },
+ diagnosticNode: { data: { responsePayload: "not-for-logging" } },
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (logConsoleProgress as jest.Mock).mockReturnValue(clearMessage);
+ (client as jest.Mock).mockReturnValue({ database });
+ updateUserContext({ authType: AuthType.MasterKey });
+ });
+
beforeAll(() => {
updateUserContext({
databaseAccount: {
@@ -23,26 +49,61 @@ describe("readCollections", () => {
});
await readCollections("database");
expect(armRequest).toHaveBeenCalled();
+ expect(client).not.toHaveBeenCalled();
+ expect(clearMessage).toHaveBeenCalledTimes(1);
});
- it("should call SDK if not logged in with non-AAD method", async () => {
- updateUserContext({
- authType: AuthType.MasterKey,
+ it("should log SDK request statistics and return collections for non-AAD authentication", async () => {
+ const resources = [{ id: "container" }];
+ fetchAll.mockResolvedValue({ resources, diagnostics });
+
+ await expect(readCollections("database")).resolves.toBe(resources);
+
+ expect(database).toHaveBeenCalledWith("database");
+ expect(readAll).toHaveBeenCalledWith();
+ expect(fetchAll).toHaveBeenCalledTimes(1);
+ expect(Logger.logInfo).toHaveBeenLastCalledWith(
+ expect.stringContaining(`diagnostics=${JSON.stringify(diagnostics.clientSideRequestStatistics)}`),
+ "readCollections",
+ );
+ expect(JSON.stringify(jest.mocked(Logger.logInfo).mock.calls)).not.toContain("not-for-logging");
+ expect(clearMessage).toHaveBeenCalledTimes(1);
+ });
+
+ it("should log SDK failure statistics and rethrow the original error", async () => {
+ const error = Object.assign(new Error("fetchAll failed"), { diagnostics });
+ fetchAll.mockRejectedValue(error);
+
+ await expect(readCollections("database")).rejects.toBe(error);
+
+ expect(Logger.logError).toHaveBeenCalledWith(
+ `readCollections: fetchAll failed for database database, diagnostics=${JSON.stringify(
+ diagnostics.clientSideRequestStatistics,
+ )}`,
+ "readCollections",
+ );
+ expect(handleError).toHaveBeenCalledWith(
+ error,
+ "ReadCollections",
+ "Error while querying containers for database database",
+ );
+ expect(clearMessage).toHaveBeenCalledTimes(1);
+ });
+
+ it("should preserve error handling when diagnostics are unavailable", async () => {
+ const error = new Error("client initialization failed");
+ (client as jest.Mock).mockImplementationOnce(() => {
+ throw error;
});
- (client as jest.Mock).mockReturnValue({
- database: () => {
- return {
- containers: {
- readAll: () => {
- return {
- fetchAll: (): unknown => [],
- };
- },
- },
- };
- },
- });
- await readCollections("database");
- expect(client).toHaveBeenCalled();
+
+ await expect(readCollections("database")).rejects.toBe(error);
+
+ expect(Logger.logError).not.toHaveBeenCalled();
+ expect(handleError).toHaveBeenCalledWith(
+ error,
+ "ReadCollections",
+ "Error while querying containers for database database",
+ );
+ expect(clearMessage).toHaveBeenCalledTimes(1);
});
});
diff --git a/src/Common/dataAccess/readCollections.ts b/src/Common/dataAccess/readCollections.ts
index d90832219..caf317a16 100644
--- a/src/Common/dataAccess/readCollections.ts
+++ b/src/Common/dataAccess/readCollections.ts
@@ -78,7 +78,9 @@ export async function readCollections(databaseId: string): Promise
Date: Wed, 16 Sep 2026 10:24:59 -0700
Subject: [PATCH 14/16] Fix MongoDB 3.6 Cloud Shell compatibility (#2603)
Use the MongoDB 3.6-compatible mongosh package for Mongo server version 3.6 while keeping the newer package for other accounts. This reads serverVersion from account metadata and adds a unit test covering the 3.6 path.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
src/Contracts/DataModels.ts | 5 +++++
.../ShellTypes/AbstractShellHandler.tsx | 15 +++++++++------
.../ShellTypes/MongoShellHandler.test.tsx | 14 ++++++++++++++
.../ShellTypes/MongoShellHandler.tsx | 2 +-
4 files changed, 29 insertions(+), 7 deletions(-)
diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts
index 770ee7948..ad8cbdf38 100644
--- a/src/Contracts/DataModels.ts
+++ b/src/Contracts/DataModels.ts
@@ -39,6 +39,7 @@ export interface DatabaseAccountBackupPolicy {
}
export interface DatabaseAccountExtendedProperties {
+ apiProperties?: DatabaseAccountApiProperties;
documentEndpoint?: string;
disableLocalAuth?: boolean;
tableEndpoint?: string;
@@ -68,6 +69,10 @@ export interface DatabaseAccountExtendedProperties {
enableAllVersionsAndDeletesChangeFeed?: boolean;
}
+export interface DatabaseAccountApiProperties {
+ serverVersion?: string;
+}
+
export interface DatabaseAccountResponseLocation {
documentEndpoint: string;
failoverPriority: number;
diff --git a/src/Explorer/Tabs/CloudShellTab/ShellTypes/AbstractShellHandler.tsx b/src/Explorer/Tabs/CloudShellTab/ShellTypes/AbstractShellHandler.tsx
index 783dcd99b..3834538c3 100644
--- a/src/Explorer/Tabs/CloudShellTab/ShellTypes/AbstractShellHandler.tsx
+++ b/src/Explorer/Tabs/CloudShellTab/ShellTypes/AbstractShellHandler.tsx
@@ -26,6 +26,9 @@ export const EXIT_COMMAND_MONGO = ` printf "\\033[1;31mSession ended. Please clo
*/
export const DISABLE_TELEMETRY_COMMAND = `mongosh --nodb --quiet --eval 'disableTelemetry()'`;
+const MONGOSH_PACKAGE_VERSION = "2.5.6";
+const MONGOSH_36_PACKAGE_VERSION = "1.10.6";
+
/**
* Abstract class that defines the interface for shell-specific handlers
* in the CloudShell terminal implementation. Each supported shell type
@@ -96,14 +99,14 @@ export abstract class AbstractShellHandler {
* Each command runs conditionally only if mongosh
* is not already present in the environment.
*/
- protected mongoShellSetupCommands(): string[] {
- const PACKAGE_VERSION: string = "2.5.6";
+ protected mongoShellSetupCommands(serverVersion?: string): string[] {
+ const packageVersion = serverVersion === "3.6" ? MONGOSH_36_PACKAGE_VERSION : MONGOSH_PACKAGE_VERSION;
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`,
- `if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
- `if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh/bin && mv mongosh-${PACKAGE_VERSION}-linux-x64/bin/mongosh ~/mongosh/bin/ && chmod +x ~/mongosh/bin/mongosh; fi`,
- `if ! command -v mongosh &> /dev/null; then rm -rf mongosh-${PACKAGE_VERSION}-linux-x64 mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`,
+ `if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${packageVersion}-linux-x64.tgz; fi`,
+ `if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-${packageVersion}-linux-x64.tgz; fi`,
+ `if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh/bin && mv mongosh-${packageVersion}-linux-x64/bin/mongosh ~/mongosh/bin/ && chmod +x ~/mongosh/bin/mongosh; fi`,
+ `if ! command -v mongosh &> /dev/null; then rm -rf mongosh-${packageVersion}-linux-x64 mongosh-${packageVersion}-linux-x64.tgz; fi`,
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
"if ! command -v mongosh &> /dev/null; then source ~/.bashrc; fi",
];
diff --git a/src/Explorer/Tabs/CloudShellTab/ShellTypes/MongoShellHandler.test.tsx b/src/Explorer/Tabs/CloudShellTab/ShellTypes/MongoShellHandler.test.tsx
index 28a79d404..dcba243bf 100644
--- a/src/Explorer/Tabs/CloudShellTab/ShellTypes/MongoShellHandler.test.tsx
+++ b/src/Explorer/Tabs/CloudShellTab/ShellTypes/MongoShellHandler.test.tsx
@@ -5,6 +5,9 @@ import { MongoShellHandler } from "./MongoShellHandler";
// Define interfaces for type safety
interface DatabaseAccountProperties {
mongoEndpoint?: string;
+ apiProperties?: {
+ serverVersion?: string;
+ };
}
interface DatabaseAccount {
@@ -80,6 +83,17 @@ describe("MongoShellHandler", () => {
expect(commands.length).toBe(7);
expect(commands[1]).toContain("mongosh-2.5.6-linux-x64.tgz");
});
+
+ it("should download a MongoDB 3.6-compatible package for 3.6 accounts", () => {
+ const properties = (userContext as UserContextType).databaseAccount.properties;
+ const originalApiProperties = properties.apiProperties;
+ properties.apiProperties = { serverVersion: "3.6" };
+
+ const commands = mongoShellHandler.getSetUpCommands();
+
+ expect(commands[1]).toContain("mongosh-1.10.6-linux-x64.tgz");
+ properties.apiProperties = originalApiProperties;
+ });
});
describe("getConnectionCommand", () => {
diff --git a/src/Explorer/Tabs/CloudShellTab/ShellTypes/MongoShellHandler.tsx b/src/Explorer/Tabs/CloudShellTab/ShellTypes/MongoShellHandler.tsx
index b41dbd6db..12869cb93 100644
--- a/src/Explorer/Tabs/CloudShellTab/ShellTypes/MongoShellHandler.tsx
+++ b/src/Explorer/Tabs/CloudShellTab/ShellTypes/MongoShellHandler.tsx
@@ -29,7 +29,7 @@ export class MongoShellHandler extends AbstractShellHandler {
}
public getSetUpCommands(): string[] {
- return this.mongoShellSetupCommands();
+ return this.mongoShellSetupCommands(userContext.databaseAccount?.properties.apiProperties?.serverVersion);
}
public getConnectionCommand(): string {
From eb65237acb8b016b6a1249b7d525023d7f4f81a5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mike=20Kr=C3=BCger?=
Date: Thu, 17 Sep 2026 19:48:16 +0200
Subject: [PATCH 15/16] Enable Cosmos DB Shell by default and clarify setup
progress (#2593)
* Enable Cosmos DB Shell by default and clarify setup progress
Default enableCosmosDBShell to true while retaining explicit opt-out support. Add localized progress for SDK installation, shell installation, update checks, and connection with safe Bash quoting. Cover default visibility, overrides, and progress messages with regression tests.
Validation: npm run build:ci and 83 focused unit tests passed.
* Fix Index Advisor Playwright tooltip interference
---
.../CommandBarComponentButtonFactory.test.ts | 59 ++++++++++++++++++-
.../ShellTypes/CosmosDBShellHandler.test.tsx | 33 ++++++++++-
.../ShellTypes/CosmosDBShellHandler.tsx | 24 ++++++--
src/Localization/en/Resources.json | 6 ++
src/Platform/Hosted/extractFeatures.test.ts | 12 ++++
src/Platform/Hosted/extractFeatures.ts | 2 +-
test/sql/indexAdvisor.spec.ts | 2 +-
7 files changed, 126 insertions(+), 12 deletions(-)
diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts
index 0008f99a3..afb7f6ba4 100644
--- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts
+++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts
@@ -1,8 +1,9 @@
import * as ko from "knockout";
+import { extractFeatures } from "Platform/Hosted/extractFeatures";
import { AuthType } from "../../../AuthType";
import { DatabaseAccount } from "../../../Contracts/DataModels";
-import { CollectionBase } from "../../../Contracts/ViewModels";
-import { updateUserContext } from "../../../UserContext";
+import { CollectionBase, TerminalKind } from "../../../Contracts/ViewModels";
+import { ApiType, updateUserContext, userContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode";
@@ -165,6 +166,60 @@ describe("CommandBarComponentButtonFactory tests", () => {
});
});
+ describe("Open Cosmos DB Shell button", () => {
+ let originalUserContext: typeof userContext;
+
+ beforeEach(() => {
+ originalUserContext = { ...userContext };
+ mockExplorer = { openNotebookTerminal: jest.fn() } as unknown as Explorer;
+ updateUserContext({
+ authType: AuthType.AAD,
+ databaseAccount: { kind: "DocumentDB", properties: { capabilities: [] } } as DatabaseAccount,
+ features: extractFeatures(new URLSearchParams()),
+ });
+ });
+
+ afterEach(() => updateUserContext(originalUserContext));
+
+ const getShellButton = () =>
+ CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, useSelectedNode.getState()).find(
+ (button) => button.commandButtonLabel === "Open Cosmos DB Shell",
+ );
+
+ it("shows and opens the shell with default features", () => {
+ const button = getShellButton();
+ expect(button).toBeDefined();
+ expect(button.disabled).toBe(false);
+ button.onCommandClick(new KeyboardEvent("keydown", { key: "Enter" }));
+ expect(mockExplorer.openNotebookTerminal).toHaveBeenCalledWith(TerminalKind.CosmosDB);
+ });
+
+ it("hides the shell when explicitly disabled by the feature flag", () => {
+ updateUserContext({
+ features: extractFeatures(new URLSearchParams({ "feature.enableCosmosDBShell": "false" })),
+ });
+ expect(getShellButton()).toBeUndefined();
+ });
+
+ it("hides the shell when Cloud Shell is unavailable", () => {
+ updateUserContext({ features: { ...userContext.features, enableCloudShell: false } });
+ expect(getShellButton()).toBeUndefined();
+ });
+
+ it.each(["Mongo", "Cassandra", "Gremlin", "Tables", "Postgres", "VCoreMongo"])(
+ "hides the shell for the %s API",
+ (apiType) => {
+ updateUserContext({ apiType });
+ expect(getShellButton()).toBeUndefined();
+ },
+ );
+
+ it("hides the shell for resource token authentication", () => {
+ updateUserContext({ authType: AuthType.ResourceToken });
+ expect(getShellButton()).toBeUndefined();
+ });
+ });
+
describe("Resource token", () => {
const mockCollection = { id: ko.observable("test") } as CollectionBase;
useSelectedNode.getState().setSelectedNode(mockCollection);
diff --git a/src/Explorer/Tabs/CloudShellTab/ShellTypes/CosmosDBShellHandler.test.tsx b/src/Explorer/Tabs/CloudShellTab/ShellTypes/CosmosDBShellHandler.test.tsx
index 99d9e75fe..21b043faf 100644
--- a/src/Explorer/Tabs/CloudShellTab/ShellTypes/CosmosDBShellHandler.test.tsx
+++ b/src/Explorer/Tabs/CloudShellTab/ShellTypes/CosmosDBShellHandler.test.tsx
@@ -1,3 +1,4 @@
+import * as Localization from "Localization/t";
import { CosmosDBShellHandler } from "./CosmosDBShellHandler";
// Mock dependencies
@@ -14,8 +15,9 @@ jest.mock("../../../../UserContext", () => ({
describe("CosmosDBShellHandler", () => {
const mockKey = "testKey";
const endpoint = "https://test-account.documents.azure.com:443/";
- const tokenConnectionCommand = `export COSMOSDB_SHELL_TOKEN='aadToken123'; cosmosdbshell --connect '${endpoint}' --connect-mode gateway --verbose`;
- const keyConnectionCommand = `export COSMOSDB_SHELL_ACCOUNT_KEY='${mockKey}'; cosmosdbshell --connect '${endpoint}' --connect-mode gateway --verbose`;
+ const connectionProgress = "printf '%s\\n' 'Connecting to your Cosmos DB account...'";
+ const tokenConnectionCommand = `${connectionProgress}; export COSMOSDB_SHELL_TOKEN='aadToken123'; cosmosdbshell --connect '${endpoint}' --connect-mode gateway --verbose`;
+ const keyConnectionCommand = `${connectionProgress}; export COSMOSDB_SHELL_ACCOUNT_KEY='${mockKey}'; cosmosdbshell --connect '${endpoint}' --connect-mode gateway --verbose`;
let cosmosDBShellHandler: CosmosDBShellHandler;
beforeEach(() => {
@@ -54,6 +56,32 @@ describe("CosmosDBShellHandler", () => {
expect(commands.some((c) => c === "export DOTNET_ROOT=$HOME/.dotnet")).toBe(true);
});
+ it("should describe each setup step only in the corresponding command branch", () => {
+ const commands = cosmosDBShellHandler.getSetUpCommands();
+ const sdkCommand = commands.find((command) => command.includes("dotnet-install.sh"));
+ const shellCommand = commands.find((command) => command.includes("dotnet tool install"));
+
+ expect(sdkCommand).toContain(
+ "then printf '%s\\n' 'Downloading and installing .NET SDK 10. First-time setup may take a few minutes.'; curl",
+ );
+ expect(shellCommand).toContain("then printf '%s\\n' 'Installing Cosmos DB Shell...'; dotnet tool install");
+ expect(shellCommand).toContain(
+ "else printf '%s\\n' 'Checking for Cosmos DB Shell updates...'; dotnet tool update",
+ );
+ expect(commands).toHaveLength(6);
+ });
+
+ it("should quote localized progress messages safely for Bash", () => {
+ const translation = jest.spyOn(Localization, "t").mockReturnValue("Account's $HOME `command` %s");
+ try {
+ expect(cosmosDBShellHandler.getConnectionCommand()).toContain(
+ "printf '%s\\n' 'Account'\\''s $HOME `command` %s'; export",
+ );
+ } finally {
+ translation.mockRestore();
+ }
+ });
+
it("should not export any credential env var in setup commands for a key credential", () => {
const commands = cosmosDBShellHandler.getSetUpCommands();
@@ -107,6 +135,7 @@ describe("CosmosDBShellHandler", () => {
const connectionCommand = handler.getConnectionCommand();
expect(connectionCommand).not.toContain("cosmosdbshell --connect");
+ expect(connectionCommand).not.toContain("Connecting to your Cosmos DB account");
expect(connectionCommand).toContain("Unable to acquire a Cosmos DB credential");
expect(connectionCommand).toContain("Login for Entra ID");
});
diff --git a/src/Explorer/Tabs/CloudShellTab/ShellTypes/CosmosDBShellHandler.tsx b/src/Explorer/Tabs/CloudShellTab/ShellTypes/CosmosDBShellHandler.tsx
index ed1b9658d..5ef137260 100644
--- a/src/Explorer/Tabs/CloudShellTab/ShellTypes/CosmosDBShellHandler.tsx
+++ b/src/Explorer/Tabs/CloudShellTab/ShellTypes/CosmosDBShellHandler.tsx
@@ -1,3 +1,4 @@
+import { ResourceKey, t } from "Localization/t";
import { userContext } from "../../../../UserContext";
import { AbstractShellHandler } from "./AbstractShellHandler";
@@ -65,14 +66,23 @@ export class CosmosDBShellHandler extends AbstractShellHandler {
return [
"export DOTNET_ROOT=$HOME/.dotnet",
"export PATH=$HOME/.dotnet:$HOME/.dotnet/tools:$PATH",
- "if ! command -v cosmosdbshell &> /dev/null; then echo '⚠️ cosmosdbshell not found. Installing .NET SDK 10 and CosmosDBShell...'; fi",
- "if ! command -v cosmosdbshell &> /dev/null && ! dotnet --list-sdks 2>/dev/null | grep -q '^10\\.'; then curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 10.0 --install-dir $HOME/.dotnet; fi",
- "if ! command -v cosmosdbshell &> /dev/null; then dotnet tool install --global CosmosDBShell --prerelease; else dotnet tool update --global CosmosDBShell --prerelease; fi",
+ `if ! command -v cosmosdbshell &> /dev/null && ! dotnet --list-sdks 2>/dev/null | grep -q '^10\\.'; then ${this._getProgressCommand(
+ "cosmosDBShell.installingSdk",
+ )}; curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 10.0 --install-dir $HOME/.dotnet; fi`,
+ `if ! command -v cosmosdbshell &> /dev/null; then ${this._getProgressCommand(
+ "cosmosDBShell.installingShell",
+ )}; dotnet tool install --global CosmosDBShell --prerelease; else ${this._getProgressCommand(
+ "cosmosDBShell.updatingShell",
+ )}; dotnet tool update --global CosmosDBShell --prerelease; fi`,
"grep -qxF 'export DOTNET_ROOT=$HOME/.dotnet' ~/.bashrc || echo 'export DOTNET_ROOT=$HOME/.dotnet' >> ~/.bashrc",
"grep -qxF 'export PATH=$HOME/.dotnet:$HOME/.dotnet/tools:$PATH' ~/.bashrc || echo 'export PATH=$HOME/.dotnet:$HOME/.dotnet/tools:$PATH' >> ~/.bashrc",
];
}
+ private _getProgressCommand(key: ResourceKey): string {
+ return `printf '%s\\n' '${t(key).replace(/'/g, "'\\''")}'`;
+ }
+
private _getKeyConnectionCommand(key: string): string {
// Export the key immediately before invoking the tool, on the same command line, so it
// never appears as a --connect flag value or lands in a separate setup step. The tool
@@ -118,9 +128,11 @@ export class CosmosDBShellHandler extends AbstractShellHandler {
endpoint: this._endpoint,
});
- return this.credential.kind === "key"
- ? this._getKeyConnectionCommand(this.credential.value)
- : this._getTokenConnectionCommand(this.credential.value);
+ const connectionCommand =
+ this.credential.kind === "key"
+ ? this._getKeyConnectionCommand(this.credential.value)
+ : this._getTokenConnectionCommand(this.credential.value);
+ return `${this._getProgressCommand("cosmosDBShell.connecting")}; ${connectionCommand}`;
}
public getTerminalSuppressedData(): string[] {
diff --git a/src/Localization/en/Resources.json b/src/Localization/en/Resources.json
index f0301eb47..e9e85e210 100644
--- a/src/Localization/en/Resources.json
+++ b/src/Localization/en/Resources.json
@@ -38,6 +38,12 @@
"off": "Off",
"preview": "Preview"
},
+ "cosmosDBShell": {
+ "installingSdk": "Downloading and installing .NET SDK 10. First-time setup may take a few minutes.",
+ "installingShell": "Installing Cosmos DB Shell...",
+ "updatingShell": "Checking for Cosmos DB Shell updates...",
+ "connecting": "Connecting to your Cosmos DB account..."
+ },
"splashScreen": {
"title": {
"default": "Welcome to Azure Cosmos DB",
diff --git a/src/Platform/Hosted/extractFeatures.test.ts b/src/Platform/Hosted/extractFeatures.test.ts
index 98fc8ebd7..5117329f9 100644
--- a/src/Platform/Hosted/extractFeatures.test.ts
+++ b/src/Platform/Hosted/extractFeatures.test.ts
@@ -1,6 +1,18 @@
import { extractFeatures, hasFlag } from "./extractFeatures";
describe("extractFeatures", () => {
+ it("enables Cosmos DB Shell by default", () => {
+ expect(extractFeatures(new URLSearchParams()).enableCosmosDBShell).toBe(true);
+ });
+
+ it.each(["feature.enableCosmosDBShell", "enableCosmosDBShell"])(
+ "respects explicit Cosmos DB Shell overrides via %s",
+ (key) => {
+ expect(extractFeatures(new URLSearchParams({ [key]: "false" })).enableCosmosDBShell).toBe(false);
+ expect(extractFeatures(new URLSearchParams({ [key]: "true" })).enableCosmosDBShell).toBe(true);
+ },
+ );
+
it("correctly detects feature flags in a case insensitive manner", () => {
const url = "https://localhost:10001/12345/notebook";
const token = "super secret";
diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts
index d8ccecae0..dbeffe906 100644
--- a/src/Platform/Hosted/extractFeatures.ts
+++ b/src/Platform/Hosted/extractFeatures.ts
@@ -105,7 +105,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
enableContainerCopy: "true" === get("enablecontainercopy"),
enableRestoreContainer: "true" === get("enablerestorecontainer"),
enableCloudShell: true,
- enableCosmosDBShell: "true" === get("enablecosmosdbshell"),
+ enableCosmosDBShell: "true" === get("enablecosmosdbshell", "true"),
mongoDisableNativeAuth: "true" === get("mongodisablenativeauth"),
};
}
diff --git a/test/sql/indexAdvisor.spec.ts b/test/sql/indexAdvisor.spec.ts
index dc6ee978c..a52d1dc23 100644
--- a/test/sql/indexAdvisor.spec.ts
+++ b/test/sql/indexAdvisor.spec.ts
@@ -38,9 +38,9 @@ async function setupIndexAdvisorTab(page: Page, customQuery?: string) {
const queryEditor = queryTab.editor();
await queryEditor.locator.waitFor({ timeout: 30 * 1000 });
await queryTab.executeCTA.waitFor();
+ await queryEditor.locator.click();
if (customQuery) {
- await queryEditor.locator.click();
await queryEditor.setText(customQuery);
}
From a92669ccbc29ee4496827c666207a241d679ef80 Mon Sep 17 00:00:00 2001
From: BChoudhury-ms
Date: Mon, 21 Sep 2026 20:44:05 +0530
Subject: [PATCH 16/16] Update CloudShell API version to 2025-09-01-preview
(#2570)
---
.../CloudShellTab/Data/CloudShellClient.test.tsx | 12 ++++++------
.../Tabs/CloudShellTab/Data/CloudShellClient.tsx | 6 +++---
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/Explorer/Tabs/CloudShellTab/Data/CloudShellClient.test.tsx b/src/Explorer/Tabs/CloudShellTab/Data/CloudShellClient.test.tsx
index 0a38e4aee..36b35d274 100644
--- a/src/Explorer/Tabs/CloudShellTab/Data/CloudShellClient.test.tsx
+++ b/src/Explorer/Tabs/CloudShellTab/Data/CloudShellClient.test.tsx
@@ -85,7 +85,7 @@ describe("CloudShellClient", () => {
host: MOCK_ARM_ENDPOINT,
path: "/providers/Microsoft.Portal/userSettings/cloudconsole",
method: "GET",
- apiVersion: "2023-02-01-preview",
+ apiVersion: "2025-09-01-preview",
});
expect(result).toEqual(mockSettings);
});
@@ -100,7 +100,7 @@ describe("CloudShellClient", () => {
host: MOCK_ARM_ENDPOINT,
path: "/providers/Microsoft.Portal/userSettings/cloudconsole",
method: "GET",
- apiVersion: "2023-02-01-preview",
+ apiVersion: "2025-09-01-preview",
});
});
});
@@ -116,7 +116,7 @@ describe("CloudShellClient", () => {
host: MOCK_ARM_ENDPOINT,
path: "/providers/Microsoft.Portal/userSettings/cloudconsole",
method: "PUT",
- apiVersion: "2023-02-01-preview",
+ apiVersion: "2025-09-01-preview",
body: {
properties: {
preferredOsType: OsType.Linux,
@@ -144,7 +144,7 @@ describe("CloudShellClient", () => {
host: MOCK_ARM_ENDPOINT,
path: "/providers/Microsoft.Portal/userSettings/cloudconsole",
method: "PUT",
- apiVersion: "2023-02-01-preview",
+ apiVersion: "2025-09-01-preview",
body: {
properties: {
preferredOsType: OsType.Linux,
@@ -247,7 +247,7 @@ describe("CloudShellClient", () => {
host: MOCK_ARM_ENDPOINT,
path: "providers/Microsoft.Portal/consoles/default",
method: "PUT",
- apiVersion: "2023-02-01-preview",
+ apiVersion: "2025-09-01-preview",
customHeaders: {
"x-ms-console-preferred-location": "eastus",
},
@@ -271,7 +271,7 @@ describe("CloudShellClient", () => {
host: MOCK_ARM_ENDPOINT,
path: "providers/Microsoft.Portal/consoles/default",
method: "PUT",
- apiVersion: "2023-02-01-preview",
+ apiVersion: "2025-09-01-preview",
customHeaders: {
"x-ms-console-preferred-location": "eastus",
},
diff --git a/src/Explorer/Tabs/CloudShellTab/Data/CloudShellClient.tsx b/src/Explorer/Tabs/CloudShellTab/Data/CloudShellClient.tsx
index 38cf1b03c..bc249db38 100644
--- a/src/Explorer/Tabs/CloudShellTab/Data/CloudShellClient.tsx
+++ b/src/Explorer/Tabs/CloudShellTab/Data/CloudShellClient.tsx
@@ -26,7 +26,7 @@ export const getUserSettings = async (): Promise => {
host: configContext.ARM_ENDPOINT,
path: `/providers/Microsoft.Portal/userSettings/cloudconsole`,
method: "GET",
- apiVersion: "2023-02-01-preview",
+ apiVersion: "2025-09-01-preview",
});
};
@@ -56,7 +56,7 @@ export const putEphemeralUserSettings = async (
host: configContext.ARM_ENDPOINT,
path: `/providers/Microsoft.Portal/userSettings/cloudconsole`,
method: "PUT",
- apiVersion: "2023-02-01-preview",
+ apiVersion: "2025-09-01-preview",
body: ephemeralSettings,
timeoutMs: CLOUDSHELL_ARM_TIMEOUT_MS,
});
@@ -92,7 +92,7 @@ export const provisionConsole = async (consoleLocation: string): Promise