mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-06-29 01:27:22 +01:00
Remove Phoenix & Notebooks — Phase 1: Decouple database shells to CloudShell (#2513)
* Add implementation plan for removing Phoenix and notebooks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Phase 1: Decouple database shells from Phoenix to CloudShell-only Migrate all shell terminals to the CloudShell path and remove the legacy Phoenix notebook-server terminal code so shells no longer depend on notebook provisioning. - TerminalTab now always uses CloudShellTerminalComponentAdapter; removed the notebook-server adapter branch, getNotebookServerInfo, and the dead VCoreMongo firewall check - Migrate Postgres and VCore Mongo quickstart tabs to CloudShellTerminalComponent (drop allocateContainer/useNotebook dependencies) - Refactor useTerminal to send input via the CloudShell WebSocket instead of postRobot/iframe; register the socket from CloudShellTerminalComponent - Simplify Explorer.openNotebookTerminal to always open a CloudShell terminal - Delete NotebookTerminalComponent(+test/less/snapshot), NotebookTerminalComponentAdapter, and the src/Terminal/ entry point - Remove the terminal.html webpack entry/HTML plugin and src/Terminal from tsconfig.strict.json Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,214 @@
|
|||||||
|
# Implementation Plan: Remove Phoenix & Notebooks
|
||||||
|
|
||||||
|
## Problem statement
|
||||||
|
|
||||||
|
Cosmos Explorer contains a large, deeply-integrated **notebooks** feature backed by
|
||||||
|
the **Phoenix** compute-container service and the **Juno** service, plus a **GitHub**
|
||||||
|
integration used to pin/browse notebook repositories. This functionality is being
|
||||||
|
retired. The goal is to remove all notebooks/Phoenix/Juno/GitHub-for-notebooks code,
|
||||||
|
dependencies, UI surfaces, telemetry, and configuration, while **preserving the
|
||||||
|
database shell terminals** (Mongo / Cassandra / Postgres / VCoreMongo), which today
|
||||||
|
share the Terminal infrastructure with notebooks.
|
||||||
|
|
||||||
|
This document is the implementation plan only. No code changes are made here.
|
||||||
|
|
||||||
|
## Scope decisions (confirmed)
|
||||||
|
|
||||||
|
- **Database shells**: Keep the Mongo/Cassandra/Postgres/VCoreMongo shells, but migrate
|
||||||
|
them to use the **CloudShell** path exclusively. Remove the legacy Phoenix
|
||||||
|
notebook-server shell path.
|
||||||
|
- **GitHub integration**: Remove entirely (it exists only for notebook pinned repos).
|
||||||
|
- **Schema Analyzer** (`src/Explorer/Notebook/SchemaAnalyzer`): Remove.
|
||||||
|
- **Phasing**: Every phase must leave the app in a **buildable, shippable** state
|
||||||
|
(build + lint + strict compile + unit tests green; shells still work).
|
||||||
|
- **Localization**: Remove notebook/GitHub strings from **all** resource files —
|
||||||
|
`src/Localization/en/Resources.json` **and** every non-English locale
|
||||||
|
(`src/Localization/<locale>/Resources.json`). (This deletion is an exception to the
|
||||||
|
usual convention of editing only the English file.)
|
||||||
|
|
||||||
|
## Prior art / related commits
|
||||||
|
|
||||||
|
This is a continuation of an in-progress removal effort. Reference commits:
|
||||||
|
|
||||||
|
- `7295d63a` — Remove gallery.html and all associated gallery functionality (#2474)
|
||||||
|
- `a36467f4` — Remove Phoenix `getDbAccountAllowedStatus`; `isPhoenixNotebooks`/
|
||||||
|
`isPhoenixFeatures` now always `false` (#2472)
|
||||||
|
- `31385950` — removed NotebookViewer file (#2281)
|
||||||
|
|
||||||
|
> **Note:** An unmerged branch `users/jawelton/remove-notebooks-terminal-052126`
|
||||||
|
> already contains related work (`5989c77c` "Remove terminal.html webpack entry point
|
||||||
|
> and notebooks terminal code", `c7f9d7e3` "Switch VCore Mongo quickstart to use
|
||||||
|
> CloudShell terminal"). These are **not** in `master`. Reconcile with that branch
|
||||||
|
> before/while starting Phase 1 to avoid duplicate or conflicting work.
|
||||||
|
|
||||||
|
## Current-state survey (what exists today)
|
||||||
|
|
||||||
|
**Core directories / files**
|
||||||
|
- `src/Phoenix/PhoenixClient.ts` — container allocation, heartbeat, status polling.
|
||||||
|
- `src/Juno/JunoClient.ts` (+ test) — pinned-repo / notebook metadata service client.
|
||||||
|
- `src/Explorer/Notebook/` — the bulk of the feature:
|
||||||
|
- `useNotebook.ts` (Zustand store), `NotebookManager.tsx`, `NotebookContentClient.ts`,
|
||||||
|
`NotebookClientV2.ts`, `NotebookContainerClient.ts`, `NotebookContentItem.ts`,
|
||||||
|
`NotebookUtil.ts`, `NTeractUtil.ts`, `FileSystemUtil.ts`
|
||||||
|
- `NotebookComponent/` (nteract redux store, epics, reducers, content providers)
|
||||||
|
- `NotebookRenderer/` (nteract cell rendering, decorators, outputs)
|
||||||
|
- `SchemaAnalyzer/`, `SecurityWarningBar/`
|
||||||
|
- `src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx` (+ less/test)
|
||||||
|
- `src/Explorer/Controls/NotebookViewer/` — read-only viewer + metadata.
|
||||||
|
- `src/Explorer/Tabs/NotebookV2Tab.ts`, `NotebookTabBase.ts`, `SchemaAnalyzerTab.ts`
|
||||||
|
- `src/Explorer/Panes/CopyNotebookPane/`
|
||||||
|
- `src/Explorer/Tabs/ShellAdapters/NotebookTerminalComponentAdapter.tsx`
|
||||||
|
- `src/CellOutputViewer/` — webpack entry `cellOutputViewer`.
|
||||||
|
- `src/Utils/NotebookConfigurationUtils.ts`, `src/hooks/useNotebookSnapshotStore.ts`
|
||||||
|
- `src/Utils/arm/generatedClients/cosmosNotebooks/` — generated ARM client.
|
||||||
|
|
||||||
|
**GitHub integration (notebook-only)**
|
||||||
|
- `src/GitHub/` (`GitHubClient.ts`, `GitHubContentProvider.ts`, `GitHubOAuthService.ts`,
|
||||||
|
`GitHubConnector.ts`), `src/Utils/GitHubUtils.ts`
|
||||||
|
- `src/Explorer/Controls/GitHub/` (AuthorizeAccess, AddRepo, GitHubRepos components)
|
||||||
|
- `src/Explorer/Panes/GitHubReposPanel/`
|
||||||
|
- webpack entry `connectToGitHub` + `src/connectToGitHub.html`
|
||||||
|
|
||||||
|
**Integration / glue points (edited, not deleted)**
|
||||||
|
- `src/Explorer/Explorer.tsx` — `phoenixClient`, `notebookManager`, `gitHubOAuthService`,
|
||||||
|
`initNotebooks`, `initiateAndRefreshNotebookList`, `allocateContainer`,
|
||||||
|
`openNotebook*`, `openNotebookTerminal`, `createNotebookContentItemFile`, etc.
|
||||||
|
- `src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx` (+ adapter, test)
|
||||||
|
— New Notebook / Open Terminal / shell buttons branching on `isShellEnabled`.
|
||||||
|
- `src/Explorer/Tree/treeNodeUtil.tsx` / `ResourceTreeAdapter.tsx` / `ResourceTree.tsx`
|
||||||
|
— "My Notebooks" / "GitHub" tree nodes; `isNotebookEnabled` plumbing.
|
||||||
|
- `src/Explorer/SplashScreen/SplashScreen.tsx` — notebook cards + `openNotebookTerminal`
|
||||||
|
for Postgres/VCoreMongo shells.
|
||||||
|
- `src/Explorer/Tabs/TerminalTab.tsx` — chooses CloudShell vs notebook-server adapter.
|
||||||
|
- `src/Explorer/OpenActions/OpenActions.tsx`, `src/Explorer/ContextMenuButtonFactory.tsx`,
|
||||||
|
`src/Explorer/Tree/Collection.ts`, `src/Explorer/useSelectedNode.ts`,
|
||||||
|
`src/Explorer/MostRecentActivity/MostRecentActivity.ts`
|
||||||
|
- `src/hooks/useKnockoutExplorer.ts`, `src/hooks/useTabs.ts`
|
||||||
|
- `src/ConfigContext.ts`, `src/Common/Constants.ts`, `src/Contracts/DataModels.ts`,
|
||||||
|
`src/Contracts/ViewModels.ts`, `src/Contracts/ActionContracts.ts`,
|
||||||
|
`src/Platform/Hosted/extractFeatures.ts` (+ test)
|
||||||
|
- `src/Shared/Telemetry/TelemetryConstants.ts`
|
||||||
|
- `src/Localization/en/Resources.json` **and all non-English** `src/Localization/<locale>/Resources.json`
|
||||||
|
- `webpack.config.js` — `cellOutputViewer`, `connectToGitHub` entries + HTML plugins.
|
||||||
|
- `package.json` — `@nteract/*`, `@jupyterlab/*`, `@phosphor/widgets`, `rx-jupyter`,
|
||||||
|
and other notebook-only dependencies.
|
||||||
|
|
||||||
|
**Critical coupling — Terminal / shells**
|
||||||
|
- `TerminalTab` uses `CloudShellTerminalComponentAdapter` when
|
||||||
|
`userContext.features.enableCloudShell`, otherwise `NotebookTerminalComponentAdapter`
|
||||||
|
(which needs a Phoenix-allocated notebook server + `terminal.html` iframe).
|
||||||
|
- Command-bar/splash shell buttons branch on
|
||||||
|
`useNotebook.getState().isShellEnabled || userContext.features.enableCloudShell`.
|
||||||
|
- `Explorer.openNotebookTerminal(...)` is the shared entry for opening shells and must
|
||||||
|
be retained (and rewired to CloudShell-only) — only its notebook-server behavior is
|
||||||
|
removed.
|
||||||
|
|
||||||
|
## Phased approach
|
||||||
|
|
||||||
|
Each phase is independently buildable and shippable. Within each phase, **all
|
||||||
|
references to removed code are also removed** so the tree compiles. After every phase
|
||||||
|
run: `npm run compile`, `npm run compile:strict`, `npm run lint`, `npm run format:check`,
|
||||||
|
`npm test`, and a webpack build (`npm run build:ci`); manually verify the four shells
|
||||||
|
still open.
|
||||||
|
|
||||||
|
### Phase 1 — Decouple database shells to CloudShell-only
|
||||||
|
Remove the legacy Phoenix notebook-server terminal path so shells no longer depend on
|
||||||
|
notebook provisioning.
|
||||||
|
- Rewire `TerminalTab` to always use `CloudShellTerminalComponentAdapter`; delete the
|
||||||
|
`NotebookTerminalComponentAdapter` branch and `getNotebookServerInfo`.
|
||||||
|
- Delete `src/Explorer/Tabs/ShellAdapters/NotebookTerminalComponentAdapter.tsx` and
|
||||||
|
`src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx` (+ less/test/snapshot).
|
||||||
|
- Simplify shell buttons in `CommandBarComponentButtonFactory` and `SplashScreen` to
|
||||||
|
drop the `isShellEnabled` branch (CloudShell path only); keep `openNotebookTerminal`.
|
||||||
|
- Verify whether the `terminal`/`terminal.html` webpack entry is still needed by
|
||||||
|
CloudShell. If unused, remove it and `src/Terminal/`; otherwise keep.
|
||||||
|
- **Outcome:** Shells run purely on CloudShell. Phoenix no longer needed for terminals.
|
||||||
|
|
||||||
|
### Phase 2 — Remove the in-app notebook authoring & rendering experience
|
||||||
|
Delete the notebook tabs, the nteract rendering engine, panes, and the read-only viewer,
|
||||||
|
and remove all UI entry points that open notebooks.
|
||||||
|
- Delete: `src/Explorer/Notebook/NotebookComponent/`,
|
||||||
|
`src/Explorer/Notebook/NotebookRenderer/`, `src/Explorer/Notebook/SecurityWarningBar/`,
|
||||||
|
`NotebookClientV2.ts`, `notebookClientV2.test.ts`, `NotebookContentClient.ts`,
|
||||||
|
`NTeractUtil.ts`, `NotebookContentItem.ts`, `NotebookUtil.ts` (+ test),
|
||||||
|
`FileSystemUtil.ts`.
|
||||||
|
- Delete tabs/panes/viewer: `src/Explorer/Tabs/NotebookV2Tab.ts`, `NotebookTabBase.ts`,
|
||||||
|
`src/Explorer/Panes/CopyNotebookPane/`, `src/Explorer/Controls/NotebookViewer/`,
|
||||||
|
`src/CellOutputViewer/` (+ `cellOutputViewer` webpack entry & HTML plugin).
|
||||||
|
- Remove notebook entry points: "New Notebook"/open-notebook buttons in
|
||||||
|
`CommandBarComponentButtonFactory` (+ test), `OpenActions.tsx`,
|
||||||
|
`ContextMenuButtonFactory.tsx`, splash-screen notebook cards & recent-notebook items
|
||||||
|
(`MostRecentActivity` OpenNotebook type), and the `openNotebook*` /
|
||||||
|
`createNotebookContentItemFile` methods on `Explorer`.
|
||||||
|
- Remove notebook deps from `package.json`: `@nteract/*`, `@jupyterlab/*`,
|
||||||
|
`@phosphor/widgets`, `rx-jupyter` (and any now-unused transitive notebook-only libs).
|
||||||
|
- **Outcome:** Notebooks can no longer be authored, opened, or rendered.
|
||||||
|
|
||||||
|
### Phase 3 — Remove Schema Analyzer
|
||||||
|
- Delete `src/Explorer/Notebook/SchemaAnalyzer/` and `src/Explorer/Tabs/SchemaAnalyzerTab.ts`.
|
||||||
|
- Remove Schema Analyzer command-bar button and any tree/menu entry points.
|
||||||
|
|
||||||
|
### Phase 4 — Remove GitHub integration
|
||||||
|
- Delete `src/GitHub/`, `src/Explorer/Controls/GitHub/`,
|
||||||
|
`src/Explorer/Panes/GitHubReposPanel/`, `src/Utils/GitHubUtils.ts`,
|
||||||
|
`src/connectToGitHub.html`, and the `connectToGitHub` webpack entry & HTML plugin.
|
||||||
|
- Remove `gitHubOAuthService`, GitHub pinned-repo wiring, and `gitHubNotebooksContentRoot`
|
||||||
|
usage from `Explorer.tsx`, `useNotebook.ts`, `NotebookManager.tsx`, and `JunoClient`
|
||||||
|
pinned-repo methods.
|
||||||
|
- Remove GitHub-related localization keys from **all** locale files (`en` + non-English).
|
||||||
|
|
||||||
|
### Phase 5 — Remove Phoenix and the notebook container/allocation core
|
||||||
|
- Delete `src/Phoenix/`, `src/Explorer/Notebook/NotebookContainerClient.ts`,
|
||||||
|
`src/Explorer/Notebook/NotebookManager.tsx`, `src/Explorer/Notebook/useNotebook.ts`,
|
||||||
|
`src/Utils/NotebookConfigurationUtils.ts`, `src/hooks/useNotebookSnapshotStore.ts`.
|
||||||
|
- Remove from `Explorer.tsx`: `phoenixClient`, `notebookManager`, `_isInitializingNotebooks`,
|
||||||
|
`initNotebooks`, `initiateAndRefreshNotebookList`, `refreshNotebookList`,
|
||||||
|
`allocateContainer`, container heartbeat/connection logic, and notebook-server URL
|
||||||
|
feature overrides.
|
||||||
|
- Remove notebook tree nodes ("My Notebooks") and `isNotebookEnabled` plumbing from
|
||||||
|
`treeNodeUtil.tsx`, `ResourceTreeAdapter.tsx`, `ResourceTree.tsx`, `Collection.ts`,
|
||||||
|
`useSelectedNode.ts` (+ update tree snapshots/tests).
|
||||||
|
- Remove notebook initialization from `useKnockoutExplorer.ts` and notebook tab handling
|
||||||
|
in `useTabs.ts`.
|
||||||
|
|
||||||
|
### Phase 6 — Remove residual clients, config, contracts, telemetry & strings
|
||||||
|
- Delete `src/Juno/` and `src/Utils/arm/generatedClients/cosmosNotebooks/`.
|
||||||
|
- Remove notebook fields from `ConfigContext.ts`, `Constants.ts` (Notebook namespace),
|
||||||
|
`DataModels.ts` (notebook/Phoenix/container interfaces), `ViewModels.ts`,
|
||||||
|
`ActionContracts.ts`, and notebook feature flags from
|
||||||
|
`extractFeatures.ts` (+ update test).
|
||||||
|
- Remove notebook/Phoenix telemetry actions/areas from `TelemetryConstants.ts` (preserve
|
||||||
|
enum numbering if other systems depend on it — mirror the cautious approach in
|
||||||
|
`a36467f4`).
|
||||||
|
- Remove remaining notebook strings from **all** locale `Resources.json` files (`en` +
|
||||||
|
every non-English locale) and any notebook images (e.g. `images/notebook/`).
|
||||||
|
- Final full build + test sweep; update `EndpointUtils.ts` (`allowedNotebookServerUrls`)
|
||||||
|
and any docs/comments referencing notebooks.
|
||||||
|
|
||||||
|
## Cross-cutting verification (run after each phase)
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run compile
|
||||||
|
npm run compile:strict
|
||||||
|
npm run lint
|
||||||
|
npm run format:check
|
||||||
|
npm test
|
||||||
|
npm run build:ci
|
||||||
|
```
|
||||||
|
Plus manual smoke test: open Mongo, Cassandra, Postgres, and VCoreMongo shells.
|
||||||
|
|
||||||
|
## Notes & considerations
|
||||||
|
|
||||||
|
- **Strict null checks:** any file edited may need to stay in / be removed from
|
||||||
|
`tsconfig.strict.json`. Remove deleted files from that list.
|
||||||
|
- **Snapshots:** several Jest snapshots reference notebook UI
|
||||||
|
(`treeNodeUtil`, `SettingsComponent`, panel snapshots). Regenerate after edits.
|
||||||
|
- **Telemetry enum safety:** prior commit `a36467f4` deliberately reverted removal of
|
||||||
|
enum values to avoid breaking downstream consumers. Prefer leaving enum numeric values
|
||||||
|
intact unless confirmed safe to remove.
|
||||||
|
- **`enableCloudShell` feature flag:** confirm it is enabled in all target environments
|
||||||
|
before removing the Phoenix shell fallback, or shells will break.
|
||||||
|
- **E2E tests:** check `test/` for notebook/terminal specs to update or remove; shells
|
||||||
|
may have E2E coverage that needs the CloudShell-only path.
|
||||||
|
- **Reconcile** with branch `users/jawelton/remove-notebooks-terminal-052126` to avoid
|
||||||
|
rework, especially in Phase 1.
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
@import "../../../../less/Common/Constants";
|
|
||||||
|
|
||||||
.notebookTerminalContainer {
|
|
||||||
padding: @DefaultSpace;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
iframe {
|
|
||||||
border: none;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import { shallow } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
|
||||||
import { NotebookTerminalComponent, NotebookTerminalComponentProps } from "./NotebookTerminalComponent";
|
|
||||||
|
|
||||||
const testAccount: DataModels.DatabaseAccount = {
|
|
||||||
id: "id",
|
|
||||||
kind: "kind",
|
|
||||||
location: "location",
|
|
||||||
name: "name",
|
|
||||||
properties: {
|
|
||||||
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
|
|
||||||
},
|
|
||||||
type: "type",
|
|
||||||
};
|
|
||||||
|
|
||||||
const testMongo32Account: DataModels.DatabaseAccount = {
|
|
||||||
...testAccount,
|
|
||||||
};
|
|
||||||
|
|
||||||
const testMongo36Account: DataModels.DatabaseAccount = {
|
|
||||||
...testAccount,
|
|
||||||
properties: {
|
|
||||||
mongoEndpoint: "https://testMongoEndpoint.azure.com/",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const testCassandraAccount: DataModels.DatabaseAccount = {
|
|
||||||
...testAccount,
|
|
||||||
properties: {
|
|
||||||
cassandraEndpoint: "https://testCassandraEndpoint.azure.com/",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const testPostgresAccount: DataModels.DatabaseAccount = {
|
|
||||||
...testAccount,
|
|
||||||
properties: {
|
|
||||||
postgresqlEndpoint: "https://testPostgresEndpoint.azure.com/",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const testVCoreMongoAccount: DataModels.DatabaseAccount = {
|
|
||||||
...testAccount,
|
|
||||||
properties: {
|
|
||||||
vcoreMongoEndpoint: "https://testVCoreMongoEndpoint.azure.com/",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const testNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
|
||||||
authToken: "authToken",
|
|
||||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com",
|
|
||||||
forwardingId: "Id",
|
|
||||||
};
|
|
||||||
|
|
||||||
const testMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
|
||||||
authToken: "authToken",
|
|
||||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
|
|
||||||
forwardingId: "Id",
|
|
||||||
};
|
|
||||||
|
|
||||||
const testCassandraNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
|
||||||
authToken: "authToken",
|
|
||||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
|
|
||||||
forwardingId: "Id",
|
|
||||||
};
|
|
||||||
|
|
||||||
const testPostgresNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
|
||||||
authToken: "authToken",
|
|
||||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/postgresql",
|
|
||||||
forwardingId: "Id",
|
|
||||||
};
|
|
||||||
|
|
||||||
const testVCoreMongoNotebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
|
||||||
authToken: "authToken",
|
|
||||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongovcore",
|
|
||||||
forwardingId: "Id",
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("NotebookTerminalComponent", () => {
|
|
||||||
it("renders terminal", () => {
|
|
||||||
const props: NotebookTerminalComponentProps = {
|
|
||||||
databaseAccount: testAccount,
|
|
||||||
notebookServerInfo: testNotebookServerInfo,
|
|
||||||
tabId: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders mongo 3.2 shell", () => {
|
|
||||||
const props: NotebookTerminalComponentProps = {
|
|
||||||
databaseAccount: testMongo32Account,
|
|
||||||
notebookServerInfo: testMongoNotebookServerInfo,
|
|
||||||
tabId: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders mongo 3.6 shell", () => {
|
|
||||||
const props: NotebookTerminalComponentProps = {
|
|
||||||
databaseAccount: testMongo36Account,
|
|
||||||
notebookServerInfo: testMongoNotebookServerInfo,
|
|
||||||
tabId: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders cassandra shell", () => {
|
|
||||||
const props: NotebookTerminalComponentProps = {
|
|
||||||
databaseAccount: testCassandraAccount,
|
|
||||||
notebookServerInfo: testCassandraNotebookServerInfo,
|
|
||||||
tabId: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders Postgres shell", () => {
|
|
||||||
const props: NotebookTerminalComponentProps = {
|
|
||||||
databaseAccount: testPostgresAccount,
|
|
||||||
notebookServerInfo: testPostgresNotebookServerInfo,
|
|
||||||
tabId: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders vCore Mongo shell", () => {
|
|
||||||
const props: NotebookTerminalComponentProps = {
|
|
||||||
databaseAccount: testVCoreMongoAccount,
|
|
||||||
notebookServerInfo: testVCoreMongoNotebookServerInfo,
|
|
||||||
tabId: undefined,
|
|
||||||
username: "username",
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<NotebookTerminalComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
/**
|
|
||||||
* Wrapper around Notebook server terminal
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useTerminal } from "hooks/useTerminal";
|
|
||||||
import postRobot from "post-robot";
|
|
||||||
import * as React from "react";
|
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
|
||||||
import { TerminalProps } from "../../../Terminal/TerminalProps";
|
|
||||||
import { userContext } from "../../../UserContext";
|
|
||||||
import * as StringUtils from "../../../Utils/StringUtils";
|
|
||||||
|
|
||||||
export interface NotebookTerminalComponentProps {
|
|
||||||
notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo;
|
|
||||||
databaseAccount: DataModels.DatabaseAccount;
|
|
||||||
tabId: string;
|
|
||||||
username?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NotebookTerminalComponent extends React.Component<NotebookTerminalComponentProps> {
|
|
||||||
private terminalWindow: Window;
|
|
||||||
|
|
||||||
constructor(props: NotebookTerminalComponentProps) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
this.sendPropsToTerminalFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<div className="notebookTerminalContainer">
|
|
||||||
<iframe
|
|
||||||
title="Terminal to Notebook Server"
|
|
||||||
onLoad={(event) => this.handleFrameLoad(event)}
|
|
||||||
src="terminal.html"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
|
|
||||||
this.terminalWindow = (event.target as HTMLIFrameElement).contentWindow;
|
|
||||||
useTerminal.getState().setTerminal(this.terminalWindow);
|
|
||||||
this.sendPropsToTerminalFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
sendPropsToTerminalFrame(): void {
|
|
||||||
if (!this.terminalWindow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let props: TerminalProps = {
|
|
||||||
terminalEndpoint: this.tryGetTerminalEndpoint(),
|
|
||||||
notebookServerEndpoint: this.props.notebookServerInfo?.notebookServerEndpoint,
|
|
||||||
authToken: this.props.notebookServerInfo?.authToken,
|
|
||||||
subscriptionId: userContext.subscriptionId,
|
|
||||||
apiType: userContext.apiType,
|
|
||||||
authType: userContext.authType,
|
|
||||||
databaseAccount: userContext.databaseAccount,
|
|
||||||
tabId: this.props.tabId,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.props.username) {
|
|
||||||
props = {
|
|
||||||
...props,
|
|
||||||
username: this.props.username,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
postRobot.send(this.terminalWindow, "props", props, {
|
|
||||||
domain: window.location.origin,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public tryGetTerminalEndpoint(): string | undefined {
|
|
||||||
let terminalEndpoint: string | undefined;
|
|
||||||
|
|
||||||
const notebookServerEndpoint = this.props.notebookServerInfo?.notebookServerEndpoint;
|
|
||||||
if (StringUtils.endsWith(notebookServerEndpoint, "mongo")) {
|
|
||||||
// mongoEndpoint is only available for Mongo 3.6 and higher, fallback to documentEndpoint otherwise
|
|
||||||
terminalEndpoint =
|
|
||||||
this.props.databaseAccount?.properties.mongoEndpoint || this.props.databaseAccount?.properties.documentEndpoint;
|
|
||||||
} else if (StringUtils.endsWith(notebookServerEndpoint, "cassandra")) {
|
|
||||||
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint;
|
|
||||||
} else if (StringUtils.endsWith(notebookServerEndpoint, "postgresql")) {
|
|
||||||
return this.props.databaseAccount?.properties.postgresqlEndpoint;
|
|
||||||
} else if (StringUtils.endsWith(notebookServerEndpoint, "mongovcore")) {
|
|
||||||
return this.props.databaseAccount?.properties.vcoreMongoEndpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (terminalEndpoint) {
|
|
||||||
return new URL(terminalEndpoint).host;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`NotebookTerminalComponent renders Postgres shell 1`] = `
|
|
||||||
<div
|
|
||||||
className="notebookTerminalContainer"
|
|
||||||
>
|
|
||||||
<iframe
|
|
||||||
onLoad={[Function]}
|
|
||||||
src="terminal.html"
|
|
||||||
title="Terminal to Notebook Server"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`NotebookTerminalComponent renders cassandra shell 1`] = `
|
|
||||||
<div
|
|
||||||
className="notebookTerminalContainer"
|
|
||||||
>
|
|
||||||
<iframe
|
|
||||||
onLoad={[Function]}
|
|
||||||
src="terminal.html"
|
|
||||||
title="Terminal to Notebook Server"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`NotebookTerminalComponent renders mongo 3.2 shell 1`] = `
|
|
||||||
<div
|
|
||||||
className="notebookTerminalContainer"
|
|
||||||
>
|
|
||||||
<iframe
|
|
||||||
onLoad={[Function]}
|
|
||||||
src="terminal.html"
|
|
||||||
title="Terminal to Notebook Server"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`NotebookTerminalComponent renders mongo 3.6 shell 1`] = `
|
|
||||||
<div
|
|
||||||
className="notebookTerminalContainer"
|
|
||||||
>
|
|
||||||
<iframe
|
|
||||||
onLoad={[Function]}
|
|
||||||
src="terminal.html"
|
|
||||||
title="Terminal to Notebook Server"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`NotebookTerminalComponent renders terminal 1`] = `
|
|
||||||
<div
|
|
||||||
className="notebookTerminalContainer"
|
|
||||||
>
|
|
||||||
<iframe
|
|
||||||
onLoad={[Function]}
|
|
||||||
src="terminal.html"
|
|
||||||
title="Terminal to Notebook Server"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`NotebookTerminalComponent renders vCore Mongo shell 1`] = `
|
|
||||||
<div
|
|
||||||
className="notebookTerminalContainer"
|
|
||||||
>
|
|
||||||
<iframe
|
|
||||||
onLoad={[Function]}
|
|
||||||
src="terminal.html"
|
|
||||||
title="Terminal to Notebook Server"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -959,25 +959,8 @@ export default class Explorer {
|
|||||||
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
|
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
|
public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
||||||
if (userContext.features.enableCloudShell) {
|
this.connectToNotebookTerminal(kind);
|
||||||
this.connectToNotebookTerminal(kind);
|
|
||||||
} else if (useNotebook.getState().isPhoenixFeatures) {
|
|
||||||
await this.allocateContainer();
|
|
||||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
|
||||||
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
|
|
||||||
this.connectToNotebookTerminal(kind);
|
|
||||||
} else {
|
|
||||||
useDialog
|
|
||||||
.getState()
|
|
||||||
.showOkModalDialog(
|
|
||||||
"Failed to connect",
|
|
||||||
"Failed to connect to temporary workspace. This could happen because of network issues. Please refresh the page and try again.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.connectToNotebookTerminal(kind);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private connectToNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
private connectToNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useTerminal } from "hooks/useTerminal";
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { Terminal } from "xterm";
|
import { Terminal } from "xterm";
|
||||||
import { FitAddon } from "xterm-addon-fit";
|
import { FitAddon } from "xterm-addon-fit";
|
||||||
@@ -59,7 +60,14 @@ export const CloudShellTerminalComponent: React.FC<CloudShellTerminalComponentPr
|
|||||||
});
|
});
|
||||||
resizeObserver.observe(terminalRef.current);
|
resizeObserver.observe(terminalRef.current);
|
||||||
|
|
||||||
socketRef.current = startCloudShellTerminal(terminal, props.shellType);
|
const initTerminal = async () => {
|
||||||
|
const socket = await startCloudShellTerminal(terminal, props.shellType);
|
||||||
|
socketRef.current = socket;
|
||||||
|
if (socket) {
|
||||||
|
useTerminal.getState().setSocket(socket);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
initTerminal();
|
||||||
|
|
||||||
// Cleanup function to close WebSocket and dispose terminal
|
// Cleanup function to close WebSocket and dispose terminal
|
||||||
return () => {
|
return () => {
|
||||||
@@ -73,6 +81,7 @@ export const CloudShellTerminalComponent: React.FC<CloudShellTerminalComponentPr
|
|||||||
resizeObserver.unobserve(terminalRef.current);
|
resizeObserver.unobserve(terminalRef.current);
|
||||||
}
|
}
|
||||||
terminal.dispose(); // Clean up XTerm instance
|
terminal.dispose(); // Clean up XTerm instance
|
||||||
|
useTerminal.getState().setSocket(undefined);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,18 @@
|
|||||||
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
import { Stack } from "@fluentui/react";
|
||||||
|
|
||||||
import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels";
|
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||||
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
|
import { TerminalKind } from "Contracts/ViewModels";
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
|
||||||
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
||||||
import { QuickstartGuide } from "Explorer/Quickstart/QuickstartGuide";
|
import { QuickstartGuide } from "Explorer/Quickstart/QuickstartGuide";
|
||||||
|
import { CloudShellTerminalComponent } from "Explorer/Tabs/CloudShellTab/CloudShellTerminalComponent";
|
||||||
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
||||||
|
|
||||||
interface QuickstartTabProps {
|
export const QuickstartTab: React.FC = (): JSX.Element => {
|
||||||
explorer: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: QuickstartTabProps): JSX.Element => {
|
|
||||||
const notebookServerInfo = useNotebook((state) => state.notebookServerInfo);
|
|
||||||
const [isAllPublicIPAddressEnabled, setIsAllPublicIPAddressEnabled] = useState<boolean>(true);
|
const [isAllPublicIPAddressEnabled, setIsAllPublicIPAddressEnabled] = useState<boolean>(true);
|
||||||
|
|
||||||
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
|
|
||||||
authToken: notebookServerInfo.authToken,
|
|
||||||
notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/postgresql`,
|
|
||||||
forwardingId: notebookServerInfo.forwardingId,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkFirewallRules(
|
checkFirewallRules(
|
||||||
"2022-11-08",
|
"2022-11-08",
|
||||||
@@ -34,10 +21,6 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
explorer.allocateContainer();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack style={{ width: "100%" }} horizontal>
|
<Stack style={{ width: "100%" }} horizontal>
|
||||||
<Stack style={{ width: "50%" }}>
|
<Stack style={{ width: "50%" }}>
|
||||||
@@ -51,24 +34,13 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
|
|||||||
shellName="PostgreSQL"
|
shellName="PostgreSQL"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
|
{isAllPublicIPAddressEnabled && (
|
||||||
<NotebookTerminalComponent
|
<CloudShellTerminalComponent
|
||||||
notebookServerInfo={getNotebookServerInfo()}
|
|
||||||
databaseAccount={userContext.databaseAccount}
|
databaseAccount={userContext.databaseAccount}
|
||||||
tabId="QuickstartPSQLShell"
|
tabId="QuickstartPSQLShell"
|
||||||
|
shellType={TerminalKind.Postgres}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isAllPublicIPAddressEnabled && !notebookServerInfo?.notebookServerEndpoint && (
|
|
||||||
<Stack style={{ margin: "auto 0" }}>
|
|
||||||
<Text block style={{ margin: "auto" }}>
|
|
||||||
Connecting to the PostgreSQL shell.
|
|
||||||
</Text>
|
|
||||||
<Text block style={{ margin: "auto" }}>
|
|
||||||
If the cluster was just created, this could take up to a minute.
|
|
||||||
</Text>
|
|
||||||
<Spinner styles={{ root: { marginTop: 16 } }} size={SpinnerSize.large}></Spinner>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
|
|
||||||
import * as React from "react";
|
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
|
||||||
import { BaseTerminalComponentAdapter } from "./BaseTerminalComponentAdapter";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notebook terminal tab
|
|
||||||
*/
|
|
||||||
export class NotebookTerminalComponentAdapter extends BaseTerminalComponentAdapter {
|
|
||||||
constructor(
|
|
||||||
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
|
|
||||||
getDatabaseAccount: () => DataModels.DatabaseAccount,
|
|
||||||
getTabId: () => string,
|
|
||||||
getUsername: () => string,
|
|
||||||
isAllPublicIPAddressesEnabled: ko.Observable<boolean>,
|
|
||||||
kind: ViewModels.TerminalKind,
|
|
||||||
) {
|
|
||||||
super(getDatabaseAccount, getTabId, getUsername, isAllPublicIPAddressesEnabled, kind);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected renderTerminalComponent(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<NotebookTerminalComponent
|
|
||||||
notebookServerInfo={this.getNotebookServerInfo()}
|
|
||||||
databaseAccount={this.getDatabaseAccount()}
|
|
||||||
tabId={this.getTabId()}
|
|
||||||
username={this.getUsername()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -333,11 +333,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
|
|||||||
return <SplashScreen explorer={explorer} />;
|
return <SplashScreen explorer={explorer} />;
|
||||||
}
|
}
|
||||||
case ReactTabKind.Quickstart:
|
case ReactTabKind.Quickstart:
|
||||||
return userContext.apiType === "VCoreMongo" ? (
|
return userContext.apiType === "VCoreMongo" ? <VcoreMongoQuickstartTab /> : <QuickstartTab />;
|
||||||
<VcoreMongoQuickstartTab explorer={explorer} />
|
|
||||||
) : (
|
|
||||||
<QuickstartTab explorer={explorer} />
|
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
throw new Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
|||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
|
||||||
import { NotebookTerminalComponentAdapter } from "./ShellAdapters/NotebookTerminalComponentAdapter";
|
|
||||||
import TabsBase from "./TabsBase";
|
import TabsBase from "./TabsBase";
|
||||||
|
|
||||||
export interface TerminalTabOptions extends ViewModels.TabOptions {
|
export interface TerminalTabOptions extends ViewModels.TabOptions {
|
||||||
@@ -29,41 +27,17 @@ export default class TerminalTab extends TabsBase {
|
|||||||
this.container = options.container;
|
this.container = options.container;
|
||||||
this.isAllPublicIPAddressesEnabled = ko.observable(true);
|
this.isAllPublicIPAddressesEnabled = ko.observable(true);
|
||||||
|
|
||||||
const commonArgs: [
|
this.notebookTerminalComponentAdapter = new CloudShellTerminalComponentAdapter(
|
||||||
() => DataModels.DatabaseAccount,
|
|
||||||
() => string,
|
|
||||||
() => string,
|
|
||||||
ko.Observable<boolean>,
|
|
||||||
ViewModels.TerminalKind,
|
|
||||||
] = [
|
|
||||||
() => userContext?.databaseAccount,
|
() => userContext?.databaseAccount,
|
||||||
() => this.tabId,
|
() => this.tabId,
|
||||||
() => this.getUsername(),
|
() => this.getUsername(),
|
||||||
this.isAllPublicIPAddressesEnabled,
|
this.isAllPublicIPAddressesEnabled,
|
||||||
options.kind,
|
options.kind,
|
||||||
];
|
);
|
||||||
|
|
||||||
if (userContext.features.enableCloudShell) {
|
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||||
this.notebookTerminalComponentAdapter = new CloudShellTerminalComponentAdapter(...commonArgs);
|
return this.isTemplateReady() && this.isAllPublicIPAddressesEnabled();
|
||||||
|
});
|
||||||
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
|
||||||
return this.isTemplateReady() && this.isAllPublicIPAddressesEnabled();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
|
|
||||||
() => this.getNotebookServerInfo(options),
|
|
||||||
...commonArgs,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
|
||||||
return (
|
|
||||||
this.isTemplateReady() &&
|
|
||||||
useNotebook.getState().isNotebookEnabled &&
|
|
||||||
useNotebook.getState().notebookServerInfo?.notebookServerEndpoint &&
|
|
||||||
this.isAllPublicIPAddressesEnabled()
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.kind === ViewModels.TerminalKind.Postgres) {
|
if (options.kind === ViewModels.TerminalKind.Postgres) {
|
||||||
checkFirewallRules(
|
checkFirewallRules(
|
||||||
@@ -72,16 +46,6 @@ export default class TerminalTab extends TabsBase {
|
|||||||
this.isAllPublicIPAddressesEnabled,
|
this.isAllPublicIPAddressesEnabled,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.kind === ViewModels.TerminalKind.VCoreMongo) {
|
|
||||||
checkFirewallRules(
|
|
||||||
"2023-03-01-preview",
|
|
||||||
(rule) =>
|
|
||||||
rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") ||
|
|
||||||
(rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"),
|
|
||||||
this.isAllPublicIPAddressesEnabled,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getContainer(): Explorer {
|
public getContainer(): Explorer {
|
||||||
@@ -96,42 +60,6 @@ export default class TerminalTab extends TabsBase {
|
|||||||
this.updateNavbarWithTabsButtons();
|
this.updateNavbarWithTabsButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNotebookServerInfo(options: TerminalTabOptions): DataModels.NotebookWorkspaceConnectionInfo {
|
|
||||||
let endpointSuffix: string;
|
|
||||||
|
|
||||||
switch (options.kind) {
|
|
||||||
case ViewModels.TerminalKind.Default:
|
|
||||||
endpointSuffix = "";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ViewModels.TerminalKind.Mongo:
|
|
||||||
endpointSuffix = "mongo";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ViewModels.TerminalKind.Cassandra:
|
|
||||||
endpointSuffix = "cassandra";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ViewModels.TerminalKind.Postgres:
|
|
||||||
endpointSuffix = "postgresql";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ViewModels.TerminalKind.VCoreMongo:
|
|
||||||
endpointSuffix = "mongovcore";
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Terminal kind: ${options.kind} not supported`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const info: DataModels.NotebookWorkspaceConnectionInfo = useNotebook.getState().notebookServerInfo;
|
|
||||||
return {
|
|
||||||
authToken: info.authToken,
|
|
||||||
notebookServerEndpoint: `${info.notebookServerEndpoint.replace(/\/+$/, "")}/${endpointSuffix}`,
|
|
||||||
forwardingId: info.forwardingId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private getUsername(): string {
|
private getUsername(): string {
|
||||||
if (userContext.apiType !== "VCoreMongo" || !userContext?.vcoreMongoConnectionParams?.adminLogin) {
|
if (userContext.apiType !== "VCoreMongo" || !userContext?.vcoreMongoConnectionParams?.adminLogin) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -1,79 +1,24 @@
|
|||||||
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
import { Stack } from "@fluentui/react";
|
||||||
|
|
||||||
import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels";
|
import { TerminalKind } from "Contracts/ViewModels";
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
|
||||||
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
|
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
|
||||||
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
|
||||||
import { VcoreMongoQuickstartGuide } from "Explorer/Quickstart/VCoreMongoQuickstartGuide";
|
import { VcoreMongoQuickstartGuide } from "Explorer/Quickstart/VCoreMongoQuickstartGuide";
|
||||||
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
|
import { CloudShellTerminalComponent } from "Explorer/Tabs/CloudShellTab/CloudShellTerminalComponent";
|
||||||
import { userContext } from "UserContext";
|
import { userContext } from "UserContext";
|
||||||
import React, { useEffect, useState } from "react";
|
import React from "react";
|
||||||
import FirewallRuleScreenshot from "../../../images/vcoreMongoFirewallRule.png";
|
|
||||||
|
|
||||||
interface VCoreMongoQuickstartTabProps {
|
|
||||||
explorer: Explorer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VcoreMongoQuickstartTab: React.FC<VCoreMongoQuickstartTabProps> = ({
|
|
||||||
explorer,
|
|
||||||
}: VCoreMongoQuickstartTabProps): JSX.Element => {
|
|
||||||
const notebookServerInfo = useNotebook((state) => state.notebookServerInfo);
|
|
||||||
const [isAllPublicIPAddressEnabled, setIsAllPublicIPAddressEnabled] = useState<boolean>(true);
|
|
||||||
|
|
||||||
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
|
|
||||||
authToken: notebookServerInfo.authToken,
|
|
||||||
notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/mongovcore`,
|
|
||||||
forwardingId: notebookServerInfo.forwardingId,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
checkFirewallRules(
|
|
||||||
"2023-03-01-preview",
|
|
||||||
(rule) =>
|
|
||||||
rule.name.startsWith("AllowAllAzureServicesAndResourcesWithinAzureIps") ||
|
|
||||||
(rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"),
|
|
||||||
setIsAllPublicIPAddressEnabled,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
explorer.allocateContainer();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
export const VcoreMongoQuickstartTab: React.FC = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Stack style={{ width: "100%" }} horizontal>
|
<Stack style={{ width: "100%" }} horizontal>
|
||||||
<Stack style={{ width: "50%" }}>
|
<Stack style={{ width: "50%" }}>
|
||||||
<VcoreMongoQuickstartGuide />
|
<VcoreMongoQuickstartGuide />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
|
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
|
||||||
{!isAllPublicIPAddressEnabled && (
|
<CloudShellTerminalComponent
|
||||||
<QuickstartFirewallNotification
|
databaseAccount={userContext.databaseAccount}
|
||||||
messageType={MessageTypes.OpenVCoreMongoNetworkingBlade}
|
tabId="QuickstartVcoreMongoShell"
|
||||||
screenshot={FirewallRuleScreenshot}
|
username={userContext.vcoreMongoConnectionParams?.adminLogin}
|
||||||
shellName="MongoDB"
|
shellType={TerminalKind.VCoreMongo}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
|
|
||||||
<NotebookTerminalComponent
|
|
||||||
notebookServerInfo={getNotebookServerInfo()}
|
|
||||||
databaseAccount={userContext.databaseAccount}
|
|
||||||
tabId="QuickstartVcoreMongoShell"
|
|
||||||
username={userContext.vcoreMongoConnectionParams.adminLogin}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isAllPublicIPAddressEnabled && !notebookServerInfo?.notebookServerEndpoint && (
|
|
||||||
<Stack style={{ margin: "auto 0" }}>
|
|
||||||
<Text block style={{ margin: "auto" }}>
|
|
||||||
Connecting to the Mongo shell.
|
|
||||||
</Text>
|
|
||||||
<Text block style={{ margin: "auto" }}>
|
|
||||||
If the cluster was just created, this could take up to a minute.
|
|
||||||
</Text>
|
|
||||||
<Spinner styles={{ root: { marginTop: 16 } }} size={SpinnerSize.large}></Spinner>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
|
|||||||
import { Dialog } from "./Explorer/Controls/Dialog";
|
import { Dialog } from "./Explorer/Controls/Dialog";
|
||||||
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
|
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
|
||||||
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
|
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
|
||||||
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
|
|
||||||
import "./Explorer/Controls/TreeComponent/treeComponent.less";
|
import "./Explorer/Controls/TreeComponent/treeComponent.less";
|
||||||
import { ErrorBoundary } from "./Explorer/ErrorBoundary";
|
import { ErrorBoundary } from "./Explorer/ErrorBoundary";
|
||||||
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
/**
|
|
||||||
* JupyterLab applications based on jupyterLab components
|
|
||||||
*/
|
|
||||||
import { ServerConnection, TerminalManager } from "@jupyterlab/services";
|
|
||||||
import { IMessage, ITerminalConnection } from "@jupyterlab/services/lib/terminal/terminal";
|
|
||||||
import { Terminal } from "@jupyterlab/terminal";
|
|
||||||
import { Panel, Widget } from "@phosphor/widgets";
|
|
||||||
import { userContext } from "UserContext";
|
|
||||||
|
|
||||||
export class JupyterLabAppFactory {
|
|
||||||
private isShellStarted: boolean | undefined;
|
|
||||||
private checkShellStarted: ((content: string | undefined) => void) | undefined;
|
|
||||||
private onShellExited: (restartShell: boolean) => void;
|
|
||||||
private restartShell: boolean;
|
|
||||||
|
|
||||||
private isShellExited(content: string | undefined) {
|
|
||||||
if (userContext.apiType === "VCoreMongo" && content?.includes("MongoServerError: Invalid key")) {
|
|
||||||
this.restartShell = true;
|
|
||||||
}
|
|
||||||
return content?.includes("cosmosshelluser@");
|
|
||||||
}
|
|
||||||
|
|
||||||
private isMongoShellStarted(content: string | undefined) {
|
|
||||||
this.isShellStarted = content?.includes("MongoDB shell version");
|
|
||||||
}
|
|
||||||
|
|
||||||
private isCassandraShellStarted(content: string | undefined) {
|
|
||||||
this.isShellStarted = content?.includes("Connected to") && content?.includes("cqlsh");
|
|
||||||
}
|
|
||||||
|
|
||||||
private isPostgresShellStarted(content: string | undefined) {
|
|
||||||
this.isShellStarted = content?.includes("citus=>");
|
|
||||||
}
|
|
||||||
|
|
||||||
private isVCoreMongoShellStarted(content: string | undefined) {
|
|
||||||
this.isShellStarted = content?.includes("Enter password");
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(closeTab: (restartShell: boolean) => void) {
|
|
||||||
this.onShellExited = closeTab;
|
|
||||||
this.isShellStarted = false;
|
|
||||||
this.checkShellStarted = undefined;
|
|
||||||
this.restartShell = false;
|
|
||||||
|
|
||||||
switch (userContext.apiType) {
|
|
||||||
case "Mongo":
|
|
||||||
this.checkShellStarted = this.isMongoShellStarted;
|
|
||||||
break;
|
|
||||||
case "Cassandra":
|
|
||||||
this.checkShellStarted = this.isCassandraShellStarted;
|
|
||||||
break;
|
|
||||||
case "Postgres":
|
|
||||||
this.checkShellStarted = this.isPostgresShellStarted;
|
|
||||||
break;
|
|
||||||
case "VCoreMongo":
|
|
||||||
this.checkShellStarted = this.isVCoreMongoShellStarted;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async createTerminalApp(serverSettings: ServerConnection.ISettings): Promise<ITerminalConnection | undefined> {
|
|
||||||
const configurationSettings: Partial<ServerConnection.ISettings> = serverSettings;
|
|
||||||
(configurationSettings.appendToken as boolean) = false;
|
|
||||||
serverSettings = ServerConnection.makeSettings(configurationSettings);
|
|
||||||
const manager = new TerminalManager({
|
|
||||||
serverSettings: serverSettings,
|
|
||||||
});
|
|
||||||
const session = await manager.startNew();
|
|
||||||
session.messageReceived.connect(async (_, message: IMessage) => {
|
|
||||||
const content = message.content && message.content[0]?.toString();
|
|
||||||
if (this.checkShellStarted && message.type == "stdout") {
|
|
||||||
//Close the terminal tab once the shell closed messages are received
|
|
||||||
if (!this.isShellStarted) {
|
|
||||||
this.checkShellStarted(content);
|
|
||||||
} else if (this.isShellExited(content)) {
|
|
||||||
this.onShellExited(this.restartShell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, this);
|
|
||||||
|
|
||||||
let internalSend = session.send;
|
|
||||||
session.send = (message: IMessage) => {
|
|
||||||
message?.content?.push(serverSettings?.token);
|
|
||||||
internalSend.call(session, message);
|
|
||||||
};
|
|
||||||
const term = new Terminal(session, { theme: "dark", shutdownOnClose: true });
|
|
||||||
|
|
||||||
if (!term) {
|
|
||||||
console.error("Failed starting terminal");
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
term.title.closable = false;
|
|
||||||
term.addClass("terminalWidget");
|
|
||||||
|
|
||||||
let panel = new Panel();
|
|
||||||
panel.addWidget(term as any);
|
|
||||||
panel.id = "main";
|
|
||||||
|
|
||||||
// Attach the widget to the dom.
|
|
||||||
Widget.attach(panel, document.body);
|
|
||||||
|
|
||||||
// Switch focus to the terminal
|
|
||||||
term.activate();
|
|
||||||
|
|
||||||
// Handle resize events.
|
|
||||||
window.addEventListener("resize", () => {
|
|
||||||
panel.update();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dispose terminal when unloading.
|
|
||||||
window.addEventListener("unload", () => {
|
|
||||||
panel.dispose();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close terminal when Ctrl key is pressed
|
|
||||||
term.node.addEventListener("keydown", (event: KeyboardEvent) => {
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
this.onShellExited(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-111
@@ -1,111 +0,0 @@
|
|||||||
/**
|
|
||||||
* Message handling with iframe parent
|
|
||||||
*/
|
|
||||||
export interface UpdateMessage {
|
|
||||||
command: string;
|
|
||||||
arg?: any;
|
|
||||||
}
|
|
||||||
export declare type ContentType = "notebook" | "file" | "directory";
|
|
||||||
export interface ContentItem {
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
type: ContentType;
|
|
||||||
}
|
|
||||||
export interface UploadData {
|
|
||||||
filepath: string;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
export interface RenameFileData {
|
|
||||||
sourcePath: string;
|
|
||||||
targetPath: string;
|
|
||||||
}
|
|
||||||
export interface RenameFileResult {
|
|
||||||
source: string;
|
|
||||||
target: ContentItem;
|
|
||||||
}
|
|
||||||
export interface FromDataExplorerMessage {
|
|
||||||
type: MessageTypes;
|
|
||||||
params: any;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
export declare type KernelStatusStates =
|
|
||||||
| "unknown"
|
|
||||||
| "starting"
|
|
||||||
| "reconnecting"
|
|
||||||
| "idle"
|
|
||||||
| "busy"
|
|
||||||
| "restarting"
|
|
||||||
| "autorestarting"
|
|
||||||
| "dead"
|
|
||||||
| "connected";
|
|
||||||
/**
|
|
||||||
* Unsolicited message
|
|
||||||
*/
|
|
||||||
export interface FromNotebookUpdateMessage {
|
|
||||||
type: NotebookUpdateTypes;
|
|
||||||
arg?: any;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Response to a Data Explorer request
|
|
||||||
*/
|
|
||||||
export interface FromNotebookResponseMessage {
|
|
||||||
id: string;
|
|
||||||
data?: any;
|
|
||||||
error?: any;
|
|
||||||
}
|
|
||||||
export interface FromNotebookMessage {
|
|
||||||
actionType: ActionTypes;
|
|
||||||
message: FromNotebookUpdateMessage | FromNotebookResponseMessage;
|
|
||||||
}
|
|
||||||
export declare type KernelOption = {
|
|
||||||
name: string;
|
|
||||||
displayName: string;
|
|
||||||
};
|
|
||||||
export interface KernelSpecs {
|
|
||||||
defaultName: string;
|
|
||||||
kernelSpecs: {
|
|
||||||
[name: string]: KernelOption;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export declare enum ActionTypes {
|
|
||||||
Update = 0,
|
|
||||||
Response = 1,
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Messages Data Explorer -> JupyterLabApp
|
|
||||||
*/
|
|
||||||
export declare enum MessageTypes {
|
|
||||||
FileList = 0,
|
|
||||||
CreateInDir = 1,
|
|
||||||
DeleteFile = 2,
|
|
||||||
UploadFile = 3,
|
|
||||||
RenameFile = 4,
|
|
||||||
ReadFileContent = 5,
|
|
||||||
CreateDirectory = 6,
|
|
||||||
InsertBelow = 7,
|
|
||||||
RunAndAdvance = 8,
|
|
||||||
Copy = 9,
|
|
||||||
Cut = 10,
|
|
||||||
Paste = 11,
|
|
||||||
Undo = 12,
|
|
||||||
ClearAllOutputs = 13,
|
|
||||||
RunAll = 14,
|
|
||||||
Redo = 15,
|
|
||||||
Save = 16,
|
|
||||||
RestartKernel = 17,
|
|
||||||
ChangeCellType = 18,
|
|
||||||
SwitchKernel = 19,
|
|
||||||
ChangeKernel = 20,
|
|
||||||
Status = 21,
|
|
||||||
KernelList = 22,
|
|
||||||
IsDirty = 23,
|
|
||||||
Shutdown = 24,
|
|
||||||
}
|
|
||||||
export declare enum NotebookUpdateTypes {
|
|
||||||
Ready = 0,
|
|
||||||
ClickEvent = 1,
|
|
||||||
ActiveCellType = 2,
|
|
||||||
KernelChange = 3,
|
|
||||||
FileSaved = 4,
|
|
||||||
SessionStatusChange = 5,
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
# Summary
|
|
||||||
This describes how to run a custom version of the Data Explorer in the Emulator which can open a jupyter notebook from with a tab.
|
|
||||||
|
|
||||||
# Requirements
|
|
||||||
This requires:
|
|
||||||
* a running instance of CosmosDB Emulator
|
|
||||||
* a running instance of the jupyter server
|
|
||||||
* access to the cosmosdb-dataexplorer git repository
|
|
||||||
|
|
||||||
# Installation
|
|
||||||
## Install CosmosDB Emulator
|
|
||||||
* Download from https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator
|
|
||||||
* Open the Emulator and create at least one Collection
|
|
||||||
|
|
||||||
## Install Jupyter server on local machine (Windows)
|
|
||||||
We use the Anaconda distribution which comes with a packaged jupyter and python.
|
|
||||||
* Download and install Anaconda from https://www.anaconda.com/distribution/ (python3 64-bit version)
|
|
||||||
Keep all default options. Install Visual Studio Code as well.
|
|
||||||
### Verify Jupyter installation and create mynotebook
|
|
||||||
* Open an "Anaconda Prompt" (hit the Window key, type "Anaconda", select "Anaconda Prompt" hit Enter)
|
|
||||||
> cd src/jupyter-server (the notebooks will be saved in this directory)
|
|
||||||
> jupyter notebook
|
|
||||||
* It should open the browser at http://localhost:8888/ with the jupyter notebook.
|
|
||||||
* Edit the notebook and save it as "mynotebook" (This should create a file: mynotebook.ipynb).
|
|
||||||
We do this, because right now, the notebook filename is hardcoded as mynotebook.
|
|
||||||
|
|
||||||
### Modify jupyter server install
|
|
||||||
In order to serve the jupyter frontend from the emulator, we need to turn off a bunch of things.
|
|
||||||
* Stop the jupyter server (Ctrl-C twice from the Anaconda Prompt where you started jupyter notebook)
|
|
||||||
* From the Anaconda Prompt, type: juypter notebook --generate-config
|
|
||||||
* This should create the file: .jupyter/jupyter_notebook in your home directory.
|
|
||||||
* Edit this file:
|
|
||||||
|
|
||||||
Enable embedding the jupyter frontend inside an iFrame in DataExplorer:
|
|
||||||
c.NotebookApp.tornado_settings = { 'headers': { 'Content-Security-Policy': "frame-ancestors * localhost:1234 localhost:12900"} }
|
|
||||||
|
|
||||||
Enable a remotely-served jupyter frontend to still talk to the jupyter server:
|
|
||||||
c.NotebookApp.allow_origin = '*'
|
|
||||||
c.NotebookApp.allow_remote_access = True <--- not sure if this one matters
|
|
||||||
c.NotebookApp.token = ''
|
|
||||||
c.NotebookApp.disable_check_xsrf = True
|
|
||||||
|
|
||||||
## Install custom Data Explorer in Emulator
|
|
||||||
* Install git from https://git-scm.com/download/win (keep all default options)
|
|
||||||
* Install nodejs and npm from: https://nodejs.org/en/ (10.15.1 LTS)
|
|
||||||
|
|
||||||
### Download and build Data Explorer
|
|
||||||
* From the Git Bash terminal:
|
|
||||||
* cd ~/src
|
|
||||||
* git clone https://msdata.visualstudio.com/DefaultCollection/CosmosDB/_git/cosmosdb-dataexplorer
|
|
||||||
* cd cosmosdb-dataexplorer/Product/Portal
|
|
||||||
* git checkout users/languye/spark-in-dataexplorer
|
|
||||||
* cd JupyterLab
|
|
||||||
* npm i
|
|
||||||
* npm run build (this builds jupyterlab (the frontend of jupyter) and copies it into ../DataExplorer/notebookapp/)
|
|
||||||
* cd ../DataExplorer
|
|
||||||
* npm i
|
|
||||||
* npm run build (this builds and copies DataExplorer into the Emulator folder)
|
|
||||||
|
|
||||||
# How to run the setup
|
|
||||||
* Run the jupter-server by opening an Anaconda Prompt and typing: jupyter notebook
|
|
||||||
* Open the emulator at: http://localhost:8081/_explorer/index.html
|
|
||||||
* Click on any Collection
|
|
||||||
* Click on "New Notebook" button in the Command bar
|
|
||||||
* You should see the "mynotebook" jupyter notebook displayed in tab (inside an iframe).
|
|
||||||
* There is a "New Cell" button in the CommandBar outside the jupyter iframe which will add a cell inside the notebook.
|
|
||||||
|
|
||||||
# Notes
|
|
||||||
* The Emulator is located in: C:\Program Files\Azure Cosmos DB Emulator\Packages\DataExplorer
|
|
||||||
* Running "jupyter notebook" serves the jupyter traditional frontend. There is an alternate frontend also developed by jupyter which is modular and customizable called: JupyterLab. We use their "notebook" example in this project slightly modified to pass the server and notebook pathname via iframe url's parameters:
|
|
||||||
https://github.com/jupyterlab/jupyterlab/tree/master/examples/notebook
|
|
||||||
jupyterlab uses the same communication protocol as the traditional frontend, so it can connect to any jupyter-server,
|
|
||||||
so one can use multiple frontends (at the same time) to connect to a given jupyter-server.
|
|
||||||
* The jupyter frontend and the server use websockets to communicate.
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { AuthType } from "../AuthType";
|
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
|
||||||
import { ApiType } from "../UserContext";
|
|
||||||
|
|
||||||
export interface TerminalProps {
|
|
||||||
authToken: string;
|
|
||||||
notebookServerEndpoint: string;
|
|
||||||
terminalEndpoint: string;
|
|
||||||
databaseAccount: DataModels.DatabaseAccount;
|
|
||||||
authType: AuthType;
|
|
||||||
apiType: ApiType;
|
|
||||||
subscriptionId: string;
|
|
||||||
tabId: string;
|
|
||||||
username?: string;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
/*-----------------------------------------------------------------------------
|
|
||||||
| Copyright (c) Jupyter Development Team.
|
|
||||||
| Distributed under the terms of the Modified BSD License.
|
|
||||||
|----------------------------------------------------------------------------*/
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: white;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#main {
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
right: 0px;
|
|
||||||
bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jp-NotebookPanel {
|
|
||||||
border-bottom: 1px solid #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminalWidget {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>Notebook</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script id="jupyter-config-data" type="application/json">
|
|
||||||
{
|
|
||||||
"terminalsAvailable": "true"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import { ServerConnection } from "@jupyterlab/services";
|
|
||||||
import { IMessage, ITerminalConnection } from "@jupyterlab/services/lib/terminal/terminal";
|
|
||||||
import "@jupyterlab/terminal/style/index.css";
|
|
||||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
|
||||||
import postRobot from "post-robot";
|
|
||||||
import { HttpHeaders } from "../Common/Constants";
|
|
||||||
import { Action } from "../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { updateUserContext } from "../UserContext";
|
|
||||||
import { JupyterLabAppFactory } from "./JupyterLabAppFactory";
|
|
||||||
import { TerminalProps } from "./TerminalProps";
|
|
||||||
import "./index.css";
|
|
||||||
|
|
||||||
let session: ITerminalConnection | undefined;
|
|
||||||
|
|
||||||
const createServerSettings = (props: TerminalProps): ServerConnection.ISettings => {
|
|
||||||
let body: BodyInit | undefined;
|
|
||||||
let headers: HeadersInit | undefined;
|
|
||||||
if (props.terminalEndpoint) {
|
|
||||||
let bodyObj: { endpoint: string; username?: string } = {
|
|
||||||
endpoint: props.terminalEndpoint,
|
|
||||||
};
|
|
||||||
if (props.username) {
|
|
||||||
bodyObj = {
|
|
||||||
...bodyObj,
|
|
||||||
username: props.username,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
body = JSON.stringify(bodyObj);
|
|
||||||
headers = {
|
|
||||||
[HttpHeaders.contentType]: "application/json",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = props.notebookServerEndpoint;
|
|
||||||
let options: Partial<ServerConnection.ISettings> = {
|
|
||||||
baseUrl: server,
|
|
||||||
init: { body, headers },
|
|
||||||
fetch: window.parent.fetch,
|
|
||||||
};
|
|
||||||
if (props.authToken) {
|
|
||||||
options = {
|
|
||||||
baseUrl: server,
|
|
||||||
token: props.authToken,
|
|
||||||
appendToken: true,
|
|
||||||
init: { body, headers },
|
|
||||||
fetch: window.parent.fetch,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return ServerConnection.makeSettings(options);
|
|
||||||
};
|
|
||||||
|
|
||||||
const initTerminal = async (props: TerminalProps): Promise<void> => {
|
|
||||||
// Initialize userContext (only properties which are needed by TelemetryProcessor)
|
|
||||||
updateUserContext({
|
|
||||||
subscriptionId: props.subscriptionId,
|
|
||||||
apiType: props.apiType,
|
|
||||||
authType: props.authType,
|
|
||||||
databaseAccount: props.databaseAccount,
|
|
||||||
});
|
|
||||||
|
|
||||||
const serverSettings = createServerSettings(props);
|
|
||||||
|
|
||||||
createTerminalApp(props, serverSettings);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createTerminalApp = async (props: TerminalProps, serverSettings: ServerConnection.ISettings) => {
|
|
||||||
const data = { baseUrl: serverSettings.baseUrl };
|
|
||||||
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
|
|
||||||
|
|
||||||
try {
|
|
||||||
session = await new JupyterLabAppFactory((restartShell: boolean) =>
|
|
||||||
closeTab(props, serverSettings, restartShell),
|
|
||||||
).createTerminalApp(serverSettings);
|
|
||||||
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
|
|
||||||
} catch (error) {
|
|
||||||
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
|
|
||||||
session = undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeTab = (props: TerminalProps, serverSettings: ServerConnection.ISettings, restartShell: boolean): void => {
|
|
||||||
if (restartShell) {
|
|
||||||
createTerminalApp(props, serverSettings);
|
|
||||||
} else {
|
|
||||||
window.parent.postMessage(
|
|
||||||
{ type: MessageTypes.CloseTab, data: { tabId: props.tabId }, signature: "pcIframe" },
|
|
||||||
window.document.referrer,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const main = async (): Promise<void> => {
|
|
||||||
postRobot.on(
|
|
||||||
"props",
|
|
||||||
{
|
|
||||||
window: window.parent,
|
|
||||||
domain: window.location.origin,
|
|
||||||
},
|
|
||||||
async (event) => {
|
|
||||||
// Typescript definition for event is wrong. So read props by casting to <any>
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const props = (event as any).data as TerminalProps;
|
|
||||||
await initTerminal(props);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
postRobot.on(
|
|
||||||
"sendMessage",
|
|
||||||
{
|
|
||||||
window: window.parent,
|
|
||||||
domain: window.location.origin,
|
|
||||||
},
|
|
||||||
async (event) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const message = (event as any).data as IMessage;
|
|
||||||
if (session) {
|
|
||||||
session.send(message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("load", main);
|
|
||||||
@@ -1,26 +1,20 @@
|
|||||||
import postRobot from "post-robot";
|
|
||||||
import create, { UseStore } from "zustand";
|
import create, { UseStore } from "zustand";
|
||||||
|
|
||||||
interface TerminalState {
|
interface TerminalState {
|
||||||
terminalWindow: Window;
|
socket: WebSocket | undefined;
|
||||||
setTerminal: (terminalWindow: Window) => void;
|
setSocket: (socket: WebSocket | undefined) => void;
|
||||||
sendMessage: (message: string) => void;
|
sendMessage: (message: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTerminal: UseStore<TerminalState> = create((set, get) => ({
|
export const useTerminal: UseStore<TerminalState> = create((set, get) => ({
|
||||||
terminalWindow: undefined,
|
socket: undefined,
|
||||||
setTerminal: (terminalWindow: Window) => {
|
setSocket: (socket: WebSocket | undefined) => {
|
||||||
set({ terminalWindow });
|
set({ socket });
|
||||||
},
|
},
|
||||||
sendMessage: (message: string) => {
|
sendMessage: (message: string) => {
|
||||||
const terminalWindow = get().terminalWindow;
|
const socket = get().socket;
|
||||||
postRobot.send(
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||||
terminalWindow,
|
socket.send(message + "\r");
|
||||||
"sendMessage",
|
}
|
||||||
{ type: "stdin", content: [message] },
|
|
||||||
{
|
|
||||||
domain: window.location.origin,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -145,7 +145,6 @@
|
|||||||
"src/Platform/Emulator/**/*",
|
"src/Platform/Emulator/**/*",
|
||||||
"src/SelfServe/Documentation/**/*",
|
"src/SelfServe/Documentation/**/*",
|
||||||
"src/Shared/Telemetry/**/*",
|
"src/Shared/Telemetry/**/*",
|
||||||
"src/Terminal/**/*",
|
|
||||||
"src/Utils/arm/**/*"
|
"src/Utils/arm/**/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,6 @@ module.exports = function (_env = {}, argv = {}) {
|
|||||||
index: "./src/Index.tsx",
|
index: "./src/Index.tsx",
|
||||||
quickstart: "./src/quickstart.ts",
|
quickstart: "./src/quickstart.ts",
|
||||||
hostedExplorer: "./src/HostedExplorer.tsx",
|
hostedExplorer: "./src/HostedExplorer.tsx",
|
||||||
terminal: "./src/Terminal/index.ts",
|
|
||||||
cellOutputViewer: "./src/CellOutputViewer/CellOutputViewer.tsx",
|
cellOutputViewer: "./src/CellOutputViewer/CellOutputViewer.tsx",
|
||||||
selfServe: "./src/SelfServe/SelfServe.tsx",
|
selfServe: "./src/SelfServe/SelfServe.tsx",
|
||||||
connectToGitHub: "./src/GitHub/GitHubConnector.ts",
|
connectToGitHub: "./src/GitHub/GitHubConnector.ts",
|
||||||
@@ -128,11 +127,6 @@ module.exports = function (_env = {}, argv = {}) {
|
|||||||
template: "src/explorer.html",
|
template: "src/explorer.html",
|
||||||
chunks: ["main"],
|
chunks: ["main"],
|
||||||
}),
|
}),
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
filename: "terminal.html",
|
|
||||||
template: "src/Terminal/index.html",
|
|
||||||
chunks: ["terminal"],
|
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
filename: "quickstart.html",
|
filename: "quickstart.html",
|
||||||
template: "src/quickstart.html",
|
template: "src/quickstart.html",
|
||||||
|
|||||||
Reference in New Issue
Block a user