From ac9ba5ccc653bf17705944113dd2137c1860695a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:43:42 -0700 Subject: [PATCH 01/40] Bump shell-quote from 1.7.3 to 1.8.4 (#2511) Bumps [shell-quote](https://github.com/ljharb/shell-quote) from 1.7.3 to 1.8.4. - [Changelog](https://github.com/ljharb/shell-quote/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/shell-quote/compare/v1.7.3...v1.8.4) --- updated-dependencies: - dependency-name: shell-quote dependency-version: 1.8.4 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 27 +++++++++++---------------- package.json | 2 +- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 06535b293..d7536e942 100644 --- a/package-lock.json +++ b/package-lock.json @@ -113,7 +113,7 @@ "react-youtube": "9.0.1", "reflect-metadata": "0.1.13", "rx-jupyter": "5.5.12", - "shell-quote": "1.7.3", + "shell-quote": "1.8.4", "styled-components": "5.0.1", "swr": "0.4.0", "terser-webpack-plugin": "5.3.9", @@ -22334,19 +22334,6 @@ "shell-quote": "^1.8.4" } }, - "node_modules/launch-editor/node_modules/shell-quote": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", - "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/leaflet": { "version": "1.9.4", "license": "BSD-2-Clause" @@ -26321,8 +26308,16 @@ } }, "node_modules/shell-quote": { - "version": "1.7.3", - "license": "MIT" + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/shiki": { "version": "1.12.0", diff --git a/package.json b/package.json index 84299d2cc..4f4a1208d 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "react-youtube": "9.0.1", "reflect-metadata": "0.1.13", "rx-jupyter": "5.5.12", - "shell-quote": "1.7.3", + "shell-quote": "1.8.4", "styled-components": "5.0.1", "swr": "0.4.0", "terser-webpack-plugin": "5.3.9", From 7c3d112b83ad656a5954aced4aa4bc5161fa5ae2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:08:35 -0700 Subject: [PATCH 02/40] Bump postcss from 8.4.31 to 8.5.15 (#2469) Bumps [postcss](https://github.com/postcss/postcss) from 8.4.31 to 8.5.15. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.31...8.5.15) --- updated-dependencies: - dependency-name: postcss dependency-version: 8.5.10 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index d7536e942..39828756f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24035,7 +24035,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -24052,9 +24054,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -24131,6 +24133,24 @@ "version": "4.2.0", "license": "MIT" }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -26537,7 +26557,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" From 74b6a92219e24bf4c088bbc8375b32388cf74950 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:21:18 -0700 Subject: [PATCH 03/40] Bump ws from 8.17.1 to 8.20.1 (#2514) Bumps [ws](https://github.com/websockets/ws) from 8.17.1 to 8.20.1. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.17.1...8.20.1) --- updated-dependencies: - dependency-name: ws dependency-version: 8.20.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 | 73 ++++------------------------------------------- package.json | 4 +-- 2 files changed, 7 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index 39828756f..c172ed01c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -122,7 +122,6 @@ "utility-types": "3.10.0", "uuid": "9.0.0", "web-vitals": "4.2.4", - "ws": "8.17.1", "zustand": "3.5.0" }, "devDependencies": { @@ -214,7 +213,7 @@ "webpack-bundle-analyzer": "5.2.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.2.3", - "ws": "8.17.1" + "ws": "8.20.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -19466,27 +19465,6 @@ "node": ">=12" } }, - "node_modules/jest-environment-jsdom/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/jest-environment-jsdom/node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", @@ -28314,27 +28292,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webpack-bundle-analyzer/node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/webpack-cli": { "version": "5.1.4", "dev": true, @@ -29141,27 +29098,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/webpack-merge": { "version": "5.10.0", "dev": true, @@ -29539,10 +29475,11 @@ } }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 4f4a1208d..9843793e6 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,7 @@ "utility-types": "3.10.0", "uuid": "9.0.0", "web-vitals": "4.2.4", - "ws": "8.17.1", + "ws": "8.20.1", "zustand": "3.5.0" }, "overrides": { @@ -224,7 +224,7 @@ "webpack-bundle-analyzer": "5.2.0", "webpack-cli": "5.1.4", "webpack-dev-server": "5.2.3", - "ws": "8.17.1" + "ws": "8.20.1" }, "scripts": { "postinstall": "patch-package && npm run generate:i18n-keys", From d19c7e0cb76c8464fdbe7852aaf79946ced9679a Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Mon, 15 Jun 2026 06:29:14 -0700 Subject: [PATCH 04/40] =?UTF-8?q?Remove=20Phoenix=20&=20Notebooks=20?= =?UTF-8?q?=E2=80=94=20Phase=201:=20Decouple=20database=20shells=20to=20Cl?= =?UTF-8?q?oudShell=20(#2513)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add implementation plan for removing Phoenix and notebooks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Phase 1: Decouple database shells from Phoenix to CloudShell-only Migrate all shell terminals to the CloudShell path and remove the legacy Phoenix notebook-server terminal code so shells no longer depend on notebook provisioning. - TerminalTab now always uses CloudShellTerminalComponentAdapter; removed the notebook-server adapter branch, getNotebookServerInfo, and the dead VCoreMongo firewall check - Migrate Postgres and VCore Mongo quickstart tabs to CloudShellTerminalComponent (drop allocateContainer/useNotebook dependencies) - Refactor useTerminal to send input via the CloudShell WebSocket instead of postRobot/iframe; register the socket from CloudShellTerminalComponent - Simplify Explorer.openNotebookTerminal to always open a CloudShell terminal - Delete NotebookTerminalComponent(+test/less/snapshot), NotebookTerminalComponentAdapter, and the src/Terminal/ entry point - Remove the terminal.html webpack entry/HTML plugin and src/Terminal from tsconfig.strict.json Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/remove-notebooks-plan.md | 214 ++++++++++++++++++ .../Notebook/NotebookTerminalComponent.less | 13 -- .../NotebookTerminalComponent.test.tsx | 146 ------------ .../Notebook/NotebookTerminalComponent.tsx | 99 -------- .../NotebookTerminalComponent.test.tsx.snap | 73 ------ src/Explorer/Explorer.tsx | 21 +- .../CloudShellTerminalComponent.tsx | 11 +- src/Explorer/Tabs/QuickstartTab.tsx | 42 +--- .../NotebookTerminalComponentAdapter.tsx | 32 --- src/Explorer/Tabs/Tabs.tsx | 6 +- src/Explorer/Tabs/TerminalTab.tsx | 82 +------ src/Explorer/Tabs/VCoreMongoQuickstartTab.tsx | 77 +------ src/Main.tsx | 1 - src/Terminal/JupyterLabAppFactory.ts | 125 ---------- src/Terminal/NotebookAppContracts.d.ts | 111 --------- src/Terminal/README.md | 74 ------ src/Terminal/TerminalProps.ts | 15 -- src/Terminal/index.css | 26 --- src/Terminal/index.html | 13 -- src/Terminal/index.ts | 125 ---------- src/hooks/useTerminal.ts | 24 +- tsconfig.strict.json | 1 - webpack.config.js | 6 - 23 files changed, 259 insertions(+), 1078 deletions(-) create mode 100644 docs/remove-notebooks-plan.md delete mode 100644 src/Explorer/Controls/Notebook/NotebookTerminalComponent.less delete mode 100644 src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx delete mode 100644 src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx delete mode 100644 src/Explorer/Controls/Notebook/__snapshots__/NotebookTerminalComponent.test.tsx.snap delete mode 100644 src/Explorer/Tabs/ShellAdapters/NotebookTerminalComponentAdapter.tsx delete mode 100644 src/Terminal/JupyterLabAppFactory.ts delete mode 100644 src/Terminal/NotebookAppContracts.d.ts delete mode 100644 src/Terminal/README.md delete mode 100644 src/Terminal/TerminalProps.ts delete mode 100644 src/Terminal/index.css delete mode 100644 src/Terminal/index.html delete mode 100644 src/Terminal/index.ts diff --git a/docs/remove-notebooks-plan.md b/docs/remove-notebooks-plan.md new file mode 100644 index 000000000..9b6104509 --- /dev/null +++ b/docs/remove-notebooks-plan.md @@ -0,0 +1,214 @@ +# Implementation Plan: Remove Phoenix & Notebooks + +## Problem statement + +Cosmos Explorer contains a large, deeply-integrated **notebooks** feature backed by +the **Phoenix** compute-container service and the **Juno** service, plus a **GitHub** +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 +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 + 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). +- **Schema Analyzer** (`src/Explorer/Notebook/SchemaAnalyzer`): Remove. +- **Phasing**: Every phase must leave the app in a **buildable, shippable** state + (build + lint + strict compile + unit tests green; shells still work). +- **Localization**: Remove notebook/GitHub strings from **all** resource files — + `src/Localization/en/Resources.json` **and** every non-English locale + (`src/Localization//Resources.json`). (This deletion is an exception to the + usual convention of editing only the English file.) + +## Prior art / related commits + +This is a continuation of an in-progress removal effort. Reference commits: + +- `7295d63a` — Remove gallery.html and all associated gallery functionality (#2474) +- `a36467f4` — Remove Phoenix `getDbAccountAllowedStatus`; `isPhoenixNotebooks`/ + `isPhoenixFeatures` now always `false` (#2472) +- `31385950` — removed NotebookViewer file (#2281) + +> **Note:** An unmerged branch `users/jawelton/remove-notebooks-terminal-052126` +> already contains related work (`5989c77c` "Remove terminal.html webpack entry point +> and notebooks terminal code", `c7f9d7e3` "Switch VCore Mongo quickstart to use +> CloudShell terminal"). These are **not** in `master`. Reconcile with that branch +> before/while starting Phase 1 to avoid duplicate or conflicting work. + +## Current-state survey (what exists today) + +**Core directories / files** +- `src/Phoenix/PhoenixClient.ts` — container allocation, heartbeat, status polling. +- `src/Juno/JunoClient.ts` (+ test) — pinned-repo / notebook metadata service client. +- `src/Explorer/Notebook/` — the bulk of the feature: + - `useNotebook.ts` (Zustand store), `NotebookManager.tsx`, `NotebookContentClient.ts`, + `NotebookClientV2.ts`, `NotebookContainerClient.ts`, `NotebookContentItem.ts`, + `NotebookUtil.ts`, `NTeractUtil.ts`, `FileSystemUtil.ts` + - `NotebookComponent/` (nteract redux store, epics, reducers, content providers) + - `NotebookRenderer/` (nteract cell rendering, decorators, outputs) + - `SchemaAnalyzer/`, `SecurityWarningBar/` +- `src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx` (+ less/test) +- `src/Explorer/Controls/NotebookViewer/` — read-only viewer + metadata. +- `src/Explorer/Tabs/NotebookV2Tab.ts`, `NotebookTabBase.ts`, `SchemaAnalyzerTab.ts` +- `src/Explorer/Panes/CopyNotebookPane/` +- `src/Explorer/Tabs/ShellAdapters/NotebookTerminalComponentAdapter.tsx` +- `src/CellOutputViewer/` — webpack entry `cellOutputViewer`. +- `src/Utils/NotebookConfigurationUtils.ts`, `src/hooks/useNotebookSnapshotStore.ts` +- `src/Utils/arm/generatedClients/cosmosNotebooks/` — generated ARM client. + +**GitHub integration (notebook-only)** +- `src/GitHub/` (`GitHubClient.ts`, `GitHubContentProvider.ts`, `GitHubOAuthService.ts`, + `GitHubConnector.ts`), `src/Utils/GitHubUtils.ts` +- `src/Explorer/Controls/GitHub/` (AuthorizeAccess, AddRepo, GitHubRepos components) +- `src/Explorer/Panes/GitHubReposPanel/` +- webpack entry `connectToGitHub` + `src/connectToGitHub.html` + +**Integration / glue points (edited, not deleted)** +- `src/Explorer/Explorer.tsx` — `phoenixClient`, `notebookManager`, `gitHubOAuthService`, + `initNotebooks`, `initiateAndRefreshNotebookList`, `allocateContainer`, + `openNotebook*`, `openNotebookTerminal`, `createNotebookContentItemFile`, etc. +- `src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx` (+ adapter, test) + — New Notebook / Open Terminal / shell buttons branching on `isShellEnabled`. +- `src/Explorer/Tree/treeNodeUtil.tsx` / `ResourceTreeAdapter.tsx` / `ResourceTree.tsx` + — "My Notebooks" / "GitHub" tree nodes; `isNotebookEnabled` plumbing. +- `src/Explorer/SplashScreen/SplashScreen.tsx` — notebook cards + `openNotebookTerminal` + for Postgres/VCoreMongo shells. +- `src/Explorer/Tabs/TerminalTab.tsx` — chooses CloudShell vs notebook-server adapter. +- `src/Explorer/OpenActions/OpenActions.tsx`, `src/Explorer/ContextMenuButtonFactory.tsx`, + `src/Explorer/Tree/Collection.ts`, `src/Explorer/useSelectedNode.ts`, + `src/Explorer/MostRecentActivity/MostRecentActivity.ts` +- `src/hooks/useKnockoutExplorer.ts`, `src/hooks/useTabs.ts` +- `src/ConfigContext.ts`, `src/Common/Constants.ts`, `src/Contracts/DataModels.ts`, + `src/Contracts/ViewModels.ts`, `src/Contracts/ActionContracts.ts`, + `src/Platform/Hosted/extractFeatures.ts` (+ test) +- `src/Shared/Telemetry/TelemetryConstants.ts` +- `src/Localization/en/Resources.json` **and all non-English** `src/Localization//Resources.json` +- `webpack.config.js` — `cellOutputViewer`, `connectToGitHub` entries + HTML plugins. +- `package.json` — `@nteract/*`, `@jupyterlab/*`, `@phosphor/widgets`, `rx-jupyter`, + and other notebook-only dependencies. + +**Critical coupling — Terminal / shells** +- `TerminalTab` uses `CloudShellTerminalComponentAdapter` when + `userContext.features.enableCloudShell`, otherwise `NotebookTerminalComponentAdapter` + (which needs a Phoenix-allocated notebook server + `terminal.html` iframe). +- Command-bar/splash shell buttons branch on + `useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell`. +- `Explorer.openNotebookTerminal(...)` is the shared entry for opening shells and must + be retained (and rewired to CloudShell-only) — only its notebook-server behavior is + removed. + +## Phased approach + +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 +still open. + +### Phase 1 — Decouple database shells to CloudShell-only +Remove the legacy Phoenix notebook-server terminal path so shells no longer depend on +notebook provisioning. +- Rewire `TerminalTab` to always use `CloudShellTerminalComponentAdapter`; delete the + `NotebookTerminalComponentAdapter` branch and `getNotebookServerInfo`. +- Delete `src/Explorer/Tabs/ShellAdapters/NotebookTerminalComponentAdapter.tsx` and + `src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx` (+ less/test/snapshot). +- Simplify shell buttons in `CommandBarComponentButtonFactory` and `SplashScreen` to + drop the `isShellEnabled` branch (CloudShell path only); keep `openNotebookTerminal`. +- Verify whether the `terminal`/`terminal.html` webpack entry is still needed by + CloudShell. If unused, remove it and `src/Terminal/`; otherwise keep. +- **Outcome:** Shells run purely on CloudShell. Phoenix no longer needed for terminals. + +### Phase 2 — Remove the in-app notebook authoring & rendering experience +Delete the notebook tabs, the nteract rendering engine, panes, and the read-only viewer, +and remove all UI entry points that open notebooks. +- Delete: `src/Explorer/Notebook/NotebookComponent/`, + `src/Explorer/Notebook/NotebookRenderer/`, `src/Explorer/Notebook/SecurityWarningBar/`, + `NotebookClientV2.ts`, `notebookClientV2.test.ts`, `NotebookContentClient.ts`, + `NTeractUtil.ts`, `NotebookContentItem.ts`, `NotebookUtil.ts` (+ test), + `FileSystemUtil.ts`. +- Delete tabs/panes/viewer: `src/Explorer/Tabs/NotebookV2Tab.ts`, `NotebookTabBase.ts`, + `src/Explorer/Panes/CopyNotebookPane/`, `src/Explorer/Controls/NotebookViewer/`, + `src/CellOutputViewer/` (+ `cellOutputViewer` webpack entry & HTML plugin). +- Remove notebook entry points: "New Notebook"/open-notebook buttons in + `CommandBarComponentButtonFactory` (+ test), `OpenActions.tsx`, + `ContextMenuButtonFactory.tsx`, splash-screen notebook cards & recent-notebook items + (`MostRecentActivity` OpenNotebook type), and the `openNotebook*` / + `createNotebookContentItemFile` methods on `Explorer`. +- Remove notebook deps from `package.json`: `@nteract/*`, `@jupyterlab/*`, + `@phosphor/widgets`, `rx-jupyter` (and any now-unused transitive notebook-only libs). +- **Outcome:** Notebooks can no longer be authored, opened, or rendered. + +### Phase 3 — Remove Schema Analyzer +- Delete `src/Explorer/Notebook/SchemaAnalyzer/` and `src/Explorer/Tabs/SchemaAnalyzerTab.ts`. +- Remove Schema Analyzer command-bar button and any tree/menu entry points. + +### Phase 4 — Remove GitHub integration +- Delete `src/GitHub/`, `src/Explorer/Controls/GitHub/`, + `src/Explorer/Panes/GitHubReposPanel/`, `src/Utils/GitHubUtils.ts`, + `src/connectToGitHub.html`, and the `connectToGitHub` webpack entry & HTML plugin. +- Remove `gitHubOAuthService`, GitHub pinned-repo wiring, and `gitHubNotebooksContentRoot` + usage from `Explorer.tsx`, `useNotebook.ts`, `NotebookManager.tsx`, and `JunoClient` + 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 +- Delete `src/Phoenix/`, `src/Explorer/Notebook/NotebookContainerClient.ts`, + `src/Explorer/Notebook/NotebookManager.tsx`, `src/Explorer/Notebook/useNotebook.ts`, + `src/Utils/NotebookConfigurationUtils.ts`, `src/hooks/useNotebookSnapshotStore.ts`. +- Remove from `Explorer.tsx`: `phoenixClient`, `notebookManager`, `_isInitializingNotebooks`, + `initNotebooks`, `initiateAndRefreshNotebookList`, `refreshNotebookList`, + `allocateContainer`, container heartbeat/connection logic, and notebook-server URL + feature overrides. +- Remove notebook tree nodes ("My Notebooks") and `isNotebookEnabled` plumbing from + `treeNodeUtil.tsx`, `ResourceTreeAdapter.tsx`, `ResourceTree.tsx`, `Collection.ts`, + `useSelectedNode.ts` (+ update tree snapshots/tests). +- Remove notebook initialization from `useKnockoutExplorer.ts` and notebook tab handling + in `useTabs.ts`. + +### Phase 6 — Remove residual clients, config, contracts, telemetry & strings +- Delete `src/Juno/` and `src/Utils/arm/generatedClients/cosmosNotebooks/`. +- Remove notebook fields from `ConfigContext.ts`, `Constants.ts` (Notebook namespace), + `DataModels.ts` (notebook/Phoenix/container interfaces), `ViewModels.ts`, + `ActionContracts.ts`, and notebook feature flags from + `extractFeatures.ts` (+ update test). +- Remove notebook/Phoenix telemetry actions/areas from `TelemetryConstants.ts` (preserve + enum numbering if other systems depend on it — mirror the cautious approach in + `a36467f4`). +- Remove remaining notebook strings from **all** locale `Resources.json` files (`en` + + every non-English locale) and any notebook images (e.g. `images/notebook/`). +- Final full build + test sweep; update `EndpointUtils.ts` (`allowedNotebookServerUrls`) + and any docs/comments referencing notebooks. + +## Cross-cutting verification (run after each phase) + +``` +npm run compile +npm run compile:strict +npm run lint +npm run format:check +npm test +npm run build:ci +``` +Plus manual smoke test: open Mongo, Cassandra, Postgres, and VCoreMongo shells. + +## Notes & considerations + +- **Strict null checks:** any file edited may need to stay in / be removed from + `tsconfig.strict.json`. Remove deleted files from that list. +- **Snapshots:** several Jest snapshots reference notebook UI + (`treeNodeUtil`, `SettingsComponent`, panel snapshots). Regenerate after edits. +- **Telemetry enum safety:** prior commit `a36467f4` deliberately reverted removal of + enum values to avoid breaking downstream consumers. Prefer leaving enum numeric values + intact unless confirmed safe to remove. +- **`enableCloudShell` feature flag:** confirm it is enabled in all target environments + before removing the Phoenix shell fallback, or shells will break. +- **E2E tests:** check `test/` for notebook/terminal specs to update or remove; shells + may have E2E coverage that needs the CloudShell-only path. +- **Reconcile** with branch `users/jawelton/remove-notebooks-terminal-052126` to avoid + rework, especially in Phase 1. diff --git a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.less b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.less deleted file mode 100644 index 305c8827c..000000000 --- a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.less +++ /dev/null @@ -1,13 +0,0 @@ -@import "../../../../less/Common/Constants"; - -.notebookTerminalContainer { - padding: @DefaultSpace; - height: 100%; - width: 100%; - - iframe { - border: none; - height: 100%; - width: 100%; - } -} \ No newline at end of file diff --git a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx deleted file mode 100644 index bd816ac31..000000000 --- a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { shallow } from "enzyme"; -import React from "react"; -import * as DataModels from "../../../Contracts/DataModels"; -import { NotebookTerminalComponent, NotebookTerminalComponentProps } from "./NotebookTerminalComponent"; - -const testAccount: DataModels.DatabaseAccount = { - id: "id", - kind: "kind", - location: "location", - name: "name", - properties: { - documentEndpoint: "https://testDocumentEndpoint.azure.com/", - }, - type: "type", -}; - -const testMongo32Account: DataModels.DatabaseAccount = { - ...testAccount, -}; - -const testMongo36Account: DataModels.DatabaseAccount = { - ...testAccount, - properties: { - mongoEndpoint: "https://testMongoEndpoint.azure.com/", - }, -}; - -const testCassandraAccount: DataModels.DatabaseAccount = { - ...testAccount, - properties: { - cassandraEndpoint: "https://testCassandraEndpoint.azure.com/", - }, -}; - -const testPostgresAccount: DataModels.DatabaseAccount = { - ...testAccount, - properties: { - postgresqlEndpoint: "https://testPostgresEndpoint.azure.com/", - }, -}; - -const testVCoreMongoAccount: DataModels.DatabaseAccount = { - ...testAccount, - properties: { - vcoreMongoEndpoint: "https://testVCoreMongoEndpoint.azure.com/", - }, -}; - -const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = { - authToken: "authToken", - notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com", - forwardingId: "Id", -}; - -const testMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = { - authToken: "authToken", - notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo", - forwardingId: "Id", -}; - -const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = { - authToken: "authToken", - notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra", - forwardingId: "Id", -}; - -const testPostgresNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = { - authToken: "authToken", - notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/postgresql", - forwardingId: "Id", -}; - -const testVCoreMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = { - authToken: "authToken", - notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongovcore", - forwardingId: "Id", -}; - -describe("NotebookTerminalComponent", () => { - it("renders terminal", () => { - const props: NotebookTerminalComponentProps = { - databaseAccount: testAccount, - notebookServerInfo: testNotebookServerInfo, - tabId: undefined, - }; - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it("renders mongo 3.2 shell", () => { - const props: NotebookTerminalComponentProps = { - databaseAccount: testMongo32Account, - notebookServerInfo: testMongoNotebookServerInfo, - tabId: undefined, - }; - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it("renders mongo 3.6 shell", () => { - const props: NotebookTerminalComponentProps = { - databaseAccount: testMongo36Account, - notebookServerInfo: testMongoNotebookServerInfo, - tabId: undefined, - }; - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it("renders cassandra shell", () => { - const props: NotebookTerminalComponentProps = { - databaseAccount: testCassandraAccount, - notebookServerInfo: testCassandraNotebookServerInfo, - tabId: undefined, - }; - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it("renders Postgres shell", () => { - const props: NotebookTerminalComponentProps = { - databaseAccount: testPostgresAccount, - notebookServerInfo: testPostgresNotebookServerInfo, - tabId: undefined, - }; - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - it("renders vCore Mongo shell", () => { - const props: NotebookTerminalComponentProps = { - databaseAccount: testVCoreMongoAccount, - notebookServerInfo: testVCoreMongoNotebookServerInfo, - tabId: undefined, - username: "username", - }; - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx b/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx deleted file mode 100644 index 96d41155e..000000000 --- a/src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Wrapper around Notebook server terminal - */ - -import { useTerminal } from "hooks/useTerminal"; -import postRobot from "post-robot"; -import * as React from "react"; -import * as DataModels from "../../../Contracts/DataModels"; -import { TerminalProps } from "../../../Terminal/TerminalProps"; -import { userContext } from "../../../UserContext"; -import * as StringUtils from "../../../Utils/StringUtils"; - -export interface NotebookTerminalComponentProps { - notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo; - databaseAccount: DataModels.DatabaseAccount; - tabId: string; - username?: string; -} - -export class NotebookTerminalComponent extends React.Component { - private terminalWindow: Window; - - constructor(props: NotebookTerminalComponentProps) { - super(props); - } - - componentDidMount(): void { - this.sendPropsToTerminalFrame(); - } - - public render(): JSX.Element { - return ( -
- )} - {!isLoggedIn && !encryptedTokenMetadata && ( - + {!isLoggedIn && !accountMetadata && ( + )} {isLoggedIn && authFailure && } {isLoggedIn && !authFailure && ( diff --git a/src/HostedExplorerChildFrame.ts b/src/HostedExplorerChildFrame.ts index 2cff6c862..3a38170ac 100644 --- a/src/HostedExplorerChildFrame.ts +++ b/src/HostedExplorerChildFrame.ts @@ -14,10 +14,12 @@ export interface AAD { export interface ConnectionString { authType: AuthType.ConnectionString; - // Connection string uses still use encrypted token for Cassandra/Mongo APIs as they us the portal backend proxy - encryptedToken: string; + // SQL, Table, and Gremlin sign data-plane requests client-side with the master key and do not need the + // proxies, so they carry no encrypted token. Mongo and Cassandra still use the encrypted + // token because their operations go through the proxies. + encryptedToken?: string; encryptedTokenMetadata: AccessInputMetadata; - // Master key is currently only used by Graph API. All other APIs use encrypted tokens and proxy with connection string + // Master key is used for the client-side signing path (SQL, Table, Gremlin). Mongo/Cassandra leave it undefined. masterKey?: string; } diff --git a/src/Localization/en/Resources.json b/src/Localization/en/Resources.json index 9b1299b06..f0301eb47 100644 --- a/src/Localization/en/Resources.json +++ b/src/Localization/en/Resources.json @@ -1175,4 +1175,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/Platform/Hosted/Components/ConnectExplorer.test.tsx b/src/Platform/Hosted/Components/ConnectExplorer.test.tsx index dc54a5f2a..a98a89ccc 100644 --- a/src/Platform/Hosted/Components/ConnectExplorer.test.tsx +++ b/src/Platform/Hosted/Components/ConnectExplorer.test.tsx @@ -12,8 +12,20 @@ it("shows the connect form", () => { const setConnectionString = jest.fn(); const setEncryptedToken = jest.fn(); const setAuthType = jest.fn(); + const setAccountMetadata = jest.fn(); - render(); + render( + , + ); expect(screen.queryByPlaceholderText("Please enter a connection string")).toBeNull(); fireEvent.click(screen.getByText("Connect to your account with connection string")); expect(screen.queryByPlaceholderText("Please enter a connection string")).toBeDefined(); @@ -25,6 +37,7 @@ it("hides the connection string link when feature.disableConnectionStringLogin i const setConnectionString = jest.fn(); const setEncryptedToken = jest.fn(); const setAuthType = jest.fn(); + const setAccountMetadata = jest.fn(); const oldFeatures = userContext.features; const params = new URLSearchParams({ @@ -34,7 +47,18 @@ it("hides the connection string link when feature.disableConnectionStringLogin i const testFeatures = extractFeatures(params); updateUserContext({ features: testFeatures }); - render(); + render( + , + ); expect(screen.queryByPlaceholderText("Connect to your account with connection string")).toBeNull(); updateUserContext({ features: oldFeatures }); diff --git a/src/Platform/Hosted/Components/ConnectExplorer.tsx b/src/Platform/Hosted/Components/ConnectExplorer.tsx index 768fddbb6..99ce638cd 100644 --- a/src/Platform/Hosted/Components/ConnectExplorer.tsx +++ b/src/Platform/Hosted/Components/ConnectExplorer.tsx @@ -1,12 +1,15 @@ 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 { HttpHeaders } from "../../../Common/Constants"; -import { configContext } from "../../../ConfigContext"; +import { fetchEncryptedToken, isAccountRestrictedForConnectionStringLogin } from "../../../Common/PortalBackendClient"; +import { AccessInputMetadata } from "../../../Contracts/DataModels"; +import { parseConnectionString } from "../Helpers/ConnectionStringParser"; import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils"; +import { isDirectConnectionStringLoginApi } from "../HostedUtils"; interface Props { connectionString: string; @@ -14,39 +17,16 @@ interface Props { setEncryptedToken: (token: string) => void; setConnectionString: (connectionString: string) => void; setAuthType: (authType: AuthType) => void; + setAccountMetadata: (metadata: AccessInputMetadata) => void; } -export const fetchEncryptedToken = async (connectionString: string): Promise => { - const headers = new Headers(); - headers.append(HttpHeaders.connectionString, connectionString); - const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/connectionstring/token/generatetoken"; - const response = await fetch(url, { headers, method: "POST" }); - if (!response.ok) { - throw response; - } - - const encryptedTokenResponse: string = await response.json(); - return decodeURIComponent(encryptedTokenResponse); -}; - -export const isAccountRestrictedForConnectionStringLogin = async (connectionString: string): Promise => { - const headers = new Headers(); - headers.append(HttpHeaders.connectionString, connectionString); - const url = configContext.PORTAL_BACKEND_ENDPOINT + "/api/guest/accountrestrictions/checkconnectionstringlogin"; - const response = await fetch(url, { headers, method: "POST" }); - if (!response.ok) { - throw response; - } - - return (await response.text()).toLowerCase() === "true"; -}; - export const ConnectExplorer: React.FunctionComponent = ({ setEncryptedToken, login, setAuthType, connectionString, setConnectionString, + setAccountMetadata, }: Props) => { const [isFormVisible, { setTrue: showForm }] = useBoolean(false); const [errorMessage, setErrorMessage] = React.useState(""); @@ -67,10 +47,15 @@ export const ConnectExplorer: React.FunctionComponent = ({ event.preventDefault(); setErrorMessage(""); - if (await isAccountRestrictedForConnectionStringLogin(connectionString)) { - setErrorMessage( - "This account has been blocked from connection-string login. Please go to cosmos.azure.com/aad for AAD based login.", - ); + try { + if (await isAccountRestrictedForConnectionStringLogin(connectionString)) { + setErrorMessage( + "This account has been blocked from connection-string login. Please go to cosmos.azure.com/aad for AAD based login.", + ); + return; + } + } catch (error) { + setErrorMessage(getErrorMessage(error)); return; } @@ -79,6 +64,15 @@ export const ConnectExplorer: React.FunctionComponent = ({ return; } + const metadata = parseConnectionString(connectionString); + if (metadata && isDirectConnectionStringLoginApi(metadata.apiKind)) { + // SQL, Table, and Gremlin sign data-plane requests client-side with the account key, so + // we skip the Portal Backend proxy and use the metadata parsed from the connection string. + setAccountMetadata(metadata); + setAuthType(AuthType.ConnectionString); + return; + } + const encryptedToken = await fetchEncryptedToken(connectionString); setEncryptedToken(encryptedToken); setAuthType(AuthType.ConnectionString); diff --git a/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts b/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts index 93d76855c..823ebb115 100644 --- a/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts +++ b/src/Platform/Hosted/Helpers/ConnectionStringParser.test.ts @@ -12,6 +12,18 @@ describe("ConnectionStringParser", () => { 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(); + }); + + it("should keep the document endpoint given by the connection string", () => { + // The endpoint is taken from the connection string rather than rebuilt from the account name, so a + // string that omits the port keeps it omitted. + const metadata = parseConnectionString( + `AccountEndpoint=https://${mockAccountName}.documents.azure.com/;AccountKey=${mockMasterKey};`, + ); + + expect(metadata.documentEndpoint).toBe(`https://${mockAccountName}.documents.azure.com/`); }); it("should parse a valid mongo account connection string", () => { @@ -39,6 +51,8 @@ describe("ConnectionStringParser", () => { expect(metadata.accountName).toBe(mockAccountName); expect(metadata.apiKind).toBe(DataModels.ApiKind.Graph); + expect(metadata.documentEndpoint).toBe(`https://${mockAccountName}.documents.azure.com:443/`); + expect(metadata.apiEndpoint).toBe(`${mockAccountName}.gremlin.cosmos.azure.com:443`); }); it("should parse a valid table account connection string", () => { @@ -48,6 +62,20 @@ describe("ConnectionStringParser", () => { 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(); + }); + + 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/;`, + ); + + 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(); }); it("should parse a valid cassandra account connection string", () => { diff --git a/src/Platform/Hosted/Helpers/ConnectionStringParser.ts b/src/Platform/Hosted/Helpers/ConnectionStringParser.ts index 6c8db5f96..44be7e82a 100644 --- a/src/Platform/Hosted/Helpers/ConnectionStringParser.ts +++ b/src/Platform/Hosted/Helpers/ConnectionStringParser.ts @@ -1,6 +1,13 @@ import * as Constants from "../../../Common/Constants"; 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 DnsPort = "443"; + export function parseConnectionString(connectionString: string): AccessInputMetadata { if (connectionString) { try { @@ -11,6 +18,9 @@ export function parseConnectionString(connectionString: string): AccessInputMeta if (RegExp(Constants.EndpointsRegex.sql).test(connectionStringPart)) { accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.sql)[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); accessInput.accountName = matches && matches.length > 1 && matches[2]; @@ -38,6 +48,17 @@ 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. + if (accessInput.accountName) { + if (accessInput.apiKind === ApiKind.Table) { + accessInput.documentEndpoint = `https://${accessInput.accountName}.${DocumentEndpointZone}:${DnsPort}/`; + } else if (accessInput.apiKind === ApiKind.Graph) { + accessInput.apiEndpoint = `${accessInput.accountName}.${GremlinEndpointZone}:${DnsPort}`; + } + } + return accessInput; } catch (error) { return undefined; diff --git a/src/Platform/Hosted/HostedUtils.test.ts b/src/Platform/Hosted/HostedUtils.test.ts index 1b8ddd666..d199aba0f 100644 --- a/src/Platform/Hosted/HostedUtils.test.ts +++ b/src/Platform/Hosted/HostedUtils.test.ts @@ -1,5 +1,9 @@ -import { AccessInputMetadata } from "../../Contracts/DataModels"; -import { getDatabaseAccountPropertiesFromMetadata } from "./HostedUtils"; +import { AccessInputMetadata, ApiKind } from "../../Contracts/DataModels"; +import { + extractMasterKeyFromDirectLoginConnectionString, + getDatabaseAccountPropertiesFromMetadata, + isDirectConnectionStringLoginApi, +} from "./HostedUtils"; describe("getDatabaseAccountPropertiesFromMetadata", () => { it("should only return an object with the mongoEndpoint key if the apiKind is mongoCompute (5)", () => { @@ -30,3 +34,58 @@ describe("getDatabaseAccountPropertiesFromMetadata", () => { }); }); }); + +describe("extractMasterKeyFromDirectLoginConnectionString", () => { + const mockAccountName = "Test"; + const mockKey = "abc123+/=someKey=="; + + it("extracts the account key from a SQL connection string", () => { + expect( + extractMasterKeyFromDirectLoginConnectionString( + `AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockKey};`, + ), + ).toBe(mockKey); + }); + + it("extracts the account key from a Table connection string", () => { + expect( + extractMasterKeyFromDirectLoginConnectionString( + `DefaultEndpointsProtocol=https;AccountName=${mockAccountName};AccountKey=${mockKey};TableEndpoint=https://${mockAccountName}.table.cosmosdb.azure.com:443/;`, + ), + ).toBe(mockKey); + }); + + it("extracts the account key from a Gremlin connection string", () => { + expect( + extractMasterKeyFromDirectLoginConnectionString( + `AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;AccountKey=${mockKey};ApiKind=Gremlin;`, + ), + ).toBe(mockKey); + }); + + it("returns undefined when there is no account key", () => { + expect( + extractMasterKeyFromDirectLoginConnectionString( + `AccountEndpoint=https://${mockAccountName}.documents.azure.com:443/;`, + ), + ).toBeUndefined(); + }); + + it("returns undefined for an empty connection string", () => { + expect(extractMasterKeyFromDirectLoginConnectionString("")).toBeUndefined(); + }); +}); + +describe("isDirectConnectionStringLoginApi", () => { + it("returns true for SQL, Table, and Graph", () => { + expect(isDirectConnectionStringLoginApi(ApiKind.SQL)).toBe(true); + expect(isDirectConnectionStringLoginApi(ApiKind.Table)).toBe(true); + expect(isDirectConnectionStringLoginApi(ApiKind.Graph)).toBe(true); + }); + + it("returns false for Mongo and Cassandra, which require the Portal Backend proxy", () => { + expect(isDirectConnectionStringLoginApi(ApiKind.MongoDB)).toBe(false); + expect(isDirectConnectionStringLoginApi(ApiKind.MongoDBCompute)).toBe(false); + expect(isDirectConnectionStringLoginApi(ApiKind.Cassandra)).toBe(false); + }); +}); diff --git a/src/Platform/Hosted/HostedUtils.ts b/src/Platform/Hosted/HostedUtils.ts index f39e318b1..32cc2e452 100644 --- a/src/Platform/Hosted/HostedUtils.ts +++ b/src/Platform/Hosted/HostedUtils.ts @@ -40,8 +40,14 @@ export function getDatabaseAccountKindFromExperience(apiExperience: typeof userC return AccountKind.GlobalDocumentDB; } -export function extractMasterKeyfromConnectionString(connectionString: string): string | undefined { - // Only Gremlin uses the actual master key for connection to cosmos - const matchedParts = connectionString.match("AccountKey=(.*);ApiKind=Gremlin;$"); +// Returns the master key carried by SQL, Table, and Gremlin connection strings. +export function extractMasterKeyFromDirectLoginConnectionString(connectionString: string): string | undefined { + const matchedParts = connectionString?.match(/AccountKey=([^;]*)/); return (matchedParts && matchedParts.length > 1 && matchedParts[1]) || undefined; } + +// SQL, Table, and Gremlin can sign data-plane requests client-side with the account key, so they do +// not need the Portal Backend proxy for connection-string login. Mongo and Cassandra still require the proxy. +export function isDirectConnectionStringLoginApi(apiKind: ApiKind): boolean { + return apiKind === ApiKind.SQL || apiKind === ApiKind.Table || apiKind === ApiKind.Graph; +} diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index d4a4f9555..41bec9797 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -467,13 +467,24 @@ function configureHostedWithConnectionString(config: ConnectionString): Explorer properties: getDatabaseAccountPropertiesFromMetadata(config.encryptedTokenMetadata), tags: {}, }; - updateUserContext({ - // For legacy reasons lots of code expects a connection string login to look and act like an encrypted token login - authType: AuthType.EncryptedToken, - accessToken: encodeURIComponent(config.encryptedToken), - databaseAccount, - masterKey: config.masterKey, - }); + if (config.masterKey && !config.encryptedToken) { + // Direct client-side signing path (SQL, Table, Gremlin). Requests are signed locally with the + // account key via the Cosmos client's tokenProvider, so no Portal Backend proxy or encrypted token + // is required. + updateUserContext({ + authType: AuthType.ConnectionString, + databaseAccount, + masterKey: config.masterKey, + }); + } else { + // Legacy encrypted-token proxy path (Mongo, Cassandra). + updateUserContext({ + authType: AuthType.EncryptedToken, + accessToken: encodeURIComponent(config.encryptedToken), + databaseAccount, + masterKey: config.masterKey, + }); + } const explorer = new Explorer(); return explorer; } diff --git a/src/hooks/usePortalAccessToken.tsx b/src/hooks/usePortalAccessToken.tsx deleted file mode 100644 index bf59dc63f..000000000 --- a/src/hooks/usePortalAccessToken.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect, useState } from "react"; -import { HttpHeaders } from "../Common/Constants"; -import { configContext } from "../ConfigContext"; -import { AccessInputMetadata } from "../Contracts/DataModels"; - -export async function fetchAccessData(portalToken: string): Promise { - const headers = new Headers(); - // Portal encrypted token API quirk: The token header must be URL encoded - headers.append(HttpHeaders.guestAccessToken, encodeURIComponent(portalToken)); - const url: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/connectionstring/runtimeproxy/accessinputmetadata`; - const options = { - method: "GET", - headers: headers, - }; - - return fetch(url, options) - .then((response) => response.json()) - .catch((error) => console.error(error)); -} - -export function useTokenMetadata(token: string): AccessInputMetadata | undefined { - const [state, setState] = useState(); - - useEffect(() => { - if (token) { - fetchAccessData(token).then((response) => setState(response)); - } - }, [token]); - return state; -} diff --git a/test/fx.ts b/test/fx.ts index 880388a33..a3cad859a 100644 --- a/test/fx.ts +++ b/test/fx.ts @@ -1,8 +1,15 @@ import { DefaultAzureCredential } from "@azure/identity"; import { Frame, Locator, Page, expect } from "@playwright/test"; -import crypto from "crypto"; +import crypto, { webcrypto } from "crypto"; import { TestContainerContext } from "./testData"; +// The @azure/cosmos client signs requests with globalThis.crypto (Web Crypto API). +// In Node.js >= 19 it's already available; only assign the polyfill for older versions. +// This lives in fx.ts (imported by every spec) so the polyfill always runs. +if (!globalThis.crypto) { + Object.defineProperty(globalThis, "crypto", { value: webcrypto, writable: true, configurable: true }); +} + const RETRY_COUNT = 3; export interface TestNameOptions { @@ -41,6 +48,14 @@ export enum TestAccount { SQL = "SQL", SQLReadOnly = "SQLReadOnly", SQLContainerCopyOnly = "SQLContainerCopyOnly", + SQLConnectionString = "SQLConnectionString", + TableConnectionString = "TableConnectionString", + GremlinConnectionString = "GremlinConnectionString", +} + +export enum TestAuthType { + EntraID = "EntraID", + ConnectionString = "ConnectionString", } export function getDefaultAccountName(accountType: TestAccount): string { @@ -66,6 +81,12 @@ export function getDefaultAccountName(accountType: TestAccount): string { return `${accountNamePrefix}-de-test-sql-readonly`; case TestAccount.SQLContainerCopyOnly: return `${accountNamePrefix}-de-test-sql-containercopy`; + case TestAccount.SQLConnectionString: + return `${accountNamePrefix}-de-test-sql-connstring-1`; + case TestAccount.TableConnectionString: + return `${accountNamePrefix}-de-test-table-connstring-1`; + case TestAccount.GremlinConnectionString: + return `${accountNamePrefix}-de-test-gremlin-connstring-1`; case TestAccount.SQL: { const shardIndex = process.env.PLAYWRIGHT_SHARD_INDEX ?? ""; if (!shardIndex) { @@ -96,7 +117,32 @@ function tryGetStandardName(accountType: TestAccount) { } } -export function getAccountName(accountType: TestAccount) { +// Maps a base API account type to its dedicated connection string (account key) account. +const connectionStringAccountTypes: Partial> = { + [TestAccount.SQL]: TestAccount.SQLConnectionString, + [TestAccount.Tables]: TestAccount.TableConnectionString, + [TestAccount.Gremlin]: TestAccount.GremlinConnectionString, +}; + +export function getAccountName(accountType: TestAccount, authType: TestAuthType = TestAuthType.EntraID): string { + // Connection string (account key) login uses dedicated *-connstring accounts that are only + // provisioned in CI (resolved via DE_ACCOUNT_PREFIX). Local runs use DE_TEST_ACCOUNT_PREFIX and + // typically don't have those accounts, so they fall back to the standard API account for the same + // API (which also has key auth enabled). + if (authType === TestAuthType.ConnectionString) { + const connectionStringType = connectionStringAccountTypes[accountType]; + if (!connectionStringType) { + throw new Error(`No connection string account defined for account type ${accountType}`); + } + const override = process.env[`DE_TEST_ACCOUNT_NAME_${connectionStringType.toLocaleUpperCase()}`]; + if (override) { + return override; + } + if (!process.env.DE_TEST_ACCOUNT_PREFIX) { + return getAccountName(connectionStringType); + } + } + return ( process.env[`DE_TEST_ACCOUNT_NAME_${accountType.toLocaleUpperCase()}`] ?? tryGetStandardName(accountType) ?? @@ -210,6 +256,13 @@ export async function getTestExplorerUrl(accountType: TestAccount, options?: Tes params.set("enableaaddataplane", "true"); } break; + + case TestAccount.SQLConnectionString: + case TestAccount.TableConnectionString: + case TestAccount.GremlinConnectionString: + // Connection string (account key) login navigates directly to hostedExplorer.html and doesn't + // use this iframe test-explorer URL or any RBAC/AAD data-plane token. + break; } if (iframeSrc) { diff --git a/test/gremlin/connectionStringLogin.spec.ts b/test/gremlin/connectionStringLogin.spec.ts new file mode 100644 index 000000000..50289d867 --- /dev/null +++ b/test/gremlin/connectionStringLogin.spec.ts @@ -0,0 +1,84 @@ +import { expect, test } from "@playwright/test"; + +import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; +import { CosmosClient, Database } from "@azure/cosmos"; +import { + DataExplorer, + Editor, + ONE_MINUTE_MS, + TestAccount, + TestAuthType, + generateUniqueName, + getAccountName, + getAzureCLICredentials, + resourceGroupName, + subscriptionId, +} from "../fx"; + +const databaseId = generateUniqueName("db"); +const graphId = "testgraph"; +const vertexId = "testvertex"; + +test.describe("Gremlin account using connection string login", () => { + let database: Database = null!; + + test.beforeAll("Seed Test Database", async () => { + const credentials = getAzureCLICredentials(); + const armClient = new CosmosDBManagementClient(credentials, subscriptionId); + const accountName = getAccountName(TestAccount.Gremlin, TestAuthType.ConnectionString); + const account = await armClient.databaseAccounts.get(resourceGroupName, accountName); + const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName); + + // Gremlin graphs are stored as documents, so seed a vertex via the SQL client using Cosmos' internal graph format. + const client = new CosmosClient({ endpoint: account.documentEndpoint!, key: keys.primaryMasterKey }); + database = (await client.databases.createIfNotExists({ id: databaseId })).database; + const { container } = await database.containers.createIfNotExists({ + id: graphId, + partitionKey: { paths: ["/pk"] }, + }); + await container.items.upsert({ id: vertexId, label: "person", pk: "pk1" }); + }); + + test.afterAll("Delete Test Database", async () => { + await database?.delete(); + }); + + test("reads a vertex after connection string login", async ({ page }) => { + const credentials = getAzureCLICredentials(); + const armClient = new CosmosDBManagementClient(credentials, subscriptionId); + const accountName = getAccountName(TestAccount.Gremlin, TestAuthType.ConnectionString); + const account = await armClient.databaseAccounts.get(resourceGroupName, accountName); + const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName); + + // Gremlin signs data-plane requests client-side with the account key, so no encrypted token is issued. + const connectionString = `AccountEndpoint=${account.documentEndpoint};AccountKey=${keys.primaryMasterKey};ApiKind=Gremlin;`; + + await page.goto("https://localhost:1234/hostedExplorer.html"); + const switchConnectionLink = page.getByTestId("Link:SwitchConnectionType"); + await switchConnectionLink.waitFor(); + await switchConnectionLink.click(); + await page.getByPlaceholder("Please enter a connection string").fill(connectionString); + await page.getByRole("button", { name: "Connect" }).click(); + + const explorer = await DataExplorer.waitForExplorer(page); + const graphNode = await explorer.waitForContainerNode(databaseId, graphId); + await graphNode.expand(); + + // Open the Graph node to load the graph explorer, then run the default query to read the seeded vertex. + const graphDataNode = await explorer.waitForNode(`${databaseId}/${graphId}/Graph`); + await graphDataNode.element.click(); + + await explorer.frame.getByRole("button", { name: "Execute Gremlin Query" }).click(); + + // Results open in the Graph view; switch to the JSON view to read the vertex document. + const jsonResultsTab = explorer.frame.getByRole("tab", { name: "JSON" }); + await jsonResultsTab.waitFor({ timeout: ONE_MINUTE_MS }); + await jsonResultsTab.click(); + + const graphJsonEditor = new Editor( + explorer.frame, + explorer.frame.locator(".graphJsonEditor").getByTestId("EditorReact/Host/Loaded"), + ); + await expect.poll(async () => await graphJsonEditor.text(), { timeout: ONE_MINUTE_MS }).toContain(vertexId); + }); +}); diff --git a/test/sql/connectionStringLogin.spec.ts b/test/sql/connectionStringLogin.spec.ts new file mode 100644 index 000000000..8359e6dee --- /dev/null +++ b/test/sql/connectionStringLogin.spec.ts @@ -0,0 +1,118 @@ +import { Page, expect, test } from "@playwright/test"; + +import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; +import { CosmosClient, Database } from "@azure/cosmos"; +import { + DataExplorer, + ONE_MINUTE_MS, + TestAccount, + TestAuthType, + generateUniqueName, + getAccountName, + getAzureCLICredentials, + resourceGroupName, + subscriptionId, +} from "../fx"; + +const databaseId = generateUniqueName("db"); +const containerId = "testcontainer"; +const documentId = "testdoc1"; + +async function loginWithConnectionString(page: Page, connectionString: string): Promise { + await page.goto("https://localhost:1234/hostedExplorer.html"); + const switchConnectionLink = page.getByTestId("Link:SwitchConnectionType"); + await switchConnectionLink.waitFor(); + await switchConnectionLink.click(); + await page.getByPlaceholder("Please enter a connection string").fill(connectionString); + await page.getByRole("button", { name: "Connect" }).click(); +} + +test.describe("SQL account using connection string login", () => { + let database: Database = null!; + let documentEndpoint: string = null!; + // SQL signs data-plane requests client-side with the account key, so no encrypted token is issued. + let connectionString: string = null!; + + test.beforeAll("Seed Test Database", async () => { + const credentials = getAzureCLICredentials(); + const armClient = new CosmosDBManagementClient(credentials, subscriptionId); + const accountName = getAccountName(TestAccount.SQL, TestAuthType.ConnectionString); + const account = await armClient.databaseAccounts.get(resourceGroupName, accountName); + const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName); + documentEndpoint = account.documentEndpoint!; + connectionString = `AccountEndpoint=${documentEndpoint};AccountKey=${keys.primaryMasterKey};`; + + const client = new CosmosClient({ endpoint: documentEndpoint, key: keys.primaryMasterKey }); + database = (await client.databases.createIfNotExists({ id: databaseId })).database; + const { container } = await database.containers.createIfNotExists({ + id: containerId, + partitionKey: { paths: ["/id"] }, + }); + await container.items.upsert({ id: documentId }); + }); + + test.afterAll("Delete Test Database", async () => { + await database?.delete(); + }); + + test("reads a document after connection string login", async ({ page }) => { + await loginWithConnectionString(page, connectionString); + + const explorer = await DataExplorer.waitForExplorer(page); + const collectionNode = await explorer.waitForContainerNode(databaseId, containerId); + await expect(collectionNode.element).toBeAttached(); + await collectionNode.expand(); + + // Open the Items node to load the Documents tab and read the seeded document through the data plane. + const itemsNode = await explorer.waitForContainerItemsNode(databaseId, containerId); + await itemsNode.element.click(); + + const documentsTab = explorer.documentsTab("tab0"); + await documentsTab.documentsFilter.waitFor(); + await documentsTab.documentsListPane.waitFor(); + await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: ONE_MINUTE_MS }); + + const documentRow = documentsTab.documentsListPane.getByText(documentId, { exact: true }).nth(0); + await documentRow.waitFor(); + await documentRow.click(); + await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: ONE_MINUTE_MS }); + + const resultText = await documentsTab.resultsEditor.text(); + expect(resultText).not.toBeNull(); + const resultData = JSON.parse(resultText!); + expect(resultData?.id).toEqual(documentId); + }); + + test("does not call the Portal Backend during login", async ({ page }) => { + // SQL derives the account metadata from the connection string and signs data-plane requests with the + // account key, so neither the encrypted token nor the access metadata endpoint should be hit. + const portalBackendCalls: string[] = []; + page.on("request", (request) => { + if (request.url().includes("/api/connectionstring/")) { + portalBackendCalls.push(request.url()); + } + }); + + await loginWithConnectionString(page, connectionString); + + const explorer = await DataExplorer.waitForExplorer(page); + const collectionNode = await explorer.waitForContainerNode(databaseId, containerId); + await expect(collectionNode.element).toBeAttached(); + + expect(portalBackendCalls).toEqual([]); + }); + + test("opens Data Explorer when the connection string has the wrong account key", async ({ page }) => { + // A well-formed but incorrect base64 account key. The login is accepted + // without checking the key against the account, so the user gets into Data Explorer either way and + // only the data-plane requests made from inside the explorer are rejected. + const wrongKey = "A".repeat(86) + "=="; + await loginWithConnectionString(page, `AccountEndpoint=${documentEndpoint};AccountKey=${wrongKey};`); + + await DataExplorer.waitForExplorer(page); + + // The connect form is replaced by the explorer rather than staying up with a login error. + await expect(page.locator("#connectExplorer")).toHaveCount(0); + await expect(page.locator(".errorDetails")).toHaveCount(0); + }); +}); diff --git a/test/tables/connectionStringLogin.spec.ts b/test/tables/connectionStringLogin.spec.ts new file mode 100644 index 000000000..02db382a2 --- /dev/null +++ b/test/tables/connectionStringLogin.spec.ts @@ -0,0 +1,82 @@ +import { expect, test } from "@playwright/test"; + +import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; +import { Container, CosmosClient } from "@azure/cosmos"; +import { + DataExplorer, + ONE_MINUTE_MS, + TestAccount, + TestAuthType, + generateUniqueName, + getAccountName, + getAzureCLICredentials, + resourceGroupName, + subscriptionId, +} from "../fx"; + +// Table API accounts store tables in a fixed "TablesDB" database, with each table as a container. +const databaseId = "TablesDB"; +const tableId = generateUniqueName("table"); +const partitionKey = "testpartition"; +const rowKey = "testrow"; + +test.describe("Tables account using connection string login", () => { + let container: Container = null!; + + test.beforeAll("Seed Test Table", async () => { + const credentials = getAzureCLICredentials(); + const armClient = new CosmosDBManagementClient(credentials, subscriptionId); + const accountName = getAccountName(TestAccount.Tables, TestAuthType.ConnectionString); + const account = await armClient.databaseAccounts.get(resourceGroupName, accountName); + const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName); + + const client = new CosmosClient({ endpoint: account.documentEndpoint!, key: keys.primaryMasterKey }); + const { database } = await client.databases.createIfNotExists({ id: databaseId }); + container = ( + await database.containers.createIfNotExists({ + id: tableId, + partitionKey: { paths: ["/'$pk'"] }, + }) + ).container; + await container.items.upsert({ $pk: partitionKey, id: rowKey, $id: rowKey }); + }); + + test.afterAll("Delete Test Table", async () => { + // Only remove the table we created; the fixed "TablesDB" database is shared by every table in the + // account, so deleting it would destroy unrelated tables. + await container?.delete(); + }); + + test("reads an entity after connection string login", async ({ page }) => { + const credentials = getAzureCLICredentials(); + const armClient = new CosmosDBManagementClient(credentials, subscriptionId); + const accountName = getAccountName(TestAccount.Tables, TestAuthType.ConnectionString); + const { connectionStrings = [] } = await armClient.databaseAccounts.listConnectionStrings( + resourceGroupName, + accountName, + ); + + // Table accounts sign data-plane requests client-side with the account key, so no encrypted token is issued. + const connectionString = connectionStrings.find((cs) => cs.type === "Table")?.connectionString; + + await page.goto("https://localhost:1234/hostedExplorer.html"); + const switchConnectionLink = page.getByTestId("Link:SwitchConnectionType"); + await switchConnectionLink.waitFor(); + await switchConnectionLink.click(); + await page.getByPlaceholder("Please enter a connection string").fill(connectionString!); + await page.getByRole("button", { name: "Connect" }).click(); + + const explorer = await DataExplorer.waitForExplorer(page); + const tableNode = await explorer.waitForContainerNode(databaseId, tableId); + await tableNode.expand(); + + // Open the Entities node to load the table entities grid and read the seeded entity through the data plane. + const entitiesNode = await explorer.waitForNode(`${databaseId}/${tableId}/Entities`); + await entitiesNode.element.click(); + + const entitiesGrid = explorer.frame.locator("#storageTable"); + await expect(entitiesGrid).toBeVisible({ timeout: ONE_MINUTE_MS }); + await expect(entitiesGrid.getByText(rowKey, { exact: true }).first()).toBeVisible({ timeout: ONE_MINUTE_MS }); + await expect(entitiesGrid.getByText(partitionKey, { exact: true }).first()).toBeVisible(); + }); +}); diff --git a/test/testData.ts b/test/testData.ts index e9ba759f1..57fbfe5a7 100644 --- a/test/testData.ts +++ b/test/testData.ts @@ -9,7 +9,6 @@ import { JSONObject, } from "@azure/cosmos"; import { Buffer } from "node:buffer"; -import { webcrypto } from "node:crypto"; import { generateUniqueName, getAccountName, @@ -19,12 +18,6 @@ import { TestAccount, } from "./fx"; -// In Node.js >= 19, globalThis.crypto is already available as a read-only getter. -// Only assign the polyfill for older versions. -if (!globalThis.crypto) { - Object.defineProperty(globalThis, "crypto", { value: webcrypto, writable: true, configurable: true }); -} - export interface TestItem { id: string; partitionKey: string; diff --git a/tsconfig.strict.json b/tsconfig.strict.json index e1021f162..947b073e5 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -25,6 +25,7 @@ "./src/Common/ObjectCache.ts", "./src/Common/OfferUtility.test.ts", "./src/Common/OfferUtility.ts", + "./src/Common/PortalBackendClient.ts", "./src/Common/Splitter.ts", "./src/Common/ThemeUtility.ts", "./src/Common/UrlUtility.ts", @@ -107,7 +108,6 @@ "./src/hooks/useDirectories.tsx", "./src/hooks/useGraphPhoto.tsx", "./src/hooks/useNotebookSnapshotStore.ts", - "./src/hooks/usePortalAccessToken.tsx", "./src/hooks/useNotificationConsole.ts", "./src/hooks/useObservable.ts", "./src/hooks/useSidePanel.ts", @@ -137,4 +137,4 @@ "src/Shared/Telemetry/**/*", "src/Utils/arm/**/*" ] -} \ No newline at end of file +} 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 37/40] 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 38/40] 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 39/40] 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 40/40] 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",