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:
jawelton74
2026-06-15 06:29:14 -07:00
committed by GitHub
parent 74b6a92219
commit d19c7e0cb7
23 changed files with 259 additions and 1078 deletions
+214
View File
@@ -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>
`;
+1 -18
View File
@@ -959,25 +959,8 @@ export default class Explorer {
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
}
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
if (userContext.features.enableCloudShell) {
public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
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 {
@@ -1,3 +1,4 @@
import { useTerminal } from "hooks/useTerminal";
import React, { useEffect, useRef } from "react";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
@@ -59,7 +60,14 @@ export const CloudShellTerminalComponent: React.FC<CloudShellTerminalComponentPr
});
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
return () => {
@@ -73,6 +81,7 @@ export const CloudShellTerminalComponent: React.FC<CloudShellTerminalComponentPr
resizeObserver.unobserve(terminalRef.current);
}
terminal.dispose(); // Clean up XTerm instance
useTerminal.getState().setSocket(undefined);
};
}, []);
+7 -35
View File
@@ -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 { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
import Explorer from "Explorer/Explorer";
import { useNotebook } from "Explorer/Notebook/useNotebook";
import { TerminalKind } from "Contracts/ViewModels";
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
import { QuickstartGuide } from "Explorer/Quickstart/QuickstartGuide";
import { CloudShellTerminalComponent } from "Explorer/Tabs/CloudShellTab/CloudShellTerminalComponent";
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
import { userContext } from "UserContext";
import React, { useEffect, useState } from "react";
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
interface QuickstartTabProps {
explorer: Explorer;
}
export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: QuickstartTabProps): JSX.Element => {
const notebookServerInfo = useNotebook((state) => state.notebookServerInfo);
export const QuickstartTab: React.FC = (): JSX.Element => {
const [isAllPublicIPAddressEnabled, setIsAllPublicIPAddressEnabled] = useState<boolean>(true);
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
authToken: notebookServerInfo.authToken,
notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/postgresql`,
forwardingId: notebookServerInfo.forwardingId,
});
useEffect(() => {
checkFirewallRules(
"2022-11-08",
@@ -34,10 +21,6 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
);
});
useEffect(() => {
explorer.allocateContainer();
}, []);
return (
<Stack style={{ width: "100%" }} horizontal>
<Stack style={{ width: "50%" }}>
@@ -51,24 +34,13 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
shellName="PostgreSQL"
/>
)}
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
<NotebookTerminalComponent
notebookServerInfo={getNotebookServerInfo()}
{isAllPublicIPAddressEnabled && (
<CloudShellTerminalComponent
databaseAccount={userContext.databaseAccount}
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>
);
@@ -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()}
/>
);
}
}
+1 -5
View File
@@ -333,11 +333,7 @@ const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): J
return <SplashScreen explorer={explorer} />;
}
case ReactTabKind.Quickstart:
return userContext.apiType === "VCoreMongo" ? (
<VcoreMongoQuickstartTab explorer={explorer} />
) : (
<QuickstartTab explorer={explorer} />
);
return userContext.apiType === "VCoreMongo" ? <VcoreMongoQuickstartTab /> : <QuickstartTab />;
default:
throw new Error(`Unsupported tab kind ${ReactTabKind[activeReactTab]}`);
}
+2 -74
View File
@@ -7,8 +7,6 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { userContext } from "../../UserContext";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import { useNotebook } from "../Notebook/useNotebook";
import { NotebookTerminalComponentAdapter } from "./ShellAdapters/NotebookTerminalComponentAdapter";
import TabsBase from "./TabsBase";
export interface TerminalTabOptions extends ViewModels.TabOptions {
@@ -29,41 +27,17 @@ export default class TerminalTab extends TabsBase {
this.container = options.container;
this.isAllPublicIPAddressesEnabled = ko.observable(true);
const commonArgs: [
() => DataModels.DatabaseAccount,
() => string,
() => string,
ko.Observable<boolean>,
ViewModels.TerminalKind,
] = [
this.notebookTerminalComponentAdapter = new CloudShellTerminalComponentAdapter(
() => userContext?.databaseAccount,
() => this.tabId,
() => this.getUsername(),
this.isAllPublicIPAddressesEnabled,
options.kind,
];
if (userContext.features.enableCloudShell) {
this.notebookTerminalComponentAdapter = new CloudShellTerminalComponentAdapter(...commonArgs);
);
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) {
checkFirewallRules(
@@ -72,16 +46,6 @@ export default class TerminalTab extends TabsBase {
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 {
@@ -96,42 +60,6 @@ export default class TerminalTab extends TabsBase {
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 {
if (userContext.apiType !== "VCoreMongo" || !userContext?.vcoreMongoConnectionParams?.adminLogin) {
return undefined;
+8 -63
View File
@@ -1,79 +1,24 @@
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import { Stack } from "@fluentui/react";
import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels";
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 { TerminalKind } from "Contracts/ViewModels";
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 React, { useEffect, useState } 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();
}, []);
import React from "react";
export const VcoreMongoQuickstartTab: React.FC = (): JSX.Element => {
return (
<Stack style={{ width: "100%" }} horizontal>
<Stack style={{ width: "50%" }}>
<VcoreMongoQuickstartGuide />
</Stack>
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
{!isAllPublicIPAddressEnabled && (
<QuickstartFirewallNotification
messageType={MessageTypes.OpenVCoreMongoNetworkingBlade}
screenshot={FirewallRuleScreenshot}
shellName="MongoDB"
/>
)}
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
<NotebookTerminalComponent
notebookServerInfo={getNotebookServerInfo()}
<CloudShellTerminalComponent
databaseAccount={userContext.databaseAccount}
tabId="QuickstartVcoreMongoShell"
username={userContext.vcoreMongoConnectionParams.adminLogin}
username={userContext.vcoreMongoConnectionParams?.adminLogin}
shellType={TerminalKind.VCoreMongo}
/>
)}
{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>
);
-1
View File
@@ -47,7 +47,6 @@ import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
import { Dialog } from "./Explorer/Controls/Dialog";
import "./Explorer/Controls/ErrorDisplayComponent/ErrorDisplayComponent.less";
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
import "./Explorer/Controls/Notebook/NotebookTerminalComponent.less";
import "./Explorer/Controls/TreeComponent/treeComponent.less";
import { ErrorBoundary } from "./Explorer/ErrorBoundary";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
-125
View File
@@ -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
View File
@@ -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,
}
-74
View File
@@ -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.
-15
View File
@@ -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;
}
-26
View File
@@ -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%;
}
-13
View File
@@ -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>
-125
View File
@@ -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);
+9 -15
View File
@@ -1,26 +1,20 @@
import postRobot from "post-robot";
import create, { UseStore } from "zustand";
interface TerminalState {
terminalWindow: Window;
setTerminal: (terminalWindow: Window) => void;
socket: WebSocket | undefined;
setSocket: (socket: WebSocket | undefined) => void;
sendMessage: (message: string) => void;
}
export const useTerminal: UseStore<TerminalState> = create((set, get) => ({
terminalWindow: undefined,
setTerminal: (terminalWindow: Window) => {
set({ terminalWindow });
socket: undefined,
setSocket: (socket: WebSocket | undefined) => {
set({ socket });
},
sendMessage: (message: string) => {
const terminalWindow = get().terminalWindow;
postRobot.send(
terminalWindow,
"sendMessage",
{ type: "stdin", content: [message] },
{
domain: window.location.origin,
},
);
const socket = get().socket;
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(message + "\r");
}
},
}));
-1
View File
@@ -145,7 +145,6 @@
"src/Platform/Emulator/**/*",
"src/SelfServe/Documentation/**/*",
"src/Shared/Telemetry/**/*",
"src/Terminal/**/*",
"src/Utils/arm/**/*"
]
}
-6
View File
@@ -111,7 +111,6 @@ module.exports = function (_env = {}, argv = {}) {
index: "./src/Index.tsx",
quickstart: "./src/quickstart.ts",
hostedExplorer: "./src/HostedExplorer.tsx",
terminal: "./src/Terminal/index.ts",
cellOutputViewer: "./src/CellOutputViewer/CellOutputViewer.tsx",
selfServe: "./src/SelfServe/SelfServe.tsx",
connectToGitHub: "./src/GitHub/GitHubConnector.ts",
@@ -128,11 +127,6 @@ module.exports = function (_env = {}, argv = {}) {
template: "src/explorer.html",
chunks: ["main"],
}),
new HtmlWebpackPlugin({
filename: "terminal.html",
template: "src/Terminal/index.html",
chunks: ["terminal"],
}),
new HtmlWebpackPlugin({
filename: "quickstart.html",
template: "src/quickstart.html",