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
This commit is contained in:
jawelton74
2026-08-26 13:42:14 -07:00
committed by GitHub
parent c375830699
commit f8cfb33618
40 changed files with 122 additions and 2608 deletions
+26 -5
View File
@@ -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 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, retired. The goal is to remove all notebooks/Phoenix/Juno/GitHub-for-notebooks code,
dependencies, UI surfaces, telemetry, and configuration, while **preserving the 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. share the Terminal infrastructure with notebooks.
This document is the implementation plan only. No code changes are made here. This document is the implementation plan only. No code changes are made here.
## Scope decisions (confirmed) ## 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 them to use the **CloudShell** path exclusively. Remove the legacy Phoenix
notebook-server shell path. notebook-server shell path.
- **GitHub integration**: Remove entirely (it exists only for notebook pinned repos). - **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 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 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`, 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. still open.
### Phase 1 — Decouple database shells to CloudShell-only ✅ COMPLETED ### 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. pinned-repo methods.
- Remove GitHub-related localization keys from **all** locale files (`en` + non-English). - 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`, - Delete `src/Phoenix/`, `src/Explorer/Notebook/NotebookContainerClient.ts`,
`src/Explorer/Notebook/NotebookManager.tsx`, `src/Explorer/Notebook/useNotebook.ts`, `src/Explorer/Notebook/NotebookManager.tsx`, `src/Explorer/Notebook/useNotebook.ts`,
`src/Explorer/Notebook/NotebookContentItem.ts`, `src/Explorer/Notebook/NotebookUtil.ts` `src/Explorer/Notebook/NotebookContentItem.ts`, `src/Explorer/Notebook/NotebookUtil.ts`
@@ -279,7 +300,7 @@ npm run format:check
npm test npm test
npm run build:ci 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 ## Notes & considerations
+9 -524
View File
@@ -19,11 +19,7 @@
"@fluentui/react": "8.119.0", "@fluentui/react": "8.119.0",
"@fluentui/react-components": "9.54.2", "@fluentui/react-components": "9.54.2",
"@microsoft/applicationinsights-web": "2.6.1", "@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/markdown": "4.6.0",
"@nteract/myths": "0.1.9",
"@octokit/request": "8.4.1", "@octokit/request": "8.4.1",
"@octokit/rest": "17.9.2", "@octokit/rest": "17.9.2",
"@testing-library/jest-dom": "6.4.6", "@testing-library/jest-dom": "6.4.6",
@@ -92,7 +88,6 @@
"react-window": "1.8.10", "react-window": "1.8.10",
"react-youtube": "9.0.1", "react-youtube": "9.0.1",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12",
"shell-quote": "1.10.0", "shell-quote": "1.10.0",
"styled-components": "5.0.1", "styled-components": "5.0.1",
"swr": "0.4.0", "swr": "0.4.0",
@@ -139,6 +134,7 @@
"@types/sinon": "2.3.3", "@types/sinon": "2.3.3",
"@types/styled-components": "5.1.32", "@types/styled-components": "5.1.32",
"@types/underscore": "1.7.36", "@types/underscore": "1.7.36",
"@types/uuid": "9.0.8",
"@types/youtube-player": "5.5.6", "@types/youtube-player": "5.5.6",
"@typescript-eslint/eslint-plugin": "6.7.4", "@typescript-eslint/eslint-plugin": "6.7.4",
"@typescript-eslint/parser": "6.7.4", "@typescript-eslint/parser": "6.7.4",
@@ -6396,154 +6392,6 @@
"node": ">= 8" "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": { "node_modules/@nteract/markdown": {
"version": "4.6.0", "version": "4.6.0",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
@@ -6568,184 +6416,6 @@
"react-dom": "^16.13.0 || ^17.0.0" "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": { "node_modules/@nteract/presentational-components": {
"version": "3.4.12", "version": "3.4.12",
"resolved": "https://registry.npmjs.org/@nteract/presentational-components/-/presentational-components-3.4.12.tgz", "resolved": "https://registry.npmjs.org/@nteract/presentational-components/-/presentational-components-3.4.12.tgz",
@@ -6763,71 +6433,6 @@
"styled-components": ">= 5.0.1" "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": { "node_modules/@octokit/auth-token": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", "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": { "node_modules/@types/hoist-non-react-statics": {
"version": "3.3.5", "version": "3.3.5",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/react": "*", "@types/react": "*",
@@ -8555,6 +8161,7 @@
}, },
"node_modules/@types/react-redux": { "node_modules/@types/react-redux": {
"version": "7.1.7", "version": "7.1.7",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/hoist-non-react-statics": "^3.3.0", "@types/hoist-non-react-statics": "^3.3.0",
@@ -8678,16 +8285,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/ungap__url-search-params": {
"version": "0.1.2",
"license": "MIT"
},
"node_modules/@types/unist": { "node_modules/@types/unist": {
"version": "2.0.10", "version": "2.0.10",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/uuid": { "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" "license": "MIT"
}, },
"node_modules/@types/ws": { "node_modules/@types/ws": {
@@ -9001,10 +8607,6 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
}, },
"node_modules/@ungap/url-search-params": {
"version": "0.2.2",
"license": "ISC"
},
"node_modules/@webassemblyjs/ast": { "node_modules/@webassemblyjs/ast": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
@@ -12093,6 +11695,7 @@
}, },
"node_modules/debounce": { "node_modules/debounce": {
"version": "1.2.1", "version": "1.2.1",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/debug": { "node_modules/debug": {
@@ -12148,10 +11751,6 @@
} }
} }
}, },
"node_modules/deep-diff": {
"version": "0.3.8",
"license": "MIT"
},
"node_modules/deep-equal": { "node_modules/deep-equal": {
"version": "1.1.2", "version": "1.1.2",
"license": "MIT", "license": "MIT",
@@ -13012,10 +12611,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/escape-carriage": {
"version": "1.3.1",
"license": "MIT"
},
"node_modules/escape-html": { "node_modules/escape-html": {
"version": "1.0.3", "version": "1.0.3",
"dev": true, "dev": true,
@@ -14021,10 +13616,6 @@
"node": "^10.12.0 || >=12.0.0" "node": "^10.12.0 || >=12.0.0"
} }
}, },
"node_modules/file-saver": {
"version": "2.0.5",
"license": "MIT"
},
"node_modules/filesize": { "node_modules/filesize": {
"version": "8.0.7", "version": "8.0.7",
"dev": true, "dev": true,
@@ -14033,13 +13624,6 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/filewatcher": {
"version": "3.0.1",
"license": "MIT",
"dependencies": {
"debounce": "^1.0.0"
}
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -14489,27 +14073,6 @@
"dev": true, "dev": true,
"license": "Unlicense" "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": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"license": "ISC" "license": "ISC"
@@ -14518,6 +14081,7 @@
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"os": [ "os": [
@@ -20659,10 +20223,6 @@
"jquery": ">=1.8.0 <4.0.0" "jquery": ">=1.8.0 <4.0.0"
} }
}, },
"node_modules/js-cookie": {
"version": "2.2.1",
"license": "MIT"
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"license": "MIT" "license": "MIT"
@@ -21090,10 +20650,6 @@
"version": "4.0.3", "version": "4.0.3",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"license": "MIT"
},
"node_modules/lodash.debounce": { "node_modules/lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
"license": "MIT" "license": "MIT"
@@ -21108,10 +20664,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.has": {
"version": "4.5.2",
"license": "MIT"
},
"node_modules/lodash.includes": { "node_modules/lodash.includes": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -23757,42 +23309,6 @@
"@babel/runtime": "^7.9.2" "@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": { "node_modules/reflect-metadata": {
"version": "0.1.13", "version": "0.1.13",
"license": "Apache-2.0" "license": "Apache-2.0"
@@ -24087,10 +23603,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/reselect": {
"version": "4.1.8",
"license": "MIT"
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.8",
"devOptional": true, "devOptional": true,
@@ -24233,29 +23745,6 @@
"version": "1.3.3", "version": "1.3.3",
"license": "BSD-3-Clause" "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": { "node_modules/safe-array-concat": {
"version": "1.1.2", "version": "1.1.2",
"license": "MIT", "license": "MIT",
@@ -26069,10 +25558,6 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/url-join": {
"version": "4.0.1",
"license": "MIT"
},
"node_modules/url-loader": { "node_modules/url-loader": {
"version": "4.1.1", "version": "4.1.1",
"dev": true, "dev": true,
+1 -5
View File
@@ -14,11 +14,7 @@
"@fluentui/react": "8.119.0", "@fluentui/react": "8.119.0",
"@fluentui/react-components": "9.54.2", "@fluentui/react-components": "9.54.2",
"@microsoft/applicationinsights-web": "2.6.1", "@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/markdown": "4.6.0",
"@nteract/myths": "0.1.9",
"@octokit/request": "8.4.1", "@octokit/request": "8.4.1",
"@octokit/rest": "17.9.2", "@octokit/rest": "17.9.2",
"@testing-library/jest-dom": "6.4.6", "@testing-library/jest-dom": "6.4.6",
@@ -87,7 +83,6 @@
"react-window": "1.8.10", "react-window": "1.8.10",
"react-youtube": "9.0.1", "react-youtube": "9.0.1",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rx-jupyter": "5.5.12",
"shell-quote": "1.10.0", "shell-quote": "1.10.0",
"styled-components": "5.0.1", "styled-components": "5.0.1",
"swr": "0.4.0", "swr": "0.4.0",
@@ -149,6 +144,7 @@
"@types/sinon": "2.3.3", "@types/sinon": "2.3.3",
"@types/styled-components": "5.1.32", "@types/styled-components": "5.1.32",
"@types/underscore": "1.7.36", "@types/underscore": "1.7.36",
"@types/uuid": "9.0.8",
"@types/youtube-player": "5.5.6", "@types/youtube-player": "5.5.6",
"@typescript-eslint/eslint-plugin": "6.7.4", "@typescript-eslint/eslint-plugin": "6.7.4",
"@typescript-eslint/parser": "6.7.4", "@typescript-eslint/parser": "6.7.4",
+3 -10
View File
@@ -28,7 +28,6 @@ import { userContext } from "../UserContext";
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils"; import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
import { useSidePanel } from "../hooks/useSidePanel"; import { useSidePanel } from "../hooks/useSidePanel";
import Explorer from "./Explorer"; import Explorer from "./Explorer";
import { useNotebook } from "./Notebook/useNotebook";
import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane"; import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane";
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import StoredProcedure from "./Tree/StoredProcedure"; import StoredProcedure from "./Tree/StoredProcedure";
@@ -120,23 +119,17 @@ export const createCollectionContextMenuButton = (
iconSrc: HostedTerminalIcon, iconSrc: HostedTerminalIcon,
onClick: () => { onClick: () => {
const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection();
if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) { if (userContext.features.enableCloudShell) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else { } else {
selectedCollection && selectedCollection.onNewMongoShellClick(); selectedCollection && selectedCollection.onNewMongoShellClick();
} }
}, },
label: label: userContext.features.enableCloudShell ? t(Keys.contextMenu.openMongoShell) : t(Keys.contextMenu.newShell),
useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell
? t(Keys.contextMenu.openMongoShell)
: t(Keys.contextMenu.newShell),
}); });
} }
if ( if (userContext.features.enableCloudShell && userContext.apiType === "Cassandra") {
(useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) &&
userContext.apiType === "Cassandra"
) {
items.push({ items.push({
iconSrc: HostedTerminalIcon, iconSrc: HostedTerminalIcon,
onClick: () => { onClick: () => {
@@ -109,25 +109,15 @@ exports[`SettingsComponent renders 1`] = `
"computedProperties": [Function], "computedProperties": [Function],
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {}, "databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
@@ -230,25 +220,15 @@ exports[`SettingsComponent renders 1`] = `
"computedProperties": [Function], "computedProperties": [Function],
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {}, "databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
@@ -455,25 +435,15 @@ exports[`SettingsComponent renders 1`] = `
"computedProperties": [Function], "computedProperties": [Function],
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {}, "databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
@@ -526,25 +496,15 @@ exports[`SettingsComponent renders 1`] = `
} }
explorer={ explorer={
Explorer { Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {}, "databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
@@ -694,25 +654,15 @@ exports[`SettingsComponent renders 1`] = `
"computedProperties": [Function], "computedProperties": [Function],
"conflictResolutionPolicy": [Function], "conflictResolutionPolicy": [Function],
"container": Explorer { "container": Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {}, "databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
@@ -765,25 +715,15 @@ exports[`SettingsComponent renders 1`] = `
} }
explorer={ explorer={
Explorer { Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {}, "databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
+15 -15
View File
@@ -10,6 +10,7 @@ import { Capability, DatabaseAccount } from "../Contracts/DataModels";
import { updateUserContext, userContext } from "../UserContext"; import { updateUserContext, userContext } from "../UserContext";
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import Explorer from "./Explorer"; import Explorer from "./Explorer";
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
const mockUpdate = update as jest.MockedFunction<typeof update>; const mockUpdate = update as jest.MockedFunction<typeof update>;
@@ -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", () => { describe("Explorer.openEnableSynapseLinkDialog", () => {
let explorer: Explorer; let explorer: Explorer;
@@ -69,6 +55,7 @@ describe("Explorer.openEnableSynapseLinkDialog", () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
mockUpdate.mockResolvedValue(undefined); mockUpdate.mockResolvedValue(undefined);
useCommandBar.getState().setIsSynapseLinkUpdating(false);
explorer = new Explorer(); explorer = new Explorer();
}); });
@@ -102,6 +89,19 @@ describe("Explorer.openEnableSynapseLinkDialog", () => {
expect(userContext.databaseAccount.properties.enableAnalyticalStorage).toBe(true); 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<ReturnType<typeof update>>;
});
explorer.openEnableSynapseLinkDialog();
const dialogProps = mockOpenDialog.mock.calls[0][0];
await dialogProps.onPrimaryButtonClick();
expect(useCommandBar.getState().isSynapseLinkUpdating).toBe(false);
});
}); });
describe("with targetAccountOverride", () => { describe("with targetAccountOverride", () => {
+5 -220
View File
@@ -1,9 +1,6 @@
import * as msal from "@azure/msal-browser"; 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 { sendMessage } from "Common/MessageHandler";
import { stringifyError } from "Common/stringifyError"; import { stringifyError } from "Common/stringifyError";
import { Platform, configContext } from "ConfigContext";
import { MessageTypes } from "Contracts/ExplorerContracts"; import { MessageTypes } from "Contracts/ExplorerContracts";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { import {
@@ -13,32 +10,27 @@ import {
scheduleRefreshFabricToken, scheduleRefreshFabricToken,
} from "Platform/Fabric/FabricUtil"; } from "Platform/Fabric/FabricUtil";
import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils"; import { acquireMsalTokenForAccount } from "Utils/AuthorizationUtils";
import { allowedNotebookServerUrls, validateEndpoint } from "Utils/EndpointUtils";
import { featureRegistered } from "Utils/FeatureRegistrationUtils"; import { featureRegistered } from "Utils/FeatureRegistrationUtils";
import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts"; import { update } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import * as ko from "knockout"; import * as ko from "knockout";
import React from "react"; import React from "react";
import _ from "underscore"; import _ from "underscore";
import shallow from "zustand/shallow";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
import { getErrorMessage, getErrorStack } from "../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../Common/ErrorHandlingUtils";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { QueriesClient } from "../Common/QueriesClient"; import { QueriesClient } from "../Common/QueriesClient";
import { readCollection } from "../Common/dataAccess/readCollection"; import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases"; import { readDatabases } from "../Common/dataAccess/readDatabases";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import { ContainerConnectionInfo, IPhoenixServiceInfo, IProvisionData, IResponse } from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { UploadDetailsRecord } from "../Contracts/ViewModels"; import { UploadDetailsRecord } from "../Contracts/ViewModels";
import MetricScenario from "../Metrics/MetricEvents"; import MetricScenario from "../Metrics/MetricEvents";
import { ApplicationMetricPhase } from "../Metrics/ScenarioConfig"; import { ApplicationMetricPhase } from "../Metrics/ScenarioConfig";
import { scenarioMonitor } from "../Metrics/ScenarioMonitor"; import { scenarioMonitor } from "../Metrics/ScenarioMonitor";
import { PhoenixClient } from "../Phoenix/PhoenixClient";
import * as ExplorerSettings from "../Shared/ExplorerSettings"; 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 * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext, userContext } from "../UserContext"; import { updateUserContext, userContext } from "../UserContext";
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils"; import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
@@ -49,8 +41,6 @@ import { ReactTabKind, useTabs } from "../hooks/useTabs";
import "./ComponentRegisterer"; import "./ComponentRegisterer";
import { DialogProps, useDialog } from "./Controls/Dialog"; import { DialogProps, useDialog } from "./Controls/Dialog";
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
import type NotebookManager from "./Notebook/NotebookManager";
import { useNotebook } from "./Notebook/useNotebook";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel"; import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel";
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane"; import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
@@ -78,13 +68,7 @@ export default class Explorer {
// Tabs // Tabs
public isTabsContentExpanded: ko.Observable<boolean>; public isTabsContentExpanded: ko.Observable<boolean>;
// Notebooks
public notebookManager?: NotebookManager;
private _isInitializingNotebooks: boolean;
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
public phoenixClient: PhoenixClient;
/** /**
* Resolves when the initial refreshAllDatabases (including collection loading) completes. * Resolves when the initial refreshAllDatabases (including collection loading) completes.
@@ -95,13 +79,6 @@ export default class Explorer {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, { const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree, 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); this.queriesClient = new QueriesClient(this);
@@ -156,31 +133,8 @@ export default class Explorer {
startKey, startKey,
); );
useNotebook.subscribe(
async () => this.initiateAndRefreshNotebookList(),
(state) => [state.isNotebookEnabled, state.isRefreshed],
shallow,
);
this.resourceTree = new ResourceTreeAdapter(this); 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()) { if (isFabricMirrored()) {
useTabs.getState().closeReactTab(ReactTabKind.Home); useTabs.getState().closeReactTab(ReactTabKind.Home);
} }
@@ -188,23 +142,6 @@ export default class Explorer {
this.refreshExplorer(); this.refreshExplorer();
} }
public async initiateAndRefreshNotebookList(): Promise<void> {
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 { public openEnableSynapseLinkDialog(targetAccountOverride?: DataModels.AccountOverride): void {
const subscriptionId = targetAccountOverride?.subscriptionId ?? userContext.subscriptionId; const subscriptionId = targetAccountOverride?.subscriptionId ?? userContext.subscriptionId;
const resourceGroup = targetAccountOverride?.resourceGroup ?? userContext.resourceGroup; const resourceGroup = targetAccountOverride?.resourceGroup ?? userContext.resourceGroup;
@@ -227,7 +164,7 @@ export default class Explorer {
const clearInProgressMessage = logConsoleProgress( 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.", "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(); useDialog.getState().closeDialog();
try { try {
@@ -248,7 +185,7 @@ export default class Explorer {
logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`); logConsoleError(`Enabling Azure Synapse Link for this account failed. ${getErrorMessage(error)}`);
TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, {}, startTime); TelemetryProcessor.traceFailure(Action.EnableAzureSynapseLink, {}, startTime);
} finally { } finally {
useNotebook.getState().setIsSynapseLinkUpdating(false); useCommandBar.getState().setIsSynapseLinkUpdating(false);
} }
}, },
@@ -431,7 +368,6 @@ export default class Explorer {
await (userContext.authType === AuthType.ResourceToken await (userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken() ? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases()); : this.refreshAllDatabases());
await this.refreshNotebookList();
} }
logConsoleInfo("Successfully refreshed databases"); logConsoleInfo("Successfully refreshed databases");
@@ -442,108 +378,6 @@ export default class Explorer {
window.open(Constants.Urls.feedbackEmail, "_blank"); window.open(Constants.Urls.feedbackEmail, "_blank");
}; };
public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise<void> {
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<void> {
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<IPhoenixServiceInfo>,
connectionStatus: DataModels.ContainerConnectionInfo,
): Promise<void> {
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( private getDeltaDatabases(
updatedDatabaseList: DataModels.Database[], updatedDatabaseList: DataModels.Database[],
databases: ViewModels.Database[], databases: ViewModels.Database[],
@@ -637,11 +471,6 @@ export default class Explorer {
} }
} }
private refreshNotebookList = (): Promise<void> => {
// Notebook authoring and listing have been removed.
return Promise.resolve();
};
public openNotebookTerminal(kind: ViewModels.TerminalKind): void { public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
this.connectToNotebookTerminal(kind); this.connectToNotebookTerminal(kind);
} }
@@ -742,24 +571,6 @@ export default class Explorer {
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />); .openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
} }
public getDownloadModalContent(fileName: string): JSX.Element {
if (useNotebook.getState().isPhoenixNotebooks) {
return (
<>
<p>{Notebook.galleryNotebookDownloadContent1}</p>
<br />
<p>
{Notebook.galleryNotebookDownloadContent2}
<Link href={Notebook.cosmosNotebookGitDocumentationUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
return <p> Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook. </p>;
}
public async refreshExplorer(): Promise<void> { public async refreshExplorer(): Promise<void> {
// Start DatabaseLoad scenario before fetching databases // Start DatabaseLoad scenario before fetching databases
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") { if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
@@ -768,8 +579,7 @@ export default class Explorer {
// Run independent initialization tasks in parallel: // Run independent initialization tasks in parallel:
// - Database loading (ARM/SDK calls for databases + collections) // - 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)
// - Feature registration check (ARM call — no dependency on databases or notebooks)
const databasesTask = const databasesTask =
userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo" userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo"
? (async () => { ? (async () => {
@@ -786,37 +596,12 @@ export default class Explorer {
})() })()
: Promise.resolve(); : Promise.resolve();
const notebooksTask = !isFabricNative()
? useNotebook.getState().refreshNotebooksEnabledStateForAccount()
: Promise.resolve();
const featureRegistrationTask = const featureRegistrationTask =
userContext.authType === AuthType.AAD && userContext.apiType === "SQL" && !isFabricNative() userContext.authType === AuthType.AAD && userContext.apiType === "SQL" && !isFabricNative()
? featureRegistered(userContext.subscriptionId, "ThroughputBucketing") ? featureRegistered(userContext.subscriptionId, "ThroughputBucketing")
: Promise.resolve(false); : Promise.resolve(false);
const [, , throughputBucketsEnabled] = await Promise.all([databasesTask, notebooksTask, featureRegistrationTask]); const [, throughputBucketsEnabled] = await Promise.all([databasesTask, 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);
}
if (throughputBucketsEnabled) { if (throughputBucketsEnabled) {
updateUserContext({ throughputBucketsEnabled }); updateUserContext({ throughputBucketsEnabled });
@@ -5,14 +5,12 @@
*/ */
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react"; import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import { makeStyles, useFluent } from "@fluentui/react-components"; import { makeStyles, useFluent } from "@fluentui/react-components";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane"; import { useDataPlaneRbac } from "Explorer/Panes/SettingsPane/SettingsPane";
import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; import { KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts";
import { isFabric } from "Platform/Fabric/FabricUtil"; import { isFabric } from "Platform/Fabric/FabricUtil";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import * as React from "react"; import * as React from "react";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import { ConnectionStatusType } from "../../../Common/Constants";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
@@ -28,6 +26,8 @@ export interface CommandBarStore {
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void; setContextButtons: (contextButtons: CommandButtonComponentProps[]) => void;
isHidden: boolean; isHidden: boolean;
setIsHidden: (isHidden: boolean) => void; setIsHidden: (isHidden: boolean) => void;
isSynapseLinkUpdating: boolean;
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => void;
} }
export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({ export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
@@ -35,6 +35,8 @@ export const useCommandBar: UseStore<CommandBarStore> = create((set) => ({
setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })), setContextButtons: (contextButtons: CommandButtonComponentProps[]) => set((state) => ({ ...state, contextButtons })),
isHidden: false, isHidden: false,
setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })), setIsHidden: (isHidden: boolean) => set((state) => ({ ...state, isHidden })),
isSynapseLinkUpdating: false,
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
})); }));
const useStyles = makeStyles({ const useStyles = makeStyles({
@@ -69,26 +71,25 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const selectedNodeState = useSelectedNode(); const selectedNodeState = useSelectedNode();
const buttons = useCommandBar((state) => state.contextButtons); const buttons = useCommandBar((state) => state.contextButtons);
const isHidden = useCommandBar((state) => state.isHidden); const isHidden = useCommandBar((state) => state.isHidden);
const isSynapseLinkUpdating = useCommandBar((state) => state.isSynapseLinkUpdating);
// targetDocument is used by referenced components // targetDocument is used by referenced components
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { targetDocument } = useFluent(); const { targetDocument } = useFluent();
const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR); const setKeyboardHandlers = useKeyboardActionGroup(KeyboardActionGroup.COMMAND_BAR);
const styles = useStyles(); 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 // Subscribe to the store changes that affect button creation
const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled); const dataPlaneRbacEnabled = useDataPlaneRbac((state) => state.dataPlaneRbacEnabled);
const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated); const aadTokenUpdated = useDataPlaneRbac((state) => state.aadTokenUpdated);
// Memoize the expensive button creation // Memoize the expensive button creation
const staticButtons = React.useMemo(() => { const staticButtons = React.useMemo(() => {
return CommandBarComponentButtonFactory.createStaticCommandBarButtons(container, selectedNodeState); return CommandBarComponentButtonFactory.createStaticCommandBarButtons(
}, [container, selectedNodeState, dataPlaneRbacEnabled, aadTokenUpdated]); container,
selectedNodeState,
isSynapseLinkUpdating,
);
}, [container, selectedNodeState, dataPlaneRbacEnabled, aadTokenUpdated, isSynapseLinkUpdating]);
if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") { if (userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo") {
const buttons = const buttons =
@@ -134,11 +135,6 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, "var(--colorNeutralBackground1)"); const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, "var(--colorNeutralBackground1)");
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true)); 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 = { const rootStyle = {
root: { root: {
backgroundColor: "var(--colorNeutralBackground1)", backgroundColor: "var(--colorNeutralBackground1)",
@@ -4,7 +4,6 @@ import { DatabaseAccount } from "../../../Contracts/DataModels";
import { CollectionBase } from "../../../Contracts/ViewModels"; import { CollectionBase } from "../../../Contracts/ViewModels";
import { updateUserContext } from "../../../UserContext"; import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import { useDatabases } from "../../useDatabases"; import { useDatabases } from "../../useDatabases";
import { useSelectedNode } from "../../useSelectedNode"; import { useSelectedNode } from "../../useSelectedNode";
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory"; import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
@@ -37,6 +36,18 @@ describe("CommandBarComponentButtonFactory tests", () => {
expect(enableAzureSynapseLinkBtn).toBeDefined(); 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 // 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 // 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. // 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", () => { it("Cassandra Api not available - button should be hidden", () => {
updateUserContext({ updateUserContext({
databaseAccount: { databaseAccount: {
@@ -127,7 +133,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
expect(openCassandraShellBtn).toBeUndefined(); 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 buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel); const openCassandraShellBtn = buttons.find((button) => button.commandButtonLabel === openCassandraShellBtnLabel);
expect(openCassandraShellBtn).toBeUndefined(); expect(openCassandraShellBtn).toBeUndefined();
@@ -30,7 +30,6 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "../../Controls/Dialog"; import { useDialog } from "../../Controls/Dialog";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane"; import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane"; import { LoadQueryPane } from "../../Panes/LoadQueryPane/LoadQueryPane";
import { SettingsPane, useDataPlaneRbac } from "../../Panes/SettingsPane/SettingsPane"; import { SettingsPane, useDataPlaneRbac } from "../../Panes/SettingsPane/SettingsPane";
@@ -43,6 +42,7 @@ let counter = 0;
export function createStaticCommandBarButtons( export function createStaticCommandBarButtons(
container: Explorer, container: Explorer,
selectedNodeState: SelectedNodeState, selectedNodeState: SelectedNodeState,
isSynapseLinkUpdating = false,
): CommandButtonComponentProps[] { ): CommandButtonComponentProps[] {
if (userContext.authType === AuthType.ResourceToken) { if (userContext.authType === AuthType.ResourceToken) {
return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState); return createStaticCommandBarButtonsForResourceToken(container, selectedNodeState);
@@ -62,7 +62,7 @@ export function createStaticCommandBarButtons(
userContext.apiType !== "Tables" && userContext.apiType !== "Tables" &&
userContext.apiType !== "Cassandra" userContext.apiType !== "Cassandra"
) { ) {
const addSynapseLink = createOpenSynapseLinkDialogButton(container); const addSynapseLink = createOpenSynapseLinkDialogButton(container, isSynapseLinkUpdating);
if (addSynapseLink) { if (addSynapseLink) {
addDivider(); addDivider();
buttons.push(addSynapseLink); buttons.push(addSynapseLink);
@@ -136,14 +136,13 @@ export function createContextCommandBarButtons(
const buttons: CommandButtonComponentProps[] = []; const buttons: CommandButtonComponentProps[] = [];
if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") { if (!selectedNodeState.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
const label = const label = userContext.features.enableCloudShell ? "Open Mongo Shell" : "New Shell";
useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell ? "Open Mongo Shell" : "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = { const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon, iconSrc: HostedTerminalIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => { onCommandClick: () => {
const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection(); const selectedCollection: ViewModels.Collection = selectedNodeState.findSelectedCollection();
if (useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) { if (userContext.features.enableCloudShell) {
container.openNotebookTerminal(ViewModels.TerminalKind.Mongo); container.openNotebookTerminal(ViewModels.TerminalKind.Mongo);
} else { } else {
selectedCollection && selectedCollection.onNewMongoShellClick(); selectedCollection && selectedCollection.onNewMongoShellClick();
@@ -157,7 +156,7 @@ export function createContextCommandBarButtons(
} }
if ( if (
(useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell) && userContext.features.enableCloudShell &&
!selectedNodeState.isDatabaseNodeOrNoneSelected() && !selectedNodeState.isDatabaseNodeOrNoneSelected() &&
userContext.apiType === "Cassandra" 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) { if (configContext.platform === Platform.Emulator) {
return undefined; return undefined;
} }
@@ -273,7 +275,7 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
onCommandClick: () => container.openEnableSynapseLinkDialog(), onCommandClick: () => container.openEnableSynapseLinkDialog(),
commandButtonLabel: label, commandButtonLabel: label,
hasPopup: false, hasPopup: false,
disabled: useNotebook.getState().isSynapseLinkUpdating, disabled: isSynapseLinkUpdating,
ariaLabel: label, ariaLabel: label,
}; };
} }
@@ -514,14 +516,9 @@ function createOpenTerminalButtonByKind(
} }
}; };
const label = `Open ${terminalFriendlyName()} Shell`; const label = `Open ${terminalFriendlyName()} Shell`;
const tooltip = const tooltip = "Cloud Shell is not available for this account.";
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const isNativeAuthDisabled = terminalKind === ViewModels.TerminalKind.VCoreMongo && isVCoreMongoNativeAuthDisabled(); const isNativeAuthDisabled = terminalKind === ViewModels.TerminalKind.VCoreMongo && isVCoreMongoNativeAuthDisabled();
const disableButton = const disableButton = !userContext.features.enableCloudShell || isNativeAuthDisabled;
(!useNotebook.getState().isNotebooksEnabledForAccount &&
!useNotebook.getState().isNotebookEnabled &&
!userContext.features.enableCloudShell) ||
isNativeAuthDisabled;
return { return {
iconSrc: HostedTerminalIcon, iconSrc: HostedTerminalIcon,
iconAlt: label, iconAlt: label,
@@ -533,7 +530,7 @@ function createOpenTerminalButtonByKind(
}); });
return; return;
} }
if (useNotebook.getState().isNotebookEnabled || userContext.features.enableCloudShell) { if (userContext.features.enableCloudShell) {
container.openNotebookTerminal(terminalKind); container.openNotebookTerminal(terminalKind);
} }
}, },
@@ -17,9 +17,6 @@ import { configContext, Platform } from "../../../ConfigContext";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; 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 * 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: () => <MemoryTracker />,
};
};
export const createConnectionStatus = (container: Explorer, key: string): ICommandBarItemProps => {
return {
key,
onRender: () => <ConnectionStatus container={container} />,
};
};
export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap { export function createKeyboardHandlers(allButtons: CommandButtonComponentProps[]): KeyboardHandlerMap {
const handlers: KeyboardHandlerMap = {}; const handlers: KeyboardHandlerMap = {};
@@ -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);
}
}
@@ -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<Props> = ({ 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<boolean>(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 (
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
<TooltipHost content={toolTipContent}>
<Stack className="connectionStatusContainer" horizontal>
<Icon iconName="ConnectVirtualMachine" className="connectIcon" />
<span>{connectionInfo.status}</span>
</Stack>
</TooltipHost>
</ActionButton>
);
}
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 (
<>
<TooltipHost
content={
containerInfo?.status === ContainerStatusType.Active
? `Connected to temporary workspace. This temporary workspace will get disconnected in ${Math.round(
containerInfo.durationLeftInMinutes,
)} minutes.`
: toolTipContent
}
>
<ActionButton
id={buttonId}
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
}
>
<Stack className="connectionStatusContainer" horizontal>
<i className={statusColor}></i>
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
{connectionInfo.status}
</span>
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
<ProgressIndicator description={minute + ":" + second} />
)}
{connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
<ProgressIndicator
className={totalGB !== 0 && usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={totalGB !== 0 ? usedGB / totalGB : 0}
/>
)}
</Stack>
{!isBarDismissed &&
containerInfo.status &&
containerInfo.status === ContainerStatusType.Active &&
Math.round(containerInfo.durationLeftInMinutes) <= Notebook.remainingTimeForAlert ? (
<FocusTrapCallout
role="alertdialog"
className={styles.callout}
gapSpace={0}
target={`#${buttonId}`}
onDismiss={() => setIsBarDismissed(true)}
setInitialFocus
>
<Text block variant="xLarge" className={styles.title}>
Remaining Time
</Text>
<Text block variant="small">
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.
</Text>
<FocusZone handleTabKey={FocusZoneTabbableElements.all} isCircularNavigation>
<Stack className={styles.buttons} gap={8} horizontal>
<DefaultButton onClick={() => setIsBarDismissed(true)}>Dimiss</DefaultButton>
</Stack>
</FocusZone>
</FocusTrapCallout>
) : undefined}
</ActionButton>
</TooltipHost>
</>
);
};
@@ -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;
}
}
}
@@ -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 (
<Stack className="memoryTrackerContainer" horizontal>
<span>Memory</span>
<Spinner size={SpinnerSize.medium} />
</Stack>
);
}
const totalGB = memoryUsageInfo.totalKB / 1048576;
const usedGB = totalGB - memoryUsageInfo.freeKB / 1048576;
return (
<Stack className="memoryTrackerContainer" horizontal>
<span>Memory</span>
<ProgressIndicator
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
percentComplete={usedGB / totalGB}
/>
</Stack>
);
};
@@ -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<DataModels.MemoryUsageInfo> {
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<DataModels.MemoryUsageInfo> {
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<IResponse<IPhoenixServiceInfo>> {
this.isResettingWorkspace = true;
let response: IResponse<IPhoenixServiceInfo>;
try {
response = await this._resetWorkspace();
} catch (error) {
Promise.reject(error);
return response;
}
this.isResettingWorkspace = false;
return response;
}
private async _resetWorkspace(): Promise<IResponse<IPhoenixServiceInfo>> {
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",
};
}
}
@@ -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,
}
-32
View File
@@ -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),
);
}
}
@@ -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"));
});
});
});
-81
View File
@@ -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}`;
}
}
-248
View File
@@ -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<void>;
findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem;
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem) => void;
updateNotebookItem: (item: NotebookContentItem) => void;
deleteNotebookItem: (item: NotebookContentItem) => void;
initializeNotebooksTree: () => Promise<void>;
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void;
setIsAllocating: (isAllocating: boolean) => void;
resetContainerConnection: (connectionStatus: ContainerConnectionInfo) => void;
setIsRefreshed: (isAllocating: boolean) => void;
setContainerStatus: (containerStatus: ContainerInfo) => void;
getPhoenixStatus: () => Promise<void>;
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => void;
setIsPhoenixFeatures: (isPhoenixFeatures: boolean) => void;
}
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
isNotebookEnabled: false,
isNotebooksEnabledForAccount: false,
notebookServerInfo: {
notebookServerEndpoint: undefined,
authToken: undefined,
forwardingId: undefined,
},
sparkClusterConnectionInfo: {
userName: undefined,
password: undefined,
endpoints: [],
},
isSynapseLinkUpdating: false,
memoryUsageInfo: undefined,
isShellEnabled: false,
notebookBasePath: Constants.Notebook.defaultBasePath,
isInitializingNotebooks: false,
myNotebooksContentRoot: undefined,
galleryContentRoot: undefined,
connectionInfo: {
status: ConnectionStatusType.Connect,
},
notebookFolderName: undefined,
isAllocating: false,
isRefreshed: false,
containerStatus: {
status: undefined,
durationLeftInMinutes: undefined,
phoenixServerInfo: undefined,
},
isPhoenixNotebooks: undefined,
isPhoenixFeatures: undefined,
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
set({ notebookServerInfo }),
setSparkClusterConnectionInfo: (sparkClusterConnectionInfo: DataModels.SparkClusterConnectionInfo) =>
set({ sparkClusterConnectionInfo }),
setIsSynapseLinkUpdating: (isSynapseLinkUpdating: boolean) => set({ isSynapseLinkUpdating }),
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
await get().getPhoenixStatus();
const { databaseAccount, authType } = userContext;
if (
authType === AuthType.EncryptedToken ||
authType === AuthType.ResourceToken ||
authType === AuthType.MasterKey
) {
set({ isNotebooksEnabledForAccount: false });
return;
}
const firstWriteLocation =
userContext.apiType === "Postgres" || userContext.apiType === "VCoreMongo"
? databaseAccount?.location
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`;
const authorizationHeader = getAuthorizationHeader();
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<void> => {
const notebookFolderName = get().isPhoenixNotebooks ? "Temporary Notebooks" : "My Notebooks";
set({ notebookFolderName });
const myNotebooksContentRoot = {
name: get().notebookFolderName,
path: get().notebookBasePath,
type: NotebookContentItemType.Directory,
};
const galleryContentRoot = {
name: "Gallery",
path: "Gallery",
type: NotebookContentItemType.File,
};
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 }),
}));
@@ -145,25 +145,15 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
<AnalyticalStoreComponent <AnalyticalStoreComponent
explorer={ explorer={
Explorer { Explorer {
"_isInitializingNotebooks": false,
"databasesRefreshed": Promise {}, "databasesRefreshed": Promise {},
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function], "isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function], "onRefreshDatabasesKeyPress": [Function],
"onRefreshResourcesClick": [Function], "onRefreshResourcesClick": [Function],
"phoenixClient": PhoenixClient {
"armResourceId": undefined,
"retryOptions": {
"maxTimeout": 5000,
"minTimeout": 5000,
"retries": 3,
},
},
"provideFeedbackEmail": [Function], "provideFeedbackEmail": [Function],
"queriesClient": QueriesClient { "queriesClient": QueriesClient {
"container": [Circular], "container": [Circular],
}, },
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter { "resourceTree": ResourceTreeAdapter {
"container": [Circular], "container": [Circular],
"parameters": [Function], "parameters": [Function],
@@ -43,7 +43,6 @@ import { getCollectionName } from "../../Utils/APITypeUtils";
import { useDialog } from "../Controls/Dialog"; import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity"; import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
import { useNotebook } from "../Notebook/useNotebook";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
@@ -188,12 +187,6 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
React.useEffect(() => { React.useEffect(() => {
subscriptions.push( subscriptions.push(
{
dispose: useNotebook.subscribe(
() => setState({}),
(state) => state.isNotebookEnabled,
),
},
{ dispose: useSelectedNode.subscribe(() => setState({})) }, { dispose: useSelectedNode.subscribe(() => setState({})) },
{ {
dispose: useCarousel.subscribe( dispose: useCarousel.subscribe(
@@ -35,15 +35,6 @@ import * as ViewModels from "../../../Contracts/ViewModels";
import { updateUserContext } from "../../../UserContext"; import { updateUserContext } from "../../../UserContext";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
jest.mock("rx-jupyter", () => ({
sessions: {
create: jest.fn(),
},
contents: {
JupyterContentProvider: jest.fn().mockImplementation(() => ({})),
},
}));
jest.mock("Common/dataAccess/queryDocuments", () => ({ jest.mock("Common/dataAccess/queryDocuments", () => ({
queryDocuments: jest.fn(() => ({ queryDocuments: jest.fn(() => ({
// Omit headers, because we can't mock a private field and we don't need to test it // Omit headers, because we can't mock a private field and we don't need to test it
@@ -19,15 +19,6 @@ import { act } from "react-dom/test-utils";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
jest.mock("rx-jupyter", () => ({
sessions: {
create: jest.fn(),
},
contents: {
JupyterContentProvider: jest.fn().mockImplementation(() => ({})),
},
}));
jest.requireActual("Explorer/Controls/Editor/EditorReact"); jest.requireActual("Explorer/Controls/Editor/EditorReact");
const PROPERTY_VALUE = "__SOME_PROPERTY_VALUE__"; const PROPERTY_VALUE = "__SOME_PROPERTY_VALUE__";
-1
View File
@@ -5,7 +5,6 @@ import {
TriggerDefinition, TriggerDefinition,
UserDefinedFunctionDefinition, UserDefinedFunctionDefinition,
} from "@azure/cosmos"; } from "@azure/cosmos";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; import { DocumentsTabV2 } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2";
import { isFabricMirrored } from "Platform/Fabric/FabricUtil"; import { isFabricMirrored } from "Platform/Fabric/FabricUtil";
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils"; import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
-9
View File
@@ -6,15 +6,6 @@ import { updateUserContext, userContext } from "../../UserContext";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import Database from "./Database"; import Database from "./Database";
jest.mock("rx-jupyter", () => ({
sessions: {
create: jest.fn(),
},
contents: {
JupyterContentProvider: jest.fn().mockImplementation(() => ({})),
},
}));
const createMockContainer = (): Explorer => { const createMockContainer = (): Explorer => {
const mockContainer = new Explorer(); const mockContainer = new Explorer();
return mockContainer; return mockContainer;
+2 -29
View File
@@ -18,12 +18,8 @@ import { userContext } from "UserContext";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
import * as React from "react"; import * as React from "react";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import shallow from "zustand/shallow";
import { useDatabaseLoadScenario } from "../../Metrics/useMetricPhases"; import { useDatabaseLoadScenario } from "../../Metrics/useMetricPhases";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useNotebook } from "../Notebook/useNotebook";
export const MyNotebooksTitle = "My Notebooks";
interface ResourceTreeProps { interface ResourceTreeProps {
explorer: Explorer; explorer: Explorer;
@@ -40,13 +36,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
const [openItems, setOpenItems] = React.useState<TreeItemValue[]>([]); const [openItems, setOpenItems] = React.useState<TreeItemValue[]>([]);
const treeStyles = useTreeStyles(); const treeStyles = useTreeStyles();
const { isNotebookEnabled } = useNotebook(
(state) => ({
isNotebookEnabled: state.isNotebookEnabled,
}),
shallow,
);
// We intentionally avoid using a state selector here because we want to re-render the tree if the active tab changes. // We intentionally avoid using a state selector here because we want to re-render the tree if the active tab changes.
const { refreshActiveTab } = useTabs(); const { refreshActiveTab } = useTabs();
@@ -65,24 +54,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ explorer }: Resource
const databaseTreeNodes = useMemo(() => { const databaseTreeNodes = useMemo(() => {
return userContext.authType === AuthType.ResourceToken return userContext.authType === AuthType.ResourceToken
? createResourceTokenTreeNodes(resourceTokenCollection) ? createResourceTokenTreeNodes(resourceTokenCollection)
: createDatabaseTreeNodes( : createDatabaseTreeNodes(explorer, databases, refreshActiveTab, searchText, sortOrder, pinnedDatabaseIds);
explorer, }, [resourceTokenCollection, databases, refreshActiveTab, searchText, sortOrder, pinnedDatabaseIds]);
isNotebookEnabled,
databases,
refreshActiveTab,
searchText,
sortOrder,
pinnedDatabaseIds,
);
}, [
resourceTokenCollection,
databases,
isNotebookEnabled,
refreshActiveTab,
searchText,
sortOrder,
pinnedDatabaseIds,
]);
const headerNodes: TreeNode[] = isFabricMirrored() const headerNodes: TreeNode[] = isFabricMirrored()
? [] ? []
-148
View File
@@ -1,34 +1,20 @@
import { TreeNodeMenuItem } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity"; import { collectionWasOpened } from "Explorer/MostRecentActivity/MostRecentActivity";
import { shouldShowScriptNodes } from "Explorer/Tree/treeNodeUtil"; import { shouldShowScriptNodes } from "Explorer/Tree/treeNodeUtil";
import { getItemName } from "Utils/APITypeUtils"; import { getItemName } from "Utils/APITypeUtils";
import * as ko from "knockout"; import * as ko from "knockout";
import * as React from "react"; import * as React from "react";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; 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 CollectionIcon from "../../../images/tree-collection.svg";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { isServerlessAccount } from "../../Utils/CapabilityUtils"; import { isServerlessAccount } from "../../Utils/CapabilityUtils";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import { useDialog } from "../Controls/Dialog";
import { LegacyTreeComponent, LegacyTreeNode } from "../Controls/TreeComponent/LegacyTreeComponent"; import { LegacyTreeComponent, LegacyTreeNode } from "../Controls/TreeComponent/LegacyTreeComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; 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 TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases"; import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode"; import { useSelectedNode } from "../useSelectedNode";
@@ -37,16 +23,10 @@ import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction"; import UserDefinedFunction from "./UserDefinedFunction";
export class ResourceTreeAdapter implements ReactAdapter { export class ResourceTreeAdapter implements ReactAdapter {
public static readonly MyNotebooksTitle = "My Notebooks";
private static readonly DataTitle = "DATA"; private static readonly DataTitle = "DATA";
private static readonly NotebooksTitle = "NOTEBOOKS";
private static readonly PseudoDirPath = "PsuedoDir";
public parameters: ko.Observable<number>; public parameters: ko.Observable<number>;
public myNotebooksContentRoot: NotebookContentItem;
public constructor(private container: Explorer) { public constructor(private container: Explorer) {
this.parameters = ko.observable(Date.now()); this.parameters = ko.observable(Date.now());
@@ -55,56 +35,15 @@ export class ResourceTreeAdapter implements ReactAdapter {
() => this.triggerRender(), () => this.triggerRender(),
(state) => state.activeTab, (state) => state.activeTab,
); );
useNotebook.subscribe(
() => this.triggerRender(),
(state) => state.isNotebookEnabled,
);
useDatabases.subscribe(() => this.triggerRender()); useDatabases.subscribe(() => this.triggerRender());
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 { public renderComponent(): JSX.Element {
const dataRootNode = this.buildDataTree(); const dataRootNode = this.buildDataTree();
return <LegacyTreeComponent className="dataResourceTree" rootNode={dataRootNode} />; return <LegacyTreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
} }
public async initialize(): Promise<void[]> {
const refreshTasks: Promise<void>[] = [];
this.myNotebooksContentRoot = {
name: useNotebook.getState().notebookFolderName,
path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory,
};
return Promise.all(refreshTasks);
}
private buildDataTree(): LegacyTreeNode { private buildDataTree(): LegacyTreeNode {
const databaseTreeNodes: LegacyTreeNode[] = useDatabases const databaseTreeNodes: LegacyTreeNode[] = useDatabases
.getState() .getState()
@@ -403,93 +342,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
return traverse(schema); 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() { public triggerRender() {
window.requestAnimationFrame(() => this.parameters(Date.now())); window.requestAnimationFrame(() => this.parameters(Date.now()));
} }
@@ -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": [ "children": [
+20 -36
View File
@@ -4,7 +4,6 @@ import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent"; import { TreeNode } from "Explorer/Controls/TreeComponent/TreeNodeComponent";
import Explorer from "Explorer/Explorer"; import Explorer from "Explorer/Explorer";
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { DeleteDatabaseConfirmationPanel } from "Explorer/Panes/DeleteDatabaseConfirmationPanel"; import { DeleteDatabaseConfirmationPanel } from "Explorer/Panes/DeleteDatabaseConfirmationPanel";
import TabsBase from "Explorer/Tabs/TabsBase"; import TabsBase from "Explorer/Tabs/TabsBase";
import StoredProcedure from "Explorer/Tree/StoredProcedure"; import StoredProcedure from "Explorer/Tree/StoredProcedure";
@@ -363,7 +362,7 @@ describe("createDatabaseTreeNodes", () => {
}, },
} as never, } as never,
}); });
nodes = createDatabaseTreeNodes(explorer, false, useDatabases.getState().databases, refreshActiveTab, ""); nodes = createDatabaseTreeNodes(explorer, useDatabases.getState().databases, refreshActiveTab, "");
}); });
it("creates expected tree", () => { it("creates expected tree", () => {
@@ -371,11 +370,10 @@ describe("createDatabaseTreeNodes", () => {
}); });
}); });
it.each<[string, Platform, boolean, Partial<DataModels.DatabaseAccountExtendedProperties>, Partial<UserContext>]>([ it.each<[string, Platform, Partial<DataModels.DatabaseAccountExtendedProperties>, Partial<UserContext>]>([
[ [
"the SQL API, on Fabric read-only (mirrored)", "the SQL API, on Fabric read-only (mirrored)",
Platform.Fabric, Platform.Fabric,
false,
{ capabilities: [], enableMultipleWriteLocations: true }, { capabilities: [], enableMultipleWriteLocations: true },
{ {
fabricContext: { fabricContext: {
@@ -387,7 +385,6 @@ describe("createDatabaseTreeNodes", () => {
[ [
"the SQL API, on Fabric non read-only (native)", "the SQL API, on Fabric non read-only (native)",
Platform.Fabric, Platform.Fabric,
false,
{ capabilities: [], enableMultipleWriteLocations: true }, { capabilities: [], enableMultipleWriteLocations: true },
{ {
fabricContext: { fabricContext: {
@@ -399,7 +396,6 @@ describe("createDatabaseTreeNodes", () => {
[ [
"the SQL API, on Portal", "the SQL API, on Portal",
Platform.Portal, Platform.Portal,
false,
{ capabilities: [], enableMultipleWriteLocations: true }, { capabilities: [], enableMultipleWriteLocations: true },
{ {
fabricContext: undefined, fabricContext: undefined,
@@ -408,7 +404,6 @@ describe("createDatabaseTreeNodes", () => {
[ [
"the Cassandra API, serverless, on Hosted", "the Cassandra API, serverless, on Hosted",
Platform.Hosted, Platform.Hosted,
false,
{ {
capabilities: [ capabilities: [
{ name: CapabilityNames.EnableCassandra, description: "" }, { name: CapabilityNames.EnableCassandra, description: "" },
@@ -418,45 +413,34 @@ describe("createDatabaseTreeNodes", () => {
{ fabricContext: undefined }, { fabricContext: undefined },
], ],
[ [
"the Mongo API, with Notebooks and Phoenix features, on Emulator", "the Mongo API, on Emulator",
Platform.Emulator, Platform.Emulator,
true,
{ {
capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }], capabilities: [{ name: CapabilityNames.EnableMongo, description: "" }],
}, },
{ fabricContext: undefined }, { fabricContext: undefined },
], ],
])( ])("generates the correct tree structure for %s", (_, platform, dbAccountProperties, userContext) => {
"generates the correct tree structure for %s", updateConfigContext({ platform });
(_, platform, isNotebookEnabled, dbAccountProperties, userContext) => { updateUserContext({
useNotebook.setState({ isPhoenixFeatures: isNotebookEnabled }); ...userContext,
updateConfigContext({ platform }); databaseAccount: {
updateUserContext({ properties: {
...userContext, enableMultipleWriteLocations: true,
databaseAccount: { ...dbAccountProperties,
properties: { },
enableMultipleWriteLocations: true, } as unknown as DataModels.DatabaseAccount,
...dbAccountProperties, });
}, const nodes = createDatabaseTreeNodes(explorer, useDatabases.getState().databases, refreshActiveTab, "");
} as unknown as DataModels.DatabaseAccount, expect(nodes).toMatchSnapshot();
}); });
const nodes = createDatabaseTreeNodes(
explorer,
isNotebookEnabled,
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. // 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. // 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. // 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", () => { 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()); const giganticDbNode = nodes.find((node) => node.label === giganticDb.id());
expect(giganticDbNode).toBeDefined(); expect(giganticDbNode).toBeDefined();
expect(giganticDbNode.children.map((node) => node.label)).toStrictEqual(["schemaCollection", "load more"]); expect(giganticDbNode.children.map((node) => node.label)).toStrictEqual(["schemaCollection", "load more"]);
@@ -488,7 +472,7 @@ describe("createDatabaseTreeNodes", () => {
}, },
} as unknown as DataModels.DatabaseAccount, } 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()); standardDbNode = nodes.find((node) => node.label === standardDb.id());
sharedDbNode = nodes.find((node) => node.label === sharedDb.id()); sharedDbNode = nodes.find((node) => node.label === sharedDb.id());
giganticDbNode = nodes.find((node) => node.label === giganticDb.id()); giganticDbNode = nodes.find((node) => node.label === giganticDb.id());
@@ -643,7 +627,7 @@ describe("createDatabaseTreeNodes", () => {
setup(); setup();
// Rebuild the nodes after changing the user/config context. // 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()); standardDbNode = nodes.find((node) => node.label === standardDb.id());
standardCollectionNode = standardDbNode.children.find((node) => node.label === standardCollection.id()); standardCollectionNode = standardDbNode.children.find((node) => node.label === standardCollection.id());
+2 -7
View File
@@ -137,7 +137,6 @@ export const createResourceTokenTreeNodes = (collection: ViewModels.CollectionBa
export const createDatabaseTreeNodes = ( export const createDatabaseTreeNodes = (
container: Explorer, container: Explorer,
isNotebookEnabled: boolean,
databases: ViewModels.Database[], databases: ViewModels.Database[],
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void, refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
searchText = "", searchText = "",
@@ -184,9 +183,7 @@ export const createDatabaseTreeNodes = (
database database
.collections() .collections()
.forEach((collection: ViewModels.Collection) => .forEach((collection: ViewModels.Collection) =>
databaseNode.children.push( databaseNode.children.push(buildCollectionNode(database, collection, container, refreshActiveTab)),
buildCollectionNode(database, collection, isNotebookEnabled, container, refreshActiveTab),
),
); );
if (database.collectionsContinuationToken) { if (database.collectionsContinuationToken) {
@@ -254,14 +251,13 @@ export const createDatabaseTreeNodes = (
export const buildCollectionNode = ( export const buildCollectionNode = (
database: ViewModels.Database, database: ViewModels.Database,
collection: ViewModels.Collection, collection: ViewModels.Collection,
isNotebookEnabled: boolean,
container: Explorer, container: Explorer,
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void, refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
): TreeNode => { ): TreeNode => {
let children: TreeNode[]; let children: TreeNode[];
// Flat Tree for Fabric // Flat Tree for Fabric
if (!isFabricMirrored()) { if (!isFabricMirrored()) {
children = buildCollectionNodeChildren(database, collection, isNotebookEnabled, container, refreshActiveTab); children = buildCollectionNodeChildren(database, collection, container, refreshActiveTab);
} }
const collectionNode: TreeNode = { const collectionNode: TreeNode = {
@@ -310,7 +306,6 @@ export const buildCollectionNode = (
const buildCollectionNodeChildren = ( const buildCollectionNodeChildren = (
database: ViewModels.Database, database: ViewModels.Database,
collection: ViewModels.Collection, collection: ViewModels.Collection,
isNotebookEnabled: boolean,
container: Explorer, container: Explorer,
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void, refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void,
): TreeNode[] => { ): TreeNode[] => {
-6
View File
@@ -1,5 +1,3 @@
import { ConnectionStatusType } from "Common/Constants";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { useTabs } from "../hooks/useTabs"; import { useTabs } from "../hooks/useTabs";
@@ -13,7 +11,6 @@ export interface SelectedNodeState {
collectionId?: string, collectionId?: string,
subnodeKinds?: ViewModels.CollectionTabKind[], subnodeKinds?: ViewModels.CollectionTabKind[],
) => boolean; ) => boolean;
isConnectedToContainer: () => boolean;
} }
export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) => ({ export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) => ({
@@ -61,7 +58,4 @@ export const useSelectedNode: UseStore<SelectedNodeState> = create((set, get) =>
subnodeKinds.includes(selectedSubnodeKind) subnodeKinds.includes(selectedSubnodeKind)
); );
}, },
isConnectedToContainer: (): boolean => {
return useNotebook.getState().connectionInfo?.status === ConnectionStatusType.Connected;
},
})); }));
-2
View File
@@ -52,8 +52,6 @@ import { ErrorBoundary } from "./Explorer/ErrorBoundary";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Menus/CommandBar/CommandBarComponent.less"; import "./Explorer/Menus/CommandBar/CommandBarComponent.less";
import { CommandBar } from "./Explorer/Menus/CommandBar/CommandBarComponentAdapter"; 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 "./Explorer/Menus/NotificationConsole/NotificationConsole.less";
import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { NotificationConsole } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import "./Explorer/Panes/PanelComponent.less"; import "./Explorer/Panes/PanelComponent.less";
-256
View File
@@ -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<IResponse<IPhoenixServiceInfo>> {
this.initializeCancelEventListener();
return promiseRetry(() => this.executeContainerAssignmentOperation(provisionData, "allocate"), {
retries: 4,
maxTimeout: 20000,
minTimeout: 20000,
signal: this.abortSignal,
});
}
public async resetContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
return this.executeContainerAssignmentOperation(provisionData, "reset");
}
private async executeContainerAssignmentOperation(
provisionData: IProvisionData,
operation: string,
): Promise<IResponse<IPhoenixServiceInfo>> {
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<ContainerInfo> {
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;
}
}
}
}
-87
View File
@@ -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<void> => {
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<void> => {
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);
};
-15
View File
@@ -83,12 +83,6 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
useEffect(() => { useEffect(() => {
const effect = async () => { const effect = async () => {
if (platform) { 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; let explorer: Explorer;
try { try {
if (platform === Platform.Hosted) { if (platform === Platform.Hosted) {
@@ -1010,15 +1004,6 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) { if (inputs.flights.indexOf(Flights.PKPartitionKeyTest) !== -1) {
userContext.features.partitionKeyDefault2 = true; 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 // Handle initial theme from portal
-15
View File
@@ -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<NotebookSnapshotHooks> = create((set) => ({
snapshot: undefined,
error: undefined,
setSnapshot: (imageSrc: string) => set((state) => ({ ...state, snapshot: imageSrc })),
setError: (error: string) => set((state) => ({ ...state, error })),
}));
-29
View File
@@ -9,7 +9,6 @@ import {
} from "Shared/AppStatePersistenceUtility"; } from "Shared/AppStatePersistenceUtility";
import create, { UseStore } from "zustand"; import create, { UseStore } from "zustand";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { CollectionTabKind } from "../Contracts/ViewModels";
import TabsBase from "../Explorer/Tabs/TabsBase"; import TabsBase from "../Explorer/Tabs/TabsBase";
export interface TabsState { export interface TabsState {
@@ -27,7 +26,6 @@ export interface TabsState {
refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void; refreshActiveTab: (comparator: (tab: TabsBase) => boolean) => void;
closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void; closeTabsByComparator: (comparator: (tab: TabsBase) => boolean) => void;
closeTab: (tab: TabsBase) => void; closeTab: (tab: TabsBase) => void;
closeAllNotebookTabs: (hardClose: boolean) => void;
openAndActivateReactTab: (tabKind: ReactTabKind) => void; openAndActivateReactTab: (tabKind: ReactTabKind) => void;
closeReactTab: (tabKind: ReactTabKind) => void; closeReactTab: (tabKind: ReactTabKind) => void;
setIsTabExecuting: (state: boolean) => void; setIsTabExecuting: (state: boolean) => void;
@@ -132,33 +130,6 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
get().persistTabsState(); 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) => { openAndActivateReactTab: (tabKind: ReactTabKind) => {
if (get().openedReactTabs.indexOf(tabKind) === -1) { if (get().openedReactTabs.indexOf(tabKind) === -1) {
set((state) => ({ set((state) => ({
-2
View File
@@ -44,7 +44,6 @@
"./src/Explorer/Graph/GraphExplorerComponent/GraphData.ts", "./src/Explorer/Graph/GraphExplorerComponent/GraphData.ts",
"./src/Explorer/LazyMonaco.ts", "./src/Explorer/LazyMonaco.ts",
"./src/Explorer/Menus/NotificationConsole/ConsoleData.tsx", "./src/Explorer/Menus/NotificationConsole/ConsoleData.tsx",
"./src/Explorer/Notebook/NotebookContentItem.ts",
"./src/Explorer/OpenFullScreen.test.tsx", "./src/Explorer/OpenFullScreen.test.tsx",
"./src/Explorer/OpenFullScreen.tsx", "./src/Explorer/OpenFullScreen.tsx",
"./src/Explorer/Panes/PanelContainerComponent.test.tsx", "./src/Explorer/Panes/PanelContainerComponent.test.tsx",
@@ -107,7 +106,6 @@
"./src/hooks/useConfig.ts", "./src/hooks/useConfig.ts",
"./src/hooks/useDirectories.tsx", "./src/hooks/useDirectories.tsx",
"./src/hooks/useGraphPhoto.tsx", "./src/hooks/useGraphPhoto.tsx",
"./src/hooks/useNotebookSnapshotStore.ts",
"./src/hooks/useNotificationConsole.ts", "./src/hooks/useNotificationConsole.ts",
"./src/hooks/useObservable.ts", "./src/hooks/useObservable.ts",
"./src/hooks/useSidePanel.ts", "./src/hooks/useSidePanel.ts",