mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-06-29 01:27:22 +01:00
Remove Phoenix & Notebooks - Phase 2: Remove in-app notebook authoring & rendering (#2515)
Delete the nteract rendering engine, notebook tabs, panes, the read-only viewer, Schema Analyzer (pulled forward from Phase 3), and all UI entry points that open notebooks. Decouple surviving files (Explorer, NotebookManager, useNotebook, ResourceTreeAdapter) with minimal edits, keeping GitHub/Juno/Phoenix wiring for later phases. Removed 22 zero-importer notebook-only npm deps; re-added phantom transitively-hoisted deps still used by surviving code: xterm, xterm-addon-fit (CloudShell), d3-collection (Graph), @nteract/myths (@nteract/core). Verified: compile, compile:strict, lint (0 errors), format:check, test (1945 passing), build:ci all green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -111,7 +111,10 @@ run: `npm run compile`, `npm run compile:strict`, `npm run lint`, `npm run forma
|
|||||||
`npm test`, and a webpack build (`npm run build:ci`); manually verify the four shells
|
`npm test`, and a webpack build (`npm run build:ci`); manually verify the four shells
|
||||||
still open.
|
still open.
|
||||||
|
|
||||||
### Phase 1 — Decouple database shells to CloudShell-only
|
### Phase 1 — Decouple database shells to CloudShell-only ✅ COMPLETED
|
||||||
|
> **Status:** Completed and merged in `d19c7e0c` ("Remove Phoenix & Notebooks — Phase 1:
|
||||||
|
> Decouple database shells to CloudShell (#2513)").
|
||||||
|
|
||||||
Remove the legacy Phoenix notebook-server terminal path so shells no longer depend on
|
Remove the legacy Phoenix notebook-server terminal path so shells no longer depend on
|
||||||
notebook provisioning.
|
notebook provisioning.
|
||||||
- Rewire `TerminalTab` to always use `CloudShellTerminalComponentAdapter`; delete the
|
- Rewire `TerminalTab` to always use `CloudShellTerminalComponentAdapter`; delete the
|
||||||
@@ -124,29 +127,77 @@ notebook provisioning.
|
|||||||
CloudShell. If unused, remove it and `src/Terminal/`; otherwise keep.
|
CloudShell. If unused, remove it and `src/Terminal/`; otherwise keep.
|
||||||
- **Outcome:** Shells run purely on CloudShell. Phoenix no longer needed for terminals.
|
- **Outcome:** Shells run purely on CloudShell. Phoenix no longer needed for terminals.
|
||||||
|
|
||||||
### Phase 2 — Remove the in-app notebook authoring & rendering experience
|
### Phase 2 — Remove the in-app notebook authoring & rendering experience ✅ COMPLETED
|
||||||
Delete the notebook tabs, the nteract rendering engine, panes, and the read-only viewer,
|
> **Status:** Implemented on branch `users/jawelton/removenotebooks-phase2-061526`. Full
|
||||||
and remove all UI entry points that open notebooks.
|
> verification sweep is green: `compile`, `compile:strict`, `lint` (0 errors), `format:check`,
|
||||||
- Delete: `src/Explorer/Notebook/NotebookComponent/`,
|
> `test` (1945 passing), and `build:ci` (webpack) all pass.
|
||||||
|
>
|
||||||
|
> **Implementation notes / deviations:**
|
||||||
|
> - `src/Explorer/Panes/StringInputPane/` was also deleted — it became unused once
|
||||||
|
> `Explorer.renameNotebook` / `onCreateDirectory` were removed.
|
||||||
|
> - Removing notebook-only npm deps stripped several **phantom (transitively-hoisted)**
|
||||||
|
> packages that surviving code still imports, so these were re-added as explicit direct
|
||||||
|
> dependencies: `xterm@4.19.0` + `xterm-addon-fit@0.5.0` (CloudShell), `d3-collection@1.0.7`
|
||||||
|
> (Graph `D3ForceGraph`), and `@nteract/myths@0.1.9` (needed by kept `@nteract/core`).
|
||||||
|
> - Jest snapshots regenerated: `SettingsComponent`, `AddGlobalSecondaryIndexPanel`,
|
||||||
|
> `GitHubReposPanel` (all just dropped the removed `copyNotebook` function from the
|
||||||
|
> serialized Explorer); deleted-component snapshots removed with their dirs.
|
||||||
|
> - `CommandBarComponentButtonFactory` / `ContextMenuButtonFactory` required no notebook-button
|
||||||
|
> removal — they only contained shell entries (kept).
|
||||||
|
|
||||||
|
Delete the notebook tabs, the nteract rendering engine, panes, the read-only viewer, and
|
||||||
|
Schema Analyzer (see decision below), and remove all UI entry points that open notebooks.
|
||||||
|
|
||||||
|
> **Cross-phase coupling — revised approach (confirmed):** Several files the original plan
|
||||||
|
> deferred to later phases are hard-coupled to the deletions above and must be handled here:
|
||||||
|
> - **Schema Analyzer is pulled forward from Phase 3 into Phase 2.** `SchemaAnalyzer.tsx`
|
||||||
|
> renders through the nteract engine (`NotebookComponent/loadTransform`,
|
||||||
|
> `NotebookRenderer/outputs/SandboxOutputs`) and `SchemaAnalyzerTab extends NotebookTabBase`,
|
||||||
|
> so it cannot compile once the engine is deleted.
|
||||||
|
> - **`NotebookContentClient.ts` and `FileSystemUtil.ts` are deleted here** (they depend on
|
||||||
|
> the deleted `@nteract` content providers and only serve removed authoring paths).
|
||||||
|
> - **`NotebookManager.tsx` / `useNotebook.ts` get minimal edits** (full deletion stays in
|
||||||
|
> Phase 5): strip the content-provider / content-client / CopyNotebookPane / SchemaAnalyzer
|
||||||
|
> usages, keeping the GitHub / Juno / `NotebookContainerClient` wiring later phases need.
|
||||||
|
> - **`NotebookContentItem.ts` and `NotebookUtil.ts` are DEFERRED, not deleted here.** They
|
||||||
|
> are still used by the surviving `useNotebook` store and the "My Notebooks" tree
|
||||||
|
> (`ResourceTreeAdapter`), and `NotebookUtil.getName`/`isNotebookFile` are still used by the
|
||||||
|
> GitHub client/content provider. They are removed in Phase 4/5 with their consumers.
|
||||||
|
|
||||||
|
- Delete (engine/content): `src/Explorer/Notebook/NotebookComponent/`,
|
||||||
`src/Explorer/Notebook/NotebookRenderer/`, `src/Explorer/Notebook/SecurityWarningBar/`,
|
`src/Explorer/Notebook/NotebookRenderer/`, `src/Explorer/Notebook/SecurityWarningBar/`,
|
||||||
`NotebookClientV2.ts`, `notebookClientV2.test.ts`, `NotebookContentClient.ts`,
|
`NotebookClientV2.ts`, `notebookClientV2.test.ts`, `NotebookContentClient.ts`,
|
||||||
`NTeractUtil.ts`, `NotebookContentItem.ts`, `NotebookUtil.ts` (+ test),
|
`NTeractUtil.ts`, `FileSystemUtil.ts`.
|
||||||
`FileSystemUtil.ts`.
|
- Delete (tabs/panes/viewer): `src/Explorer/Tabs/NotebookV2Tab.ts`, `NotebookTabBase.ts`,
|
||||||
- Delete tabs/panes/viewer: `src/Explorer/Tabs/NotebookV2Tab.ts`, `NotebookTabBase.ts`,
|
|
||||||
`src/Explorer/Panes/CopyNotebookPane/`, `src/Explorer/Controls/NotebookViewer/`,
|
`src/Explorer/Panes/CopyNotebookPane/`, `src/Explorer/Controls/NotebookViewer/`,
|
||||||
`src/CellOutputViewer/` (+ `cellOutputViewer` webpack entry & HTML plugin).
|
`src/CellOutputViewer/` (+ `cellOutputViewer` webpack entry & HTML/inline plugins).
|
||||||
|
- Delete (Schema Analyzer, pulled forward): `src/Explorer/Notebook/SchemaAnalyzer/` and
|
||||||
|
`src/Explorer/Tabs/SchemaAnalyzerTab.ts`; remove its command-bar button and any tree/menu
|
||||||
|
entry points.
|
||||||
|
- Edit to decouple survivors: `NotebookManager.tsx` (see note above), `Explorer.tsx`
|
||||||
|
(remove `openNotebook*`, import/copy/rename/createDir/read/download/upload/delete/refresh
|
||||||
|
notebook methods + `notebookToImport`; KEEP `openNotebookTerminal` /
|
||||||
|
`connectToNotebookTerminal` and the Phoenix container methods for Phase 5),
|
||||||
|
`ResourceTreeAdapter.tsx` (drop `NotebookV2Tab` import + open/copy actions; neutralize node
|
||||||
|
click handlers — full "My Notebooks" tree removal remains Phase 5), `useTabs.ts` (drop
|
||||||
|
`NotebookTabV2`), `Contracts/ViewModels.ts` (fix notebook-tab interfaces that break compile;
|
||||||
|
keep `CollectionTabKind` enum values).
|
||||||
- Remove notebook entry points: "New Notebook"/open-notebook buttons in
|
- Remove notebook entry points: "New Notebook"/open-notebook buttons in
|
||||||
`CommandBarComponentButtonFactory` (+ test), `OpenActions.tsx`,
|
`CommandBarComponentButtonFactory` (+ test), `OpenActions.tsx` (`OpenSampleNotebook`),
|
||||||
`ContextMenuButtonFactory.tsx`, splash-screen notebook cards & recent-notebook items
|
`ContextMenuButtonFactory.tsx`, splash-screen notebook cards & recent-notebook items
|
||||||
(`MostRecentActivity` OpenNotebook type), and the `openNotebook*` /
|
(`MostRecentActivity` `OpenNotebook` type).
|
||||||
`createNotebookContentItemFile` methods on `Explorer`.
|
- `tsconfig.strict.json`: remove entries for deleted files (keep `NotebookContentItem.ts`).
|
||||||
- Remove notebook deps from `package.json`: `@nteract/*`, `@jupyterlab/*`,
|
- Remove from `package.json` ONLY the notebook-only deps with **zero** remaining importers
|
||||||
`@phosphor/widgets`, `rx-jupyter` (and any now-unused transitive notebook-only libs).
|
after the deletions (e.g. `@jupyterlab/*`, `@phosphor/widgets`, and any `@nteract/*` not
|
||||||
- **Outcome:** Notebooks can no longer be authored, opened, or rendered.
|
still used). DEFER `@nteract/core`, `@nteract/commutable`, and `rx-jupyter` (still imported
|
||||||
|
by the kept `NotebookManager` / GitHub code) to Phase 4/5.
|
||||||
|
- Update/delete affected tests; regenerate Jest snapshots (`treeNodeUtil`,
|
||||||
|
`SettingsComponent`, `StringInputPane`).
|
||||||
|
- **Outcome:** Notebooks and Schema Analyzer can no longer be authored, opened, or rendered.
|
||||||
|
|
||||||
### Phase 3 — Remove Schema Analyzer
|
### Phase 3 — (folded into Phase 2)
|
||||||
- Delete `src/Explorer/Notebook/SchemaAnalyzer/` and `src/Explorer/Tabs/SchemaAnalyzerTab.ts`.
|
Schema Analyzer removal was pulled forward into Phase 2 because it is rendered by the nteract
|
||||||
- Remove Schema Analyzer command-bar button and any tree/menu entry points.
|
engine deleted there. No separate work remains for this phase.
|
||||||
|
|
||||||
### Phase 4 — Remove GitHub integration
|
### Phase 4 — Remove GitHub integration
|
||||||
- Delete `src/GitHub/`, `src/Explorer/Controls/GitHub/`,
|
- Delete `src/GitHub/`, `src/Explorer/Controls/GitHub/`,
|
||||||
@@ -160,7 +211,13 @@ and remove all UI entry points that open notebooks.
|
|||||||
### Phase 5 — Remove Phoenix and the notebook container/allocation core
|
### Phase 5 — Remove Phoenix and the notebook container/allocation core
|
||||||
- Delete `src/Phoenix/`, `src/Explorer/Notebook/NotebookContainerClient.ts`,
|
- Delete `src/Phoenix/`, `src/Explorer/Notebook/NotebookContainerClient.ts`,
|
||||||
`src/Explorer/Notebook/NotebookManager.tsx`, `src/Explorer/Notebook/useNotebook.ts`,
|
`src/Explorer/Notebook/NotebookManager.tsx`, `src/Explorer/Notebook/useNotebook.ts`,
|
||||||
`src/Utils/NotebookConfigurationUtils.ts`, `src/hooks/useNotebookSnapshotStore.ts`.
|
`src/Explorer/Notebook/NotebookContentItem.ts`, `src/Explorer/Notebook/NotebookUtil.ts`
|
||||||
|
(+ `NotebookUtil.test.ts`), `src/Utils/NotebookConfigurationUtils.ts`,
|
||||||
|
`src/hooks/useNotebookSnapshotStore.ts`. (`NotebookContentItem.ts` and `NotebookUtil.ts`
|
||||||
|
deletion was deferred from Phase 2 because the "My Notebooks" tree/store and the GitHub
|
||||||
|
client still referenced them; remove them here once those consumers are gone. If GitHub
|
||||||
|
was removed in Phase 4, `NotebookUtil`'s `getName`/`isNotebookFile` helpers were inlined
|
||||||
|
there.)
|
||||||
- Remove from `Explorer.tsx`: `phoenixClient`, `notebookManager`, `_isInitializingNotebooks`,
|
- Remove from `Explorer.tsx`: `phoenixClient`, `notebookManager`, `_isInitializingNotebooks`,
|
||||||
`initNotebooks`, `initiateAndRefreshNotebookList`, `refreshNotebookList`,
|
`initNotebooks`, `initiateAndRefreshNotebookList`, `refreshNotebookList`,
|
||||||
`allocateContainer`, container heartbeat/connection logic, and notebook-server URL
|
`allocateContainer`, container heartbeat/connection logic, and notebook-server URL
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ module.exports = {
|
|||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
"^.*[.](png|gif|less|css)$": "<rootDir>/mockModule",
|
"^.*[.](png|gif|less|css)$": "<rootDir>/mockModule",
|
||||||
"(.*)$[.](svg)": "<rootDir>/mockModule/$1",
|
"(.*)$[.](svg)": "<rootDir>/mockModule/$1",
|
||||||
"@nteract/stateful-components/(.*)$": "<rootDir>/mockModule",
|
|
||||||
"@fluentui/react/lib/(.*)$": "@fluentui/react/lib-commonjs/$1", // https://github.com/microsoft/fluentui/wiki/Version-8-release-notes
|
"@fluentui/react/lib/(.*)$": "@fluentui/react/lib-commonjs/$1", // https://github.com/microsoft/fluentui/wiki/Version-8-release-notes
|
||||||
"monaco-editor/(.*)$": "<rootDir>/__mocks__/monaco-editor",
|
"monaco-editor/(.*)$": "<rootDir>/__mocks__/monaco-editor",
|
||||||
"^dnd-core$": "dnd-core/dist/cjs",
|
"^dnd-core$": "dnd-core/dist/cjs",
|
||||||
|
|||||||
Generated
+32
-1858
File diff suppressed because it is too large
Load Diff
+4
-22
@@ -13,35 +13,14 @@
|
|||||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||||
"@fluentui/react": "8.119.0",
|
"@fluentui/react": "8.119.0",
|
||||||
"@fluentui/react-components": "9.54.2",
|
"@fluentui/react-components": "9.54.2",
|
||||||
"@jupyterlab/services": "6.0.2",
|
|
||||||
"@jupyterlab/terminal": "3.6.8",
|
|
||||||
"@microsoft/applicationinsights-web": "2.6.1",
|
"@microsoft/applicationinsights-web": "2.6.1",
|
||||||
"@nteract/commutable": "7.5.1",
|
"@nteract/commutable": "7.5.1",
|
||||||
"@nteract/connected-components": "6.8.2",
|
|
||||||
"@nteract/core": "15.1.9",
|
"@nteract/core": "15.1.9",
|
||||||
"@nteract/data-explorer": "8.2.12",
|
|
||||||
"@nteract/directory-listing": "2.0.6",
|
|
||||||
"@nteract/dropdown-menu": "1.0.1",
|
|
||||||
"@nteract/editor": "10.1.12",
|
|
||||||
"@nteract/fixtures": "2.3.0",
|
"@nteract/fixtures": "2.3.0",
|
||||||
"@nteract/iron-icons": "1.0.0",
|
|
||||||
"@nteract/jupyter-widgets": "2.0.0",
|
|
||||||
"@nteract/logos": "1.0.0",
|
|
||||||
"@nteract/markdown": "4.6.0",
|
"@nteract/markdown": "4.6.0",
|
||||||
"@nteract/monaco-editor": "3.2.2",
|
"@nteract/myths": "0.1.9",
|
||||||
"@nteract/octicons": "2.0.0",
|
|
||||||
"@nteract/outputs": "3.0.9",
|
|
||||||
"@nteract/presentational-components": "3.4.12",
|
|
||||||
"@nteract/stateful-components": "1.7.0",
|
|
||||||
"@nteract/styles": "2.0.2",
|
|
||||||
"@nteract/transform-geojson": "5.1.8",
|
|
||||||
"@nteract/transform-model-debug": "5.0.1",
|
|
||||||
"@nteract/transform-plotly": "6.1.6",
|
|
||||||
"@nteract/transform-vdom": "4.0.11",
|
|
||||||
"@nteract/transform-vega": "7.0.6",
|
|
||||||
"@octokit/request": "8.4.1",
|
"@octokit/request": "8.4.1",
|
||||||
"@octokit/rest": "17.9.2",
|
"@octokit/rest": "17.9.2",
|
||||||
"@phosphor/widgets": "1.9.3",
|
|
||||||
"@testing-library/jest-dom": "6.4.6",
|
"@testing-library/jest-dom": "6.4.6",
|
||||||
"@types/lodash": "4.14.171",
|
"@types/lodash": "4.14.171",
|
||||||
"@types/mkdirp": "1.0.1",
|
"@types/mkdirp": "1.0.1",
|
||||||
@@ -59,6 +38,7 @@
|
|||||||
"crossroads": "0.12.2",
|
"crossroads": "0.12.2",
|
||||||
"css-element-queries": "1.1.1",
|
"css-element-queries": "1.1.1",
|
||||||
"d3": "7.9.0",
|
"d3": "7.9.0",
|
||||||
|
"d3-collection": "1.0.7",
|
||||||
"datatables.net-colreorder-dt": "1.7.0",
|
"datatables.net-colreorder-dt": "1.7.0",
|
||||||
"datatables.net-dt": "1.13.8",
|
"datatables.net-dt": "1.13.8",
|
||||||
"date-fns": "1.29.0",
|
"date-fns": "1.29.0",
|
||||||
@@ -118,6 +98,8 @@
|
|||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
"web-vitals": "4.2.4",
|
"web-vitals": "4.2.4",
|
||||||
"ws": "8.20.1",
|
"ws": "8.20.1",
|
||||||
|
"xterm": "4.19.0",
|
||||||
|
"xterm-addon-fit": "0.5.0",
|
||||||
"zustand": "3.5.0"
|
"zustand": "3.5.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
diff --git a/node_modules/@phosphor/virtualdom/lib/index.d.ts b/node_modules/@phosphor/virtualdom/lib/index.d.ts
|
|
||||||
index 95682b9..73e2daa 100644
|
|
||||||
--- a/node_modules/@phosphor/virtualdom/lib/index.d.ts
|
|
||||||
+++ b/node_modules/@phosphor/virtualdom/lib/index.d.ts
|
|
||||||
@@ -58,7 +58,7 @@ export declare type ElementEventMap = {
|
|
||||||
ondrop: DragEvent;
|
|
||||||
ondurationchange: Event;
|
|
||||||
onemptied: Event;
|
|
||||||
- onended: MediaStreamErrorEvent;
|
|
||||||
+ onended: ErrorEvent;
|
|
||||||
onerror: ErrorEvent;
|
|
||||||
onfocus: FocusEvent;
|
|
||||||
oninput: Event;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
.schema-analyzer-cell-outputs {
|
|
||||||
padding: 10px 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mimic FluentUI8's DocumentCard style
|
|
||||||
.schema-analyzer-cell-output {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding: 14px 20px;
|
|
||||||
border: 1px solid rgb(237, 235, 233);
|
|
||||||
}
|
|
||||||
|
|
||||||
.schema-analyzer-cell-output:hover {
|
|
||||||
border-color: rgb(200, 198, 196);
|
|
||||||
box-shadow: inset 0 0 0 1px rgb(200, 198, 196)
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import { createImmutableOutput, JSONObject, OnDiskOutput } from "@nteract/commutable";
|
|
||||||
// import outputs individually to avoid increasing the bundle size
|
|
||||||
import { KernelOutputError } from "@nteract/outputs/lib/components/kernel-output-error";
|
|
||||||
import { Output } from "@nteract/outputs/lib/components/output";
|
|
||||||
import { StreamText } from "@nteract/outputs/lib/components/stream-text";
|
|
||||||
import { ContentRef } from "@nteract/types";
|
|
||||||
import "bootstrap/dist/css/bootstrap.css";
|
|
||||||
import postRobot from "post-robot";
|
|
||||||
import * as React from "react";
|
|
||||||
import * as ReactDOM from "react-dom";
|
|
||||||
import "../../externals/iframeResizer.contentWindow.min.js"; // Required for iFrameResizer to work
|
|
||||||
import { SnapshotRequest } from "../Explorer/Notebook/NotebookComponent/types";
|
|
||||||
import "../Explorer/Notebook/NotebookRenderer/base.css";
|
|
||||||
import "../Explorer/Notebook/NotebookRenderer/default.css";
|
|
||||||
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
|
||||||
import "./CellOutputViewer.less";
|
|
||||||
import { TransformMedia } from "./TransformMedia";
|
|
||||||
|
|
||||||
export interface SnapshotResponse {
|
|
||||||
imageSrc: string;
|
|
||||||
requestId: string;
|
|
||||||
}
|
|
||||||
export interface CellOutputViewerProps {
|
|
||||||
id: string;
|
|
||||||
contentRef: ContentRef;
|
|
||||||
outputsContainerClassName: string;
|
|
||||||
outputClassName: string;
|
|
||||||
outputs: OnDiskOutput[];
|
|
||||||
onMetadataChange: (metadata: JSONObject, mediaType: string, index?: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onInit = async () => {
|
|
||||||
postRobot.on(
|
|
||||||
"props",
|
|
||||||
{
|
|
||||||
window: window.parent,
|
|
||||||
domain: window.location.origin,
|
|
||||||
},
|
|
||||||
(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 CellOutputViewerProps;
|
|
||||||
const outputs = (
|
|
||||||
<div data-iframe-height className={props.outputsContainerClassName}>
|
|
||||||
{props.outputs?.map((output, index) => (
|
|
||||||
<div className={props.outputClassName} key={index}>
|
|
||||||
<Output output={createImmutableOutput(output)} key={index}>
|
|
||||||
<TransformMedia
|
|
||||||
output_type={"display_data"}
|
|
||||||
id={props.id}
|
|
||||||
contentRef={props.contentRef}
|
|
||||||
onMetadataChange={(metadata, mediaType) => props.onMetadataChange(metadata, mediaType, index)}
|
|
||||||
/>
|
|
||||||
<TransformMedia
|
|
||||||
output_type={"execute_result"}
|
|
||||||
id={props.id}
|
|
||||||
contentRef={props.contentRef}
|
|
||||||
onMetadataChange={(metadata, mediaType) => props.onMetadataChange(metadata, mediaType, index)}
|
|
||||||
/>
|
|
||||||
<KernelOutputError />
|
|
||||||
<StreamText />
|
|
||||||
</Output>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
ReactDOM.render(outputs, document.getElementById("cellOutput"));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
postRobot.on(
|
|
||||||
"snapshotRequest",
|
|
||||||
{
|
|
||||||
window: window.parent,
|
|
||||||
domain: window.location.origin,
|
|
||||||
},
|
|
||||||
async (event): Promise<SnapshotResponse> => {
|
|
||||||
const topNode = document.getElementById("cellOutput");
|
|
||||||
if (!topNode) {
|
|
||||||
const errorMsg = "No top node to snapshot";
|
|
||||||
return Promise.reject(new Error(errorMsg));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Typescript definition for event is wrong. So read props by casting to <any>
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const snapshotRequest = (event as any).data as SnapshotRequest;
|
|
||||||
const result = await NotebookUtil.takeScreenshotDomToImage(
|
|
||||||
topNode,
|
|
||||||
snapshotRequest.aspectRatio,
|
|
||||||
undefined,
|
|
||||||
snapshotRequest.downloadFilename,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
imageSrc: result.imageSrc,
|
|
||||||
requestId: snapshotRequest.requestId,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Entry point
|
|
||||||
window.addEventListener("load", onInit);
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import { ImmutableDisplayData, ImmutableExecuteResult, JSONObject } from "@nteract/commutable";
|
|
||||||
// import outputs individually to avoid increasing the bundle size
|
|
||||||
import { HTML } from "@nteract/outputs/lib/components/media/html";
|
|
||||||
import { Image } from "@nteract/outputs/lib/components/media/image";
|
|
||||||
import { JavaScript } from "@nteract/outputs/lib/components/media/javascript";
|
|
||||||
import { Json } from "@nteract/outputs/lib/components/media/json";
|
|
||||||
import { LaTeX } from "@nteract/outputs/lib/components/media/latex";
|
|
||||||
import { Plain } from "@nteract/outputs/lib/components/media/plain";
|
|
||||||
import { SVG } from "@nteract/outputs/lib/components/media/svg";
|
|
||||||
import { ContentRef } from "@nteract/types";
|
|
||||||
import React, { Suspense } from "react";
|
|
||||||
|
|
||||||
const EmptyTransform = (): JSX.Element => <></>;
|
|
||||||
|
|
||||||
const displayOrder = [
|
|
||||||
"application/vnd.jupyter.widget-view+json",
|
|
||||||
"application/vnd.vega.v5+json",
|
|
||||||
"application/vnd.vega.v4+json",
|
|
||||||
"application/vnd.vega.v3+json",
|
|
||||||
"application/vnd.vega.v2+json",
|
|
||||||
"application/vnd.vegalite.v4+json",
|
|
||||||
"application/vnd.vegalite.v3+json",
|
|
||||||
"application/vnd.vegalite.v2+json",
|
|
||||||
"application/vnd.vegalite.v1+json",
|
|
||||||
"application/geo+json",
|
|
||||||
"application/vnd.plotly.v1+json",
|
|
||||||
"text/vnd.plotly.v1+html",
|
|
||||||
"application/x-nteract-model-debug+json",
|
|
||||||
"application/vnd.dataresource+json",
|
|
||||||
"application/vdom.v1+json",
|
|
||||||
"application/json",
|
|
||||||
"application/javascript",
|
|
||||||
"text/html",
|
|
||||||
"text/markdown",
|
|
||||||
"text/latex",
|
|
||||||
"image/svg+xml",
|
|
||||||
"image/gif",
|
|
||||||
"image/png",
|
|
||||||
"image/jpeg",
|
|
||||||
"text/plain",
|
|
||||||
];
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const transformsById = new Map<string, React.ComponentType<any>>([
|
|
||||||
["text/vnd.plotly.v1+html", React.lazy(() => import("@nteract/transform-plotly"))],
|
|
||||||
["application/vnd.plotly.v1+json", React.lazy(() => import("@nteract/transform-plotly"))],
|
|
||||||
["application/geo+json", EmptyTransform], // TODO: The geojson transform will likely need some work because of the basemap URL(s)
|
|
||||||
["application/x-nteract-model-debug+json", React.lazy(() => import("@nteract/transform-model-debug"))],
|
|
||||||
["application/vnd.dataresource+json", React.lazy(() => import("@nteract/data-explorer"))],
|
|
||||||
["application/vnd.jupyter.widget-view+json", React.lazy(() => import("./transforms/WidgetDisplay"))],
|
|
||||||
["application/vnd.vegalite.v1+json", React.lazy(() => import("./transforms/VegaLite1"))],
|
|
||||||
["application/vnd.vegalite.v2+json", React.lazy(() => import("./transforms/VegaLite2"))],
|
|
||||||
["application/vnd.vegalite.v3+json", React.lazy(() => import("./transforms/VegaLite3"))],
|
|
||||||
["application/vnd.vegalite.v4+json", React.lazy(() => import("./transforms/VegaLite4"))],
|
|
||||||
["application/vnd.vega.v2+json", React.lazy(() => import("./transforms/Vega2"))],
|
|
||||||
["application/vnd.vega.v3+json", React.lazy(() => import("./transforms/Vega3"))],
|
|
||||||
["application/vnd.vega.v4+json", React.lazy(() => import("./transforms/Vega4"))],
|
|
||||||
["application/vnd.vega.v5+json", React.lazy(() => import("./transforms/Vega5"))],
|
|
||||||
["application/vdom.v1+json", React.lazy(() => import("@nteract/transform-vdom"))],
|
|
||||||
["application/json", Json],
|
|
||||||
["application/javascript", JavaScript],
|
|
||||||
["text/html", HTML],
|
|
||||||
["text/markdown", React.lazy(() => import("@nteract/outputs/lib/components/media/markdown"))], // Markdown increases the bundle size so lazy load it
|
|
||||||
["text/latex", LaTeX],
|
|
||||||
["image/svg+xml", SVG],
|
|
||||||
["image/gif", Image],
|
|
||||||
["image/png", Image],
|
|
||||||
["image/jpeg", Image],
|
|
||||||
["text/plain", Plain],
|
|
||||||
]);
|
|
||||||
|
|
||||||
interface TransformMediaProps {
|
|
||||||
output_type: string;
|
|
||||||
id: string;
|
|
||||||
contentRef: ContentRef;
|
|
||||||
output?: ImmutableDisplayData | ImmutableExecuteResult;
|
|
||||||
onMetadataChange: (metadata: JSONObject, mediaType: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TransformMedia = (props: TransformMediaProps): JSX.Element => {
|
|
||||||
const { Media, mediaType, data, metadata } = getMediaInfo(props);
|
|
||||||
|
|
||||||
// If we had no valid result, return an empty output
|
|
||||||
if (!mediaType || !data) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
|
||||||
<Media
|
|
||||||
onMetadataChange={props.onMetadataChange}
|
|
||||||
data={data}
|
|
||||||
metadata={metadata}
|
|
||||||
contentRef={props.contentRef}
|
|
||||||
id={props.id}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMediaInfo = (props: TransformMediaProps) => {
|
|
||||||
const { output, output_type } = props;
|
|
||||||
// This component should only be used with display data and execute result
|
|
||||||
if (!output || !(output_type === "display_data" || output_type === "execute_result")) {
|
|
||||||
console.warn("connected transform media managed to get a non media bundle output");
|
|
||||||
return {
|
|
||||||
Media: EmptyTransform,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the first mediaType in the output data that we support with a handler
|
|
||||||
const mediaType = displayOrder.find(
|
|
||||||
(key) =>
|
|
||||||
Object.prototype.hasOwnProperty.call(output.data, key) &&
|
|
||||||
(Object.prototype.hasOwnProperty.call(transformsById, key) || transformsById.get(key)),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (mediaType) {
|
|
||||||
const metadata = output.metadata.get(mediaType);
|
|
||||||
const data = output.data[mediaType];
|
|
||||||
|
|
||||||
const Media = transformsById.get(mediaType);
|
|
||||||
return {
|
|
||||||
Media,
|
|
||||||
mediaType,
|
|
||||||
data,
|
|
||||||
metadata,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
Media: EmptyTransform,
|
|
||||||
mediaType,
|
|
||||||
output,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TransformMedia;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Cell Output Viewer</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="cellOutput" id="cellOutput"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { Vega2 as default } from "@nteract/transform-vega";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { Vega3 as default } from "@nteract/transform-vega";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { Vega4 as default } from "@nteract/transform-vega";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { Vega5 as default } from "@nteract/transform-vega";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { VegaLite1 as default } from "@nteract/transform-vega";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { VegaLite2 as default } from "@nteract/transform-vega";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { VegaLite3 as default } from "@nteract/transform-vega";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { VegaLite4 as default } from "@nteract/transform-vega";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { WidgetDisplay as default } from "@nteract/jupyter-widgets";
|
|
||||||
@@ -160,7 +160,6 @@ export interface Collection extends CollectionBase {
|
|||||||
onTableEntitiesClick(): void;
|
onTableEntitiesClick(): void;
|
||||||
onGraphDocumentsClick(): void;
|
onGraphDocumentsClick(): void;
|
||||||
onMongoDBDocumentsClick(): void;
|
onMongoDBDocumentsClick(): void;
|
||||||
onSchemaAnalyzerClick(): void;
|
|
||||||
openTab(): void;
|
openTab(): void;
|
||||||
|
|
||||||
onSettingsClick: () => Promise<void>;
|
onSettingsClick: () => Promise<void>;
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
.notebookViewerMetadataContainer {
|
|
||||||
margin: 0px 10px;
|
|
||||||
|
|
||||||
.title, .decoration, .persona {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.extras {
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { shallow } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
import { NotebookMetadataComponent, NotebookMetadataComponentProps } from "./NotebookMetadataComponent";
|
|
||||||
|
|
||||||
describe("NotebookMetadataComponent", () => {
|
|
||||||
it("renders un-liked notebook", () => {
|
|
||||||
const props: NotebookMetadataComponentProps = {
|
|
||||||
data: {
|
|
||||||
id: "id",
|
|
||||||
name: "name",
|
|
||||||
description: "description",
|
|
||||||
author: "author",
|
|
||||||
thumbnailUrl: "thumbnailUrl",
|
|
||||||
created: "created",
|
|
||||||
gitSha: "gitSha",
|
|
||||||
tags: ["tag"],
|
|
||||||
isSample: false,
|
|
||||||
downloads: 0,
|
|
||||||
favorites: 0,
|
|
||||||
views: 0,
|
|
||||||
newCellId: undefined,
|
|
||||||
policyViolations: undefined,
|
|
||||||
pendingScanJobIds: undefined,
|
|
||||||
},
|
|
||||||
isFavorite: false,
|
|
||||||
downloadButtonText: "Download",
|
|
||||||
onTagClick: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders liked notebook", () => {
|
|
||||||
const props: NotebookMetadataComponentProps = {
|
|
||||||
data: {
|
|
||||||
id: "id",
|
|
||||||
name: "name",
|
|
||||||
description: "description",
|
|
||||||
author: "author",
|
|
||||||
thumbnailUrl: "thumbnailUrl",
|
|
||||||
created: "created",
|
|
||||||
gitSha: "gitSha",
|
|
||||||
tags: ["tag"],
|
|
||||||
isSample: false,
|
|
||||||
downloads: 0,
|
|
||||||
favorites: 0,
|
|
||||||
views: 0,
|
|
||||||
newCellId: undefined,
|
|
||||||
policyViolations: undefined,
|
|
||||||
pendingScanJobIds: undefined,
|
|
||||||
},
|
|
||||||
isFavorite: true,
|
|
||||||
downloadButtonText: "Download",
|
|
||||||
onTagClick: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO Add test for metadata display
|
|
||||||
});
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
/**
|
|
||||||
* Wrapper around Notebook metadata
|
|
||||||
*/
|
|
||||||
import { FontWeights, Icon, Link, Persona, PersonaSize, Stack, Text } from "@fluentui/react";
|
|
||||||
import * as React from "react";
|
|
||||||
import { IGalleryItem } from "../../../Juno/JunoClient";
|
|
||||||
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
|
|
||||||
import "./NotebookViewerComponent.less";
|
|
||||||
import CosmosDBLogo from "../../../../images/CosmosDB-logo.svg";
|
|
||||||
|
|
||||||
export interface NotebookMetadataComponentProps {
|
|
||||||
data: IGalleryItem;
|
|
||||||
isFavorite: boolean;
|
|
||||||
downloadButtonText?: string;
|
|
||||||
onTagClick: (tag: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> {
|
|
||||||
public render(): JSX.Element {
|
|
||||||
const options: Intl.DateTimeFormatOptions = {
|
|
||||||
year: "numeric",
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
};
|
|
||||||
|
|
||||||
const dateString = new Date(this.props.data.created).toLocaleString("default", options);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack tokens={{ childrenGap: 10 }}>
|
|
||||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 30 }}>
|
|
||||||
<Stack.Item>
|
|
||||||
<Text variant="xxLarge" nowrap>
|
|
||||||
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
|
|
||||||
</Text>
|
|
||||||
</Stack.Item>
|
|
||||||
|
|
||||||
<Stack.Item>
|
|
||||||
<Text>
|
|
||||||
<Icon iconName="Heart" /> {this.props.data.favorites} likes
|
|
||||||
</Text>
|
|
||||||
</Stack.Item>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 10 }}>
|
|
||||||
<Persona
|
|
||||||
imageUrl={this.props.data.isSample && CosmosDBLogo}
|
|
||||||
text={this.props.data.author}
|
|
||||||
size={PersonaSize.size32}
|
|
||||||
/>
|
|
||||||
<Text>{dateString}</Text>
|
|
||||||
<Text>
|
|
||||||
<Icon iconName="RedEye" /> {this.props.data.views}
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
<Icon iconName="Download" />
|
|
||||||
{this.props.data.downloads}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Text nowrap>
|
|
||||||
{this.props.data.tags?.map((tag, index, array) => (
|
|
||||||
<span key={tag}>
|
|
||||||
<Link onClick={(): void => this.props.onTagClick(tag)}>{tag}</Link>
|
|
||||||
{index === array.length - 1 ? <></> : ", "}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text variant="large" styles={{ root: { fontWeight: FontWeights.semibold } }}>
|
|
||||||
Description
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text>{this.props.data.description}</Text>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
@import "../../../../less/Common/Constants";
|
|
||||||
|
|
||||||
.notebookViewerContainer {
|
|
||||||
padding: 30px;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
/**
|
|
||||||
* Wrapper around Notebook Viewer Read only content
|
|
||||||
*/
|
|
||||||
import { Icon, Link, ProgressIndicator } from "@fluentui/react";
|
|
||||||
import { Notebook } from "@nteract/commutable";
|
|
||||||
import { createContentRef } from "@nteract/core";
|
|
||||||
import * as React from "react";
|
|
||||||
import { contents } from "rx-jupyter";
|
|
||||||
import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
|
|
||||||
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
|
|
||||||
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
|
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import Explorer from "../../Explorer";
|
|
||||||
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
|
|
||||||
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
|
||||||
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
|
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
|
||||||
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
|
||||||
import "./NotebookViewerComponent.less";
|
|
||||||
|
|
||||||
export interface NotebookViewerComponentProps {
|
|
||||||
container?: Explorer;
|
|
||||||
junoClient?: JunoClient;
|
|
||||||
notebookUrl: string;
|
|
||||||
galleryItem?: IGalleryItem;
|
|
||||||
isFavorite?: boolean;
|
|
||||||
backNavigationText: string;
|
|
||||||
hideInputs?: boolean;
|
|
||||||
hidePrompts?: boolean;
|
|
||||||
onBackClick: () => void;
|
|
||||||
onTagClick: (tag: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NotebookViewerComponentState {
|
|
||||||
content: Notebook;
|
|
||||||
galleryItem?: IGalleryItem;
|
|
||||||
isFavorite?: boolean;
|
|
||||||
showProgressBar: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NotebookViewerComponent extends React.Component<
|
|
||||||
NotebookViewerComponentProps,
|
|
||||||
NotebookViewerComponentState
|
|
||||||
> {
|
|
||||||
private clientManager: NotebookClientV2;
|
|
||||||
private notebookComponentBootstrapper: NotebookComponentBootstrapper;
|
|
||||||
|
|
||||||
constructor(props: NotebookViewerComponentProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.clientManager = new NotebookClientV2({
|
|
||||||
connectionInfo: { authToken: undefined, notebookServerEndpoint: undefined, forwardingId: undefined },
|
|
||||||
databaseAccountName: undefined,
|
|
||||||
defaultExperience: "NotebookViewer",
|
|
||||||
isReadOnly: true,
|
|
||||||
cellEditorType: "codemirror",
|
|
||||||
autoSaveInterval: 365 * 24 * 3600 * 1000, // There is no way to turn off auto-save, set to 1 year
|
|
||||||
contentProvider: contents.JupyterContentProvider, // NotebookViewer only knows how to talk to Jupyter contents API
|
|
||||||
});
|
|
||||||
|
|
||||||
this.notebookComponentBootstrapper = new NotebookComponentBootstrapper({
|
|
||||||
notebookClient: this.clientManager,
|
|
||||||
contentRef: createContentRef(),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
content: undefined,
|
|
||||||
galleryItem: props.galleryItem,
|
|
||||||
isFavorite: props.isFavorite,
|
|
||||||
showProgressBar: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.loadNotebookContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadNotebookContent(): Promise<void> {
|
|
||||||
const startKey = traceStart(Action.NotebooksGalleryViewNotebook, {
|
|
||||||
notebookUrl: this.props.notebookUrl,
|
|
||||||
notebookId: this.props.galleryItem?.id,
|
|
||||||
isSample: this.props.galleryItem?.isSample,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(this.props.notebookUrl);
|
|
||||||
if (!response.ok) {
|
|
||||||
this.setState({ showProgressBar: false });
|
|
||||||
throw new Error(`Received HTTP ${response.status} while fetching ${this.props.notebookUrl}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
traceSuccess(
|
|
||||||
Action.NotebooksGalleryViewNotebook,
|
|
||||||
{
|
|
||||||
notebookUrl: this.props.notebookUrl,
|
|
||||||
notebookId: this.props.galleryItem?.id,
|
|
||||||
isSample: this.props.galleryItem?.isSample,
|
|
||||||
},
|
|
||||||
startKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
const notebook: Notebook = await response.json();
|
|
||||||
this.notebookComponentBootstrapper.setContent("json", notebook);
|
|
||||||
this.setState({ content: notebook, showProgressBar: false });
|
|
||||||
|
|
||||||
if (this.props.galleryItem && !SessionStorageUtility.getEntry(this.props.galleryItem.id)) {
|
|
||||||
const response = await this.props.junoClient.increaseNotebookViews(this.props.galleryItem.id);
|
|
||||||
if (!response.data) {
|
|
||||||
throw new Error(`Received HTTP ${response.status} while increasing notebook views`);
|
|
||||||
}
|
|
||||||
this.setState({ galleryItem: response.data });
|
|
||||||
SessionStorageUtility.setEntry(this.props.galleryItem?.id, "true");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
traceFailure(
|
|
||||||
Action.NotebooksGalleryViewNotebook,
|
|
||||||
{
|
|
||||||
notebookUrl: this.props.notebookUrl,
|
|
||||||
notebookId: this.props.galleryItem?.id,
|
|
||||||
isSample: this.props.galleryItem?.isSample,
|
|
||||||
error: getErrorMessage(error),
|
|
||||||
errorStack: getErrorStack(error),
|
|
||||||
},
|
|
||||||
startKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.setState({ showProgressBar: false });
|
|
||||||
handleError(error, "NotebookViewerComponent/loadNotebookContent", "Failed to load notebook content");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<div className="notebookViewerContainer">
|
|
||||||
{this.props.backNavigationText !== undefined ? (
|
|
||||||
<Link onClick={this.props.onBackClick}>
|
|
||||||
<Icon iconName="Back" /> {this.props.backNavigationText}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{this.state.galleryItem ? (
|
|
||||||
<div style={{ margin: 10 }}>
|
|
||||||
<NotebookMetadataComponent
|
|
||||||
data={this.state.galleryItem}
|
|
||||||
isFavorite={this.state.isFavorite}
|
|
||||||
downloadButtonText={this.props.container && `Download to ${useNotebook.getState().notebookFolderName}`}
|
|
||||||
onTagClick={this.props.onTagClick}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{this.state.showProgressBar && <ProgressIndicator />}
|
|
||||||
|
|
||||||
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
|
|
||||||
hideInputs: this.props.hideInputs,
|
|
||||||
hidePrompts: this.props.hidePrompts,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getDerivedStateFromProps(
|
|
||||||
props: NotebookViewerComponentProps,
|
|
||||||
state: NotebookViewerComponentState,
|
|
||||||
): Partial<NotebookViewerComponentState> {
|
|
||||||
let galleryItem = props.galleryItem;
|
|
||||||
let isFavorite = props.isFavorite;
|
|
||||||
|
|
||||||
if (state.galleryItem !== undefined) {
|
|
||||||
galleryItem = state.galleryItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.isFavorite !== undefined) {
|
|
||||||
isFavorite = state.isFavorite;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
galleryItem,
|
|
||||||
isFavorite,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-197
@@ -1,197 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
|
||||||
<Stack
|
|
||||||
tokens={
|
|
||||||
{
|
|
||||||
"childrenGap": 10,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
tokens={
|
|
||||||
{
|
|
||||||
"childrenGap": 30,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
verticalAlign="center"
|
|
||||||
>
|
|
||||||
<StackItem>
|
|
||||||
<Text
|
|
||||||
nowrap={true}
|
|
||||||
variant="xxLarge"
|
|
||||||
>
|
|
||||||
name
|
|
||||||
</Text>
|
|
||||||
</StackItem>
|
|
||||||
<StackItem>
|
|
||||||
<Text>
|
|
||||||
<Icon
|
|
||||||
iconName="Heart"
|
|
||||||
/>
|
|
||||||
|
|
||||||
0
|
|
||||||
likes
|
|
||||||
</Text>
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
tokens={
|
|
||||||
{
|
|
||||||
"childrenGap": 10,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
verticalAlign="center"
|
|
||||||
>
|
|
||||||
<StyledPersonaBase
|
|
||||||
imageUrl={false}
|
|
||||||
size={11}
|
|
||||||
text="author"
|
|
||||||
/>
|
|
||||||
<Text>
|
|
||||||
Invalid Date
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
<Icon
|
|
||||||
iconName="RedEye"
|
|
||||||
/>
|
|
||||||
|
|
||||||
0
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
<Icon
|
|
||||||
iconName="Download"
|
|
||||||
/>
|
|
||||||
0
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Text
|
|
||||||
nowrap={true}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
key="tag"
|
|
||||||
>
|
|
||||||
<StyledLinkBase
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
tag
|
|
||||||
</StyledLinkBase>
|
|
||||||
</span>
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
styles={
|
|
||||||
{
|
|
||||||
"root": {
|
|
||||||
"fontWeight": 600,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
variant="large"
|
|
||||||
>
|
|
||||||
Description
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
description
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
|
|
||||||
<Stack
|
|
||||||
tokens={
|
|
||||||
{
|
|
||||||
"childrenGap": 10,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
tokens={
|
|
||||||
{
|
|
||||||
"childrenGap": 30,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
verticalAlign="center"
|
|
||||||
>
|
|
||||||
<StackItem>
|
|
||||||
<Text
|
|
||||||
nowrap={true}
|
|
||||||
variant="xxLarge"
|
|
||||||
>
|
|
||||||
name
|
|
||||||
</Text>
|
|
||||||
</StackItem>
|
|
||||||
<StackItem>
|
|
||||||
<Text>
|
|
||||||
<Icon
|
|
||||||
iconName="Heart"
|
|
||||||
/>
|
|
||||||
|
|
||||||
0
|
|
||||||
likes
|
|
||||||
</Text>
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
tokens={
|
|
||||||
{
|
|
||||||
"childrenGap": 10,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
verticalAlign="center"
|
|
||||||
>
|
|
||||||
<StyledPersonaBase
|
|
||||||
imageUrl={false}
|
|
||||||
size={11}
|
|
||||||
text="author"
|
|
||||||
/>
|
|
||||||
<Text>
|
|
||||||
Invalid Date
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
<Icon
|
|
||||||
iconName="RedEye"
|
|
||||||
/>
|
|
||||||
|
|
||||||
0
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
<Icon
|
|
||||||
iconName="Download"
|
|
||||||
/>
|
|
||||||
0
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
<Text
|
|
||||||
nowrap={true}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
key="tag"
|
|
||||||
>
|
|
||||||
<StyledLinkBase
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
tag
|
|
||||||
</StyledLinkBase>
|
|
||||||
</span>
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
styles={
|
|
||||||
{
|
|
||||||
"root": {
|
|
||||||
"fontWeight": 600,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
variant="large"
|
|
||||||
>
|
|
||||||
Description
|
|
||||||
</Text>
|
|
||||||
<Text>
|
|
||||||
description
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
`;
|
|
||||||
@@ -130,7 +130,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"refreshNotebookList": [Function],
|
"refreshNotebookList": [Function],
|
||||||
"resourceTree": ResourceTreeAdapter {
|
"resourceTree": ResourceTreeAdapter {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -252,7 +251,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"refreshNotebookList": [Function],
|
"refreshNotebookList": [Function],
|
||||||
"resourceTree": ResourceTreeAdapter {
|
"resourceTree": ResourceTreeAdapter {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -479,7 +477,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"refreshNotebookList": [Function],
|
"refreshNotebookList": [Function],
|
||||||
"resourceTree": ResourceTreeAdapter {
|
"resourceTree": ResourceTreeAdapter {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -551,7 +548,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"refreshNotebookList": [Function],
|
"refreshNotebookList": [Function],
|
||||||
"resourceTree": ResourceTreeAdapter {
|
"resourceTree": ResourceTreeAdapter {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -720,7 +716,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"refreshNotebookList": [Function],
|
"refreshNotebookList": [Function],
|
||||||
"resourceTree": ResourceTreeAdapter {
|
"resourceTree": ResourceTreeAdapter {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -792,7 +787,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"refreshNotebookList": [Function],
|
"refreshNotebookList": [Function],
|
||||||
"resourceTree": ResourceTreeAdapter {
|
"resourceTree": ResourceTreeAdapter {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-344
@@ -24,7 +24,7 @@ import { AuthType } from "../AuthType";
|
|||||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
|
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
|
||||||
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../Common/ErrorHandlingUtils";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { QueriesClient } from "../Common/QueriesClient";
|
import { QueriesClient } from "../Common/QueriesClient";
|
||||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||||
@@ -43,28 +43,20 @@ import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"
|
|||||||
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { updateUserContext, userContext } from "../UserContext";
|
import { updateUserContext, userContext } from "../UserContext";
|
||||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||||
import { stringToBlob } from "../Utils/BlobUtils";
|
|
||||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||||
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
|
||||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
|
||||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
import { ReactTabKind, useTabs } from "../hooks/useTabs";
|
import { ReactTabKind, useTabs } from "../hooks/useTabs";
|
||||||
import "./ComponentRegisterer";
|
import "./ComponentRegisterer";
|
||||||
import { DialogProps, useDialog } from "./Controls/Dialog";
|
import { DialogProps, useDialog } from "./Controls/Dialog";
|
||||||
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
|
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
|
||||||
import type NotebookManager from "./Notebook/NotebookManager";
|
import type NotebookManager from "./Notebook/NotebookManager";
|
||||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
|
||||||
import { useNotebook } from "./Notebook/useNotebook";
|
import { useNotebook } from "./Notebook/useNotebook";
|
||||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel";
|
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel";
|
||||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
|
||||||
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
||||||
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
||||||
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
|
|
||||||
import TabsBase from "./Tabs/TabsBase";
|
import TabsBase from "./Tabs/TabsBase";
|
||||||
import TerminalTab from "./Tabs/TerminalTab";
|
import TerminalTab from "./Tabs/TerminalTab";
|
||||||
import Database from "./Tree/Database";
|
import Database from "./Tree/Database";
|
||||||
@@ -93,10 +85,6 @@ export default class Explorer {
|
|||||||
public notebookManager?: NotebookManager;
|
public notebookManager?: NotebookManager;
|
||||||
|
|
||||||
private _isInitializingNotebooks: boolean;
|
private _isInitializingNotebooks: boolean;
|
||||||
private notebookToImport: {
|
|
||||||
name: string;
|
|
||||||
content: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
||||||
public phoenixClient: PhoenixClient;
|
public phoenixClient: PhoenixClient;
|
||||||
@@ -652,313 +640,11 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public uploadFile(
|
private refreshNotebookList = (): Promise<void> => {
|
||||||
name: string,
|
// Notebook authoring and listing have been removed.
|
||||||
content: string,
|
return Promise.resolve();
|
||||||
parent: NotebookContentItem,
|
|
||||||
isGithubTree?: boolean,
|
|
||||||
): Promise<NotebookContentItem> {
|
|
||||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
|
||||||
const error = "Attempt to upload notebook, but notebook is not enabled";
|
|
||||||
handleError(error, "Explorer/uploadFile");
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent, isGithubTree);
|
|
||||||
promise
|
|
||||||
.then(() => this.resourceTree.triggerRender())
|
|
||||||
.catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", getErrorMessage(reason)));
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async importAndOpen(path: string): Promise<boolean> {
|
|
||||||
const name = NotebookUtil.getName(path);
|
|
||||||
const item = NotebookUtil.createNotebookContentItem(name, path, "file");
|
|
||||||
const parent = this.resourceTree.myNotebooksContentRoot;
|
|
||||||
|
|
||||||
if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) {
|
|
||||||
const existingItem = _.find(parent.children, (node) => node.name === name);
|
|
||||||
if (existingItem) {
|
|
||||||
return this.openNotebook(existingItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await this.readFile(item);
|
|
||||||
const uploadedItem = await this.uploadFile(name, content, parent);
|
|
||||||
return this.openNotebook(uploadedItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async importAndOpenContent(name: string, content: string): Promise<boolean> {
|
|
||||||
const parent = this.resourceTree.myNotebooksContentRoot;
|
|
||||||
|
|
||||||
if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) {
|
|
||||||
if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) {
|
|
||||||
this.notebookToImport = undefined; // we don't want to try opening this notebook again
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingItem = _.find(parent.children, (node) => node.name === name);
|
|
||||||
if (existingItem) {
|
|
||||||
return this.openNotebook(existingItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadedItem = await this.uploadFile(name, content, parent);
|
|
||||||
return this.openNotebook(uploadedItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notebookToImport = { name, content }; // we'll try opening this notebook later on
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public copyNotebook(name: string, content: string): void {
|
|
||||||
this.notebookManager?.openCopyNotebookPane(name, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree.
|
|
||||||
* Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder.
|
|
||||||
* Manually creating the missing folders between the root and its parent dir would break the UX: expanding a folder
|
|
||||||
* will not fetch its content if the children array exists (and has only one child which was manually created).
|
|
||||||
* Fetching the intermediate folders possibly involves a few chained async calls which isn't ideal.
|
|
||||||
*
|
|
||||||
* @param name
|
|
||||||
* @param path
|
|
||||||
*/
|
|
||||||
public createNotebookContentItemFile(name: string, path: string): NotebookContentItem {
|
|
||||||
return NotebookUtil.createNotebookContentItem(name, path, "file");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async openNotebook(notebookContentItem: NotebookContentItem): Promise<boolean> {
|
|
||||||
if (!notebookContentItem || !notebookContentItem.path) {
|
|
||||||
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
|
|
||||||
}
|
|
||||||
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) {
|
|
||||||
await this.allocateContainer();
|
|
||||||
}
|
|
||||||
|
|
||||||
const notebookTabs = useTabs
|
|
||||||
.getState()
|
|
||||||
.getTabs(
|
|
||||||
ViewModels.CollectionTabKind.NotebookV2,
|
|
||||||
(tab) =>
|
|
||||||
(tab as NotebookV2Tab).notebookPath &&
|
|
||||||
FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path),
|
|
||||||
) as NotebookV2Tab[];
|
|
||||||
let notebookTab = notebookTabs && notebookTabs[0];
|
|
||||||
|
|
||||||
if (notebookTab) {
|
|
||||||
useTabs.getState().activateTab(notebookTab);
|
|
||||||
} else {
|
|
||||||
const options: NotebookTabOptions = {
|
|
||||||
account: userContext.databaseAccount,
|
|
||||||
tabKind: ViewModels.CollectionTabKind.NotebookV2,
|
|
||||||
node: undefined,
|
|
||||||
title: notebookContentItem.name,
|
|
||||||
tabPath: notebookContentItem.path,
|
|
||||||
collection: undefined,
|
|
||||||
masterKey: userContext.masterKey || "",
|
|
||||||
isTabsContentExpanded: ko.observable(true),
|
|
||||||
onLoadStartKey: undefined,
|
|
||||||
container: this,
|
|
||||||
notebookContentItem,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab");
|
|
||||||
notebookTab = new NotebookTabV2.default(options);
|
|
||||||
useTabs.getState().activateNewTab(notebookTab);
|
|
||||||
} catch (reason) {
|
|
||||||
console.error("Import NotebookV2Tab failed!", reason);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public renameNotebook(notebookFile: NotebookContentItem, isGithubTree?: boolean): void {
|
|
||||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
|
||||||
const error = "Attempt to rename notebook, but notebook is not enabled";
|
|
||||||
handleError(error, "Explorer/renameNotebook");
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't delete if tab is open to avoid accidental deletion
|
|
||||||
const openedNotebookTabs = useTabs
|
|
||||||
.getState()
|
|
||||||
.getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => {
|
|
||||||
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path);
|
|
||||||
});
|
|
||||||
if (openedNotebookTabs.length > 0) {
|
|
||||||
useDialog
|
|
||||||
.getState()
|
|
||||||
.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
|
|
||||||
} else {
|
|
||||||
useSidePanel.getState().openSidePanel(
|
|
||||||
"Rename Notebook",
|
|
||||||
<StringInputPane
|
|
||||||
closePanel={() => {
|
|
||||||
useSidePanel.getState().closeSidePanel();
|
|
||||||
this.resourceTree.triggerRender();
|
|
||||||
}}
|
|
||||||
inputLabel="Enter new notebook name"
|
|
||||||
submitButtonLabel="Rename"
|
|
||||||
errorMessage="Could not rename notebook"
|
|
||||||
inProgressMessage="Renaming notebook to"
|
|
||||||
successMessage="Renamed notebook to"
|
|
||||||
paneTitle="Rename Notebook"
|
|
||||||
defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")}
|
|
||||||
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
|
|
||||||
this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input, isGithubTree)
|
|
||||||
}
|
|
||||||
notebookFile={notebookFile}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public onCreateDirectory(parent: NotebookContentItem, isGithubTree?: boolean): void {
|
|
||||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
|
||||||
const error = "Attempt to create notebook directory, but notebook is not enabled";
|
|
||||||
handleError(error, "Explorer/onCreateDirectory");
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
useSidePanel.getState().openSidePanel(
|
|
||||||
"Create new directory",
|
|
||||||
<StringInputPane
|
|
||||||
closePanel={() => {
|
|
||||||
useSidePanel.getState().closeSidePanel();
|
|
||||||
this.resourceTree.triggerRender();
|
|
||||||
}}
|
|
||||||
errorMessage="Could not create directory "
|
|
||||||
inProgressMessage="Creating directory "
|
|
||||||
successMessage="Created directory "
|
|
||||||
inputLabel="Enter new directory name"
|
|
||||||
paneTitle="Create new directory"
|
|
||||||
submitButtonLabel="Create"
|
|
||||||
defaultInput=""
|
|
||||||
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
|
|
||||||
this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree)
|
|
||||||
}
|
|
||||||
notebookFile={parent}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public readFile(notebookFile: NotebookContentItem): Promise<string> {
|
|
||||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
|
||||||
const error = "Attempt to read file, but notebook is not enabled";
|
|
||||||
handleError(error, "Explorer/downloadFile");
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public downloadFile(notebookFile: NotebookContentItem): Promise<void> {
|
|
||||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
|
||||||
const error = "Attempt to download file, but notebook is not enabled";
|
|
||||||
handleError(error, "Explorer/downloadFile");
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Downloading ${notebookFile.path}`);
|
|
||||||
|
|
||||||
return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then(
|
|
||||||
(content: string) => {
|
|
||||||
const blob = stringToBlob(content, "text/plain");
|
|
||||||
if (navigator.msSaveBlob) {
|
|
||||||
// for IE and Edge
|
|
||||||
navigator.msSaveBlob(blob, notebookFile.name);
|
|
||||||
} else {
|
|
||||||
const downloadLink: HTMLAnchorElement = document.createElement("a");
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
downloadLink.href = url;
|
|
||||||
downloadLink.target = "_self";
|
|
||||||
downloadLink.download = notebookFile.name;
|
|
||||||
|
|
||||||
// for some reason, FF displays the download prompt only when
|
|
||||||
// the link is added to the dom so we add and remove it
|
|
||||||
document.body.appendChild(downloadLink);
|
|
||||||
downloadLink.click();
|
|
||||||
downloadLink.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
clearMessage();
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
logConsoleError(`Could not download notebook ${getErrorMessage(error)}`);
|
|
||||||
clearMessage();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private refreshNotebookList = async (): Promise<void> => {
|
|
||||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.resourceTree.initialize();
|
|
||||||
await useNotebook.getState().initializeNotebooksTree(this.notebookManager);
|
|
||||||
|
|
||||||
this.notebookManager?.refreshPinnedRepos();
|
|
||||||
if (this.notebookToImport) {
|
|
||||||
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public deleteNotebookFile(item: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
|
|
||||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
|
||||||
const error = "Attempt to delete notebook file, but notebook is not enabled";
|
|
||||||
handleError(error, "Explorer/deleteNotebookFile");
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't delete if tab is open to avoid accidental deletion
|
|
||||||
const openedNotebookTabs = useTabs
|
|
||||||
.getState()
|
|
||||||
.getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => {
|
|
||||||
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path);
|
|
||||||
});
|
|
||||||
if (openedNotebookTabs.length > 0) {
|
|
||||||
useDialog
|
|
||||||
.getState()
|
|
||||||
.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again.");
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.type === NotebookContentItemType.Directory && item.children && item.children.length > 0) {
|
|
||||||
useDialog.getState().openDialog({
|
|
||||||
isModal: true,
|
|
||||||
title: "Unable to delete file",
|
|
||||||
subText: "Directory is not empty.",
|
|
||||||
primaryButtonText: "Close",
|
|
||||||
secondaryButtonText: undefined,
|
|
||||||
onPrimaryButtonClick: () => useDialog.getState().closeDialog(),
|
|
||||||
onSecondaryButtonClick: undefined,
|
|
||||||
});
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.notebookManager?.notebookContentClient.deleteContentItem(item, isGithubTree).then(
|
|
||||||
() => logConsoleInfo(`Successfully deleted: ${item.path}`),
|
|
||||||
(reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Delete this function when ResourceTreeAdapter is removed.
|
|
||||||
public async refreshContentItem(item: NotebookContentItem): Promise<void> {
|
|
||||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
|
||||||
const error = "Attempt to refresh notebook list, but notebook is not enabled";
|
|
||||||
handleError(error, "Explorer/refreshContentItem");
|
|
||||||
return Promise.reject(new Error(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
||||||
this.connectToNotebookTerminal(kind);
|
this.connectToNotebookTerminal(kind);
|
||||||
}
|
}
|
||||||
@@ -1046,32 +732,6 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleOpenFileAction(path: string): Promise<void> {
|
|
||||||
if (useNotebook.getState().isPhoenixNotebooks === undefined) {
|
|
||||||
await useNotebook.getState().getPhoenixStatus();
|
|
||||||
}
|
|
||||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
|
||||||
await this.allocateContainer();
|
|
||||||
}
|
|
||||||
|
|
||||||
// We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb
|
|
||||||
// when launching a notebook quickstart from Portal. In future we should just use gallery id and use Juno to fetch instead of directly
|
|
||||||
// calling GitHub. For now convert this url to a raw url and download content.
|
|
||||||
const gitHubInfo = fromContentUri(path);
|
|
||||||
if (gitHubInfo) {
|
|
||||||
const rawUrl = toRawContentUri(gitHubInfo.owner, gitHubInfo.repo, gitHubInfo.branch, gitHubInfo.path);
|
|
||||||
const response = await fetch(rawUrl);
|
|
||||||
if (response.status === Constants.HttpStatusCodes.OK) {
|
|
||||||
this.notebookToImport = {
|
|
||||||
name: NotebookUtil.getName(path),
|
|
||||||
content: await response.text(),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public openUploadItemsPane(onUpload?: (data: UploadDetailsRecord[]) => void): void {
|
public openUploadItemsPane(onUpload?: (data: UploadDetailsRecord[]) => void): void {
|
||||||
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane onUpload={onUpload} />);
|
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane onUpload={onUpload} />);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
|||||||
|
|
||||||
export enum Type {
|
export enum Type {
|
||||||
OpenCollection = "OpenCollection",
|
OpenCollection = "OpenCollection",
|
||||||
OpenNotebook = "OpenNotebook",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OpenNotebookItem {
|
|
||||||
type: Type.OpenNotebook;
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenCollectionItem {
|
export interface OpenCollectionItem {
|
||||||
@@ -19,7 +12,7 @@ export interface OpenCollectionItem {
|
|||||||
collectionId: string;
|
collectionId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Item = OpenNotebookItem | OpenCollectionItem;
|
type Item = OpenCollectionItem;
|
||||||
|
|
||||||
const itemsMaxNumber: number = 5;
|
const itemsMaxNumber: number = 5;
|
||||||
|
|
||||||
@@ -42,14 +35,14 @@ const migrateOldData = () => {
|
|||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
componentName: AppStateComponentNames.MostRecentActivity,
|
||||||
globalAccountName: accountName,
|
globalAccountName: accountName,
|
||||||
},
|
},
|
||||||
itemsMap[accountId].map((item) => {
|
itemsMap[accountId]
|
||||||
if ((item.type as unknown as number) === 0) {
|
.filter((item) => (item.type as unknown as number) !== 1 && (item.type as string) !== "OpenNotebook")
|
||||||
item.type = Type.OpenCollection;
|
.map((item) => {
|
||||||
} else if ((item.type as unknown as number) === 1) {
|
if ((item.type as unknown as number) === 0) {
|
||||||
item.type = Type.OpenNotebook;
|
item.type = Type.OpenCollection;
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -97,7 +90,7 @@ export const getItems = (accountName: string): Item[] => {
|
|||||||
componentName: AppStateComponentNames.MostRecentActivity,
|
componentName: AppStateComponentNames.MostRecentActivity,
|
||||||
globalAccountName: accountName,
|
globalAccountName: accountName,
|
||||||
}) as Item[]) || []
|
}) as Item[]) || []
|
||||||
);
|
).filter((item) => item.type in Type);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const collectionWasOpened = (
|
export const collectionWasOpened = (
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
/**
|
|
||||||
* file list returns path starting with ./blah
|
|
||||||
* rename returns simply blah.
|
|
||||||
* Both are the same. This method only handles these two cases and no other complicated paths that may contain ..
|
|
||||||
* ./ inside the path.
|
|
||||||
* TODO: this should go away when not using jupyter for file operations and use normalized paths.
|
|
||||||
* @param path1
|
|
||||||
* @param path2
|
|
||||||
*/
|
|
||||||
export function isPathEqual(path1: string, path2: string): boolean {
|
|
||||||
const normalize = (path: string): string => {
|
|
||||||
const dotSlash = "./";
|
|
||||||
if (path.indexOf(dotSlash) === 0) {
|
|
||||||
path = path.substring(dotSlash.length);
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
};
|
|
||||||
|
|
||||||
return normalize(path1) === normalize(path2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove extension
|
|
||||||
* @param path
|
|
||||||
* @param extension Without the ".". e.g. "ipynb" (and not ".ipynb")
|
|
||||||
*/
|
|
||||||
export function stripExtension(path: string, extension: string): string {
|
|
||||||
const splitted = path.split(".");
|
|
||||||
if (splitted[splitted.length - 1] === extension) {
|
|
||||||
splitted.pop();
|
|
||||||
}
|
|
||||||
return splitted.join(".");
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { NotebookContentRecordProps, selectors } from "@nteract/core";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A bunch of utilities to interact with nteract
|
|
||||||
*/
|
|
||||||
export function getCurrentCellType(content: NotebookContentRecordProps): "markdown" | "code" | "raw" | undefined {
|
|
||||||
if (!content) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cellFocusedId = selectors.notebook.cellFocused(content.model);
|
|
||||||
if (cellFocusedId) {
|
|
||||||
const cell = selectors.notebook.cellById(content.model, { id: cellFocusedId });
|
|
||||||
if (cell) {
|
|
||||||
return cell.cell_type;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
// Manages all the redux logic for the notebook nteract code
|
|
||||||
// TODO: Merge with NotebookClient?
|
|
||||||
// Vendor modules
|
|
||||||
import {
|
|
||||||
actions,
|
|
||||||
AppState,
|
|
||||||
ContentRecord,
|
|
||||||
createHostRef,
|
|
||||||
createKernelspecsRef,
|
|
||||||
HostRecord,
|
|
||||||
HostRef,
|
|
||||||
IContentProvider,
|
|
||||||
KernelspecsRef,
|
|
||||||
makeAppRecord,
|
|
||||||
makeCommsRecord,
|
|
||||||
makeContentsRecord,
|
|
||||||
makeEditorsRecord,
|
|
||||||
makeEntitiesRecord,
|
|
||||||
makeHostsRecord,
|
|
||||||
makeJupyterHostRecord,
|
|
||||||
makeStateRecord,
|
|
||||||
makeTransformsRecord,
|
|
||||||
} from "@nteract/core";
|
|
||||||
import { configOption, defineConfigOption } from "@nteract/mythic-configuration";
|
|
||||||
import { Media } from "@nteract/outputs";
|
|
||||||
import TransformVDOM from "@nteract/transform-vdom";
|
|
||||||
import * as Immutable from "immutable";
|
|
||||||
import { Notification } from "react-notification-system";
|
|
||||||
import { AnyAction, Dispatch, Middleware, MiddlewareAPI, Store } from "redux";
|
|
||||||
import * as Constants from "../../Common/Constants";
|
|
||||||
import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels";
|
|
||||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { userContext } from "../../UserContext";
|
|
||||||
import configureStore from "./NotebookComponent/store";
|
|
||||||
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
|
|
||||||
|
|
||||||
export type KernelSpecsDisplay = { name: string; displayName: string };
|
|
||||||
|
|
||||||
export interface NotebookClientV2Parameters {
|
|
||||||
connectionInfo: NotebookWorkspaceConnectionInfo;
|
|
||||||
databaseAccountName: string;
|
|
||||||
defaultExperience: string;
|
|
||||||
isReadOnly?: boolean; // if true: do not fetch kernelspecs automatically (this is for notebook viewer)
|
|
||||||
cellEditorType?: string; // override "codemirror" default,
|
|
||||||
autoSaveInterval?: number; // in ms
|
|
||||||
contentProvider: IContentProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ActionListener = (newValue: any) => void;
|
|
||||||
|
|
||||||
export class NotebookClientV2 {
|
|
||||||
private store: Store<AppState, AnyAction>;
|
|
||||||
private contentHostRef: HostRef;
|
|
||||||
private kernelSpecsForDisplay: KernelSpecsDisplay[] = [];
|
|
||||||
private kernelSpecsRef: KernelspecsRef;
|
|
||||||
|
|
||||||
private databaseAccountName: string;
|
|
||||||
private defaultExperience: string;
|
|
||||||
|
|
||||||
constructor(params: NotebookClientV2Parameters) {
|
|
||||||
this.databaseAccountName = params.databaseAccountName;
|
|
||||||
this.defaultExperience = params.defaultExperience;
|
|
||||||
|
|
||||||
this.configureStore(params);
|
|
||||||
|
|
||||||
this.kernelSpecsRef = createKernelspecsRef();
|
|
||||||
|
|
||||||
// Fetch kernel specs when opening new tab
|
|
||||||
if (!params.isReadOnly) {
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.fetchKernelspecs({
|
|
||||||
hostRef: this.contentHostRef,
|
|
||||||
kernelspecsRef: this.kernelSpecsRef,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getAvailableKernelSpecs(): KernelSpecsDisplay[] {
|
|
||||||
return this.kernelSpecsForDisplay;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getStore(): Store<AppState, AnyAction> {
|
|
||||||
return this.store;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lazy init redux store as singleton.
|
|
||||||
* Don't move store in Explorer yet as it is typed to AppState which is nteract-specific
|
|
||||||
*/
|
|
||||||
private configureStore(params: NotebookClientV2Parameters): void {
|
|
||||||
const jupyterHostRecord = makeJupyterHostRecord({
|
|
||||||
id: null,
|
|
||||||
type: "jupyter",
|
|
||||||
defaultKernelName: "python",
|
|
||||||
token: params.connectionInfo.authToken,
|
|
||||||
origin: params.connectionInfo.notebookServerEndpoint,
|
|
||||||
basePath: "/", // Jupyter server base URL
|
|
||||||
bookstoreEnabled: false, //!!config.bookstore.version,
|
|
||||||
showHeaderEditor: true,
|
|
||||||
crossDomain: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.contentHostRef = createHostRef();
|
|
||||||
const NullTransform = (): any => null;
|
|
||||||
const kernelspecsRef = createKernelspecsRef();
|
|
||||||
|
|
||||||
const initialState: CdbAppState = {
|
|
||||||
app: makeAppRecord({
|
|
||||||
version: "dataExplorer 1.0",
|
|
||||||
host: jupyterHostRecord,
|
|
||||||
// TODO: tamitta: notificationSystem.addNotification was removed, do we need a substitute?
|
|
||||||
}),
|
|
||||||
core: makeStateRecord({
|
|
||||||
currentKernelspecsRef: kernelspecsRef,
|
|
||||||
entities: makeEntitiesRecord({
|
|
||||||
editors: makeEditorsRecord({}),
|
|
||||||
hosts: makeHostsRecord({
|
|
||||||
byRef: Immutable.Map<string, HostRecord>().set(this.contentHostRef, jupyterHostRecord),
|
|
||||||
}),
|
|
||||||
comms: makeCommsRecord(),
|
|
||||||
contents: makeContentsRecord({
|
|
||||||
byRef: Immutable.Map<string, ContentRecord>(),
|
|
||||||
}),
|
|
||||||
transforms: userContext.features.sandboxNotebookOutputs
|
|
||||||
? undefined
|
|
||||||
: makeTransformsRecord({
|
|
||||||
displayOrder: Immutable.List([
|
|
||||||
"application/vnd.jupyter.widget-view+json",
|
|
||||||
"application/vnd.vega.v5+json",
|
|
||||||
"application/vnd.vega.v4+json",
|
|
||||||
"application/vnd.vega.v3+json",
|
|
||||||
"application/vnd.vega.v2+json",
|
|
||||||
"application/vnd.vegalite.v3+json",
|
|
||||||
"application/vnd.vegalite.v2+json",
|
|
||||||
"application/vnd.vegalite.v1+json",
|
|
||||||
"application/geo+json",
|
|
||||||
"application/vnd.plotly.v1+json",
|
|
||||||
"text/vnd.plotly.v1+html",
|
|
||||||
"application/x-nteract-model-debug+json",
|
|
||||||
"application/vnd.dataresource+json",
|
|
||||||
"application/vdom.v1+json",
|
|
||||||
"application/json",
|
|
||||||
"application/javascript",
|
|
||||||
"text/html",
|
|
||||||
"text/markdown",
|
|
||||||
"text/latex",
|
|
||||||
"image/svg+xml",
|
|
||||||
"image/gif",
|
|
||||||
"image/png",
|
|
||||||
"image/jpeg",
|
|
||||||
"text/plain",
|
|
||||||
]),
|
|
||||||
byId: Immutable.Map({
|
|
||||||
"text/vnd.plotly.v1+html": NullTransform,
|
|
||||||
"application/vnd.plotly.v1+json": NullTransform,
|
|
||||||
"application/geo+json": NullTransform,
|
|
||||||
"application/x-nteract-model-debug+json": NullTransform,
|
|
||||||
"application/vnd.dataresource+json": NullTransform,
|
|
||||||
"application/vnd.jupyter.widget-view+json": NullTransform,
|
|
||||||
"application/vnd.vegalite.v1+json": NullTransform,
|
|
||||||
"application/vnd.vegalite.v2+json": NullTransform,
|
|
||||||
"application/vnd.vegalite.v3+json": NullTransform,
|
|
||||||
"application/vnd.vega.v2+json": NullTransform,
|
|
||||||
"application/vnd.vega.v3+json": NullTransform,
|
|
||||||
"application/vnd.vega.v4+json": NullTransform,
|
|
||||||
"application/vnd.vega.v5+json": NullTransform,
|
|
||||||
"application/vdom.v1+json": TransformVDOM,
|
|
||||||
"application/json": Media.Json,
|
|
||||||
"application/javascript": Media.JavaScript,
|
|
||||||
"text/html": Media.HTML,
|
|
||||||
"text/markdown": Media.Markdown,
|
|
||||||
"text/latex": Media.LaTeX,
|
|
||||||
"image/svg+xml": Media.SVG,
|
|
||||||
"image/gif": Media.Image,
|
|
||||||
"image/png": Media.Image,
|
|
||||||
"image/jpeg": Media.Image,
|
|
||||||
"text/plain": Media.Plain,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
cdb: makeCdbRecord({
|
|
||||||
databaseAccountName: params.databaseAccountName,
|
|
||||||
defaultExperience: params.defaultExperience,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Intercept kernelspecs updates actions rather than subscribing to the store state changes (which
|
|
||||||
* is triggered for *any* state change).
|
|
||||||
* TODO: Use react-redux connect() to subscribe to state changes?
|
|
||||||
*/
|
|
||||||
const cacheKernelSpecsMiddleware: Middleware =
|
|
||||||
<D extends Dispatch<AnyAction>, S extends AppState>({ dispatch, getState }: MiddlewareAPI<D, S>) =>
|
|
||||||
(next: Dispatch<AnyAction>) =>
|
|
||||||
<A extends AnyAction>(action: A): A => {
|
|
||||||
switch (action.type) {
|
|
||||||
case actions.FETCH_KERNELSPECS_FULFILLED: {
|
|
||||||
const payload = (action as unknown as actions.FetchKernelspecsFulfilled).payload;
|
|
||||||
const defaultKernelName = payload.defaultKernelName;
|
|
||||||
this.kernelSpecsForDisplay = Object.values(payload.kernelspecs)
|
|
||||||
.filter((spec) => !spec.metadata?.hasOwnProperty("hidden"))
|
|
||||||
.map((spec) => ({
|
|
||||||
name: spec.name,
|
|
||||||
displayName: spec.displayName,
|
|
||||||
}))
|
|
||||||
.sort((a: KernelSpecsDisplay, b: KernelSpecsDisplay) => {
|
|
||||||
// Put default at the top, otherwise lexicographically compare
|
|
||||||
if (a.displayName === defaultKernelName) {
|
|
||||||
return -1;
|
|
||||||
} else if (b.name === defaultKernelName) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return a.displayName.localeCompare(b.displayName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return next(action);
|
|
||||||
};
|
|
||||||
|
|
||||||
const traceErrorFct = (title: string, message: string) => {
|
|
||||||
TelemetryProcessor.traceFailure(Action.NotebookErrorNotification, {
|
|
||||||
dataExplorerArea: Constants.Areas.Notebook,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
level: "Error",
|
|
||||||
});
|
|
||||||
console.error(`${title}: ${message}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.store = configureStore(
|
|
||||||
initialState,
|
|
||||||
params.contentProvider,
|
|
||||||
traceErrorFct,
|
|
||||||
[cacheKernelSpecsMiddleware],
|
|
||||||
!params.isReadOnly,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Additional configuration
|
|
||||||
this.store.dispatch(configOption("editorType").action(params.cellEditorType ?? "codemirror"));
|
|
||||||
this.store.dispatch(
|
|
||||||
configOption("autoSaveInterval").action(params.autoSaveInterval ?? Constants.Notebook.autoSaveIntervalMs),
|
|
||||||
);
|
|
||||||
this.store.dispatch(configOption("codeMirror.lineNumbers").action(true));
|
|
||||||
|
|
||||||
const readOnlyConfigOption = configOption("codeMirror.readOnly");
|
|
||||||
const readOnlyValue = params.isReadOnly ? "nocursor" : undefined;
|
|
||||||
if (!readOnlyConfigOption) {
|
|
||||||
defineConfigOption({
|
|
||||||
label: "Read-only",
|
|
||||||
key: "codeMirror.readOnly",
|
|
||||||
values: [
|
|
||||||
{ label: "Read-Only", value: "nocursor" },
|
|
||||||
{ label: "Not read-only", value: undefined },
|
|
||||||
],
|
|
||||||
defaultValue: readOnlyValue,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.store.dispatch(readOnlyConfigOption.action(readOnlyValue));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle notification coming from nteract
|
|
||||||
* The messages coming from nteract are not good enough to expose to user.
|
|
||||||
* We use the notificationsToUserEpic to control the messages from action.
|
|
||||||
* We log possible errors coming from nteract in telemetry and display in console
|
|
||||||
*/
|
|
||||||
private handleNotification = (msg: Notification): void => {
|
|
||||||
if (msg.level === "error") {
|
|
||||||
TelemetryProcessor.traceFailure(Action.NotebookErrorNotification, {
|
|
||||||
dataExplorerArea: Constants.Areas.Notebook,
|
|
||||||
title: msg.title,
|
|
||||||
message: msg.message,
|
|
||||||
level: msg.level,
|
|
||||||
});
|
|
||||||
console.error(`${msg.title}: ${msg.message}`);
|
|
||||||
} else {
|
|
||||||
console.log(`${msg.title}: ${msg.message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import { FileType, IContent, IContentProvider, ServerConfig } from "@nteract/core";
|
|
||||||
import { Observable, of } from "rxjs";
|
|
||||||
import { AjaxResponse } from "rxjs/ajax";
|
|
||||||
import { HttpStatusCodes } from "../../../../Common/Constants";
|
|
||||||
import { getErrorMessage } from "../../../../Common/ErrorHandlingUtils";
|
|
||||||
import * as Logger from "../../../../Common/Logger";
|
|
||||||
|
|
||||||
export interface InMemoryContentProviderParams {
|
|
||||||
[path: string]: { readonly: boolean; content: IContent<FileType> };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nteract relies on `errno` property to figure out the kind of failure
|
|
||||||
// That's why we need a custom wrapper around Error to include `errno` property
|
|
||||||
class InMemoryContentProviderError extends Error {
|
|
||||||
constructor(
|
|
||||||
error: string,
|
|
||||||
public errno: number = InMemoryContentProvider.SelfErrorCode,
|
|
||||||
) {
|
|
||||||
super(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class InMemoryContentProvider implements IContentProvider {
|
|
||||||
public static readonly SelfErrorCode = 666;
|
|
||||||
|
|
||||||
constructor(private params: InMemoryContentProviderParams) {}
|
|
||||||
|
|
||||||
public remove(): Observable<AjaxResponse> {
|
|
||||||
return this.errorResponse("Not implemented", "remove");
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
public get(_config: ServerConfig, uri: string): Observable<AjaxResponse> {
|
|
||||||
const item = this.params[uri];
|
|
||||||
if (item) {
|
|
||||||
return of(this.createSuccessAjaxResponse(HttpStatusCodes.OK, item.content));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.errorResponse(`${uri} not found`, "get");
|
|
||||||
}
|
|
||||||
|
|
||||||
public update(): Observable<AjaxResponse> {
|
|
||||||
return this.errorResponse("Not implemented", "update");
|
|
||||||
}
|
|
||||||
|
|
||||||
public create(): Observable<AjaxResponse> {
|
|
||||||
return this.errorResponse("Not implemented", "create");
|
|
||||||
}
|
|
||||||
|
|
||||||
public save<FT extends FileType>(
|
|
||||||
_config: ServerConfig, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
||||||
uri: string,
|
|
||||||
model: Partial<IContent<FT>>,
|
|
||||||
): Observable<AjaxResponse> {
|
|
||||||
const item = this.params[uri];
|
|
||||||
if (item) {
|
|
||||||
if (!item.readonly) {
|
|
||||||
Object.assign(item.content, model);
|
|
||||||
}
|
|
||||||
return of(this.createSuccessAjaxResponse(HttpStatusCodes.OK, item.content));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.errorResponse(`${uri} not found`, "save");
|
|
||||||
}
|
|
||||||
|
|
||||||
public listCheckpoints(): Observable<AjaxResponse> {
|
|
||||||
return this.errorResponse("Not implemented", "listCheckpoints");
|
|
||||||
}
|
|
||||||
|
|
||||||
public createCheckpoint(): Observable<AjaxResponse> {
|
|
||||||
return this.errorResponse("Not implemented", "createCheckpoint");
|
|
||||||
}
|
|
||||||
|
|
||||||
public deleteCheckpoint(): Observable<AjaxResponse> {
|
|
||||||
return this.errorResponse("Not implemented", "deleteCheckpoint");
|
|
||||||
}
|
|
||||||
|
|
||||||
public restoreFromCheckpoint(): Observable<AjaxResponse> {
|
|
||||||
return this.errorResponse("Not implemented", "restoreFromCheckpoint");
|
|
||||||
}
|
|
||||||
|
|
||||||
private errorResponse(message: string, functionName: string): Observable<AjaxResponse> {
|
|
||||||
const error = new InMemoryContentProviderError(message);
|
|
||||||
Logger.logError(error.message, `InMemoryContentProvider/${functionName}`, error.errno);
|
|
||||||
return of(this.createErrorAjaxResponse(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
private createSuccessAjaxResponse(status: number, content: IContent<FileType>): AjaxResponse {
|
|
||||||
return {
|
|
||||||
originalEvent: new Event("no-op"),
|
|
||||||
xhr: new XMLHttpRequest(),
|
|
||||||
request: {},
|
|
||||||
status,
|
|
||||||
response: content ? content : undefined,
|
|
||||||
responseText: content ? JSON.stringify(content) : undefined,
|
|
||||||
responseType: "json",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private createErrorAjaxResponse(error: InMemoryContentProviderError): AjaxResponse {
|
|
||||||
return {
|
|
||||||
originalEvent: new Event("no-op"),
|
|
||||||
xhr: new XMLHttpRequest(),
|
|
||||||
request: {},
|
|
||||||
status: error.errno,
|
|
||||||
response: error,
|
|
||||||
responseText: getErrorMessage(error),
|
|
||||||
responseType: "json",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
// memory://<path>
|
|
||||||
// Custom scheme for in memory content
|
|
||||||
export const ContentUriPattern = /memory:\/\/([^/]*)/;
|
|
||||||
|
|
||||||
export function fromContentUri(contentUri: string): undefined | string {
|
|
||||||
const matches = contentUri.match(ContentUriPattern);
|
|
||||||
if (matches && matches.length > 1) {
|
|
||||||
return matches[1];
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toContentUri(path: string): string {
|
|
||||||
return `memory://${path}`;
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import * as InMemoryContentProviderUtils from "./ContentProviders/InMemoryContentProviderUtils";
|
|
||||||
|
|
||||||
describe("fromContentUri", () => {
|
|
||||||
it("fromContentUri should return valid result", () => {
|
|
||||||
const contentUri = "memory://resource/path";
|
|
||||||
const result = "resource";
|
|
||||||
|
|
||||||
expect(InMemoryContentProviderUtils.fromContentUri(contentUri)).toEqual(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fromContentUri should return undefined on invalid input", () => {
|
|
||||||
const contentUri = "invalid";
|
|
||||||
|
|
||||||
expect(InMemoryContentProviderUtils.fromContentUri(contentUri)).toEqual(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("toContentUri should return valid result", () => {
|
|
||||||
const path = "resource/path";
|
|
||||||
const result = "memory://resource/path";
|
|
||||||
|
|
||||||
expect(InMemoryContentProviderUtils.toContentUri(path)).toEqual(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
.notebookComponentContainer {
|
|
||||||
text-transform: none;
|
|
||||||
line-height: 1.28581;
|
|
||||||
letter-spacing: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 400;
|
|
||||||
color: #182026;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.hotKeys {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { ContentRef } from "@nteract/core";
|
|
||||||
import * as React from "react";
|
|
||||||
import NotificationSystem, { System as ReactNotificationSystem } from "react-notification-system";
|
|
||||||
import { default as Contents } from "./contents";
|
|
||||||
|
|
||||||
export class NotebookComponent extends React.Component<{ contentRef: ContentRef }> {
|
|
||||||
notificationSystem!: ReactNotificationSystem;
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: { contentRef: ContentRef }): boolean {
|
|
||||||
return nextProps.contentRef !== this.props.contentRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<div className="notebookComponentContainer">
|
|
||||||
<Contents contentRef={this.props.contentRef} />
|
|
||||||
<NotificationSystem
|
|
||||||
ref={(notificationSystem: ReactNotificationSystem) => {
|
|
||||||
this.notificationSystem = notificationSystem;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// Vendor modules
|
|
||||||
import { actions, createContentRef, createKernelRef, selectors } from "@nteract/core";
|
|
||||||
import * as React from "react";
|
|
||||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
|
||||||
import { NotebookClientV2 } from "../NotebookClientV2";
|
|
||||||
import { NotebookContentItem } from "../NotebookContentItem";
|
|
||||||
import { NotebookComponentBootstrapper } from "./NotebookComponentBootstrapper";
|
|
||||||
import VirtualCommandBarComponent from "./VirtualCommandBarComponent";
|
|
||||||
|
|
||||||
export interface NotebookComponentAdapterOptions {
|
|
||||||
contentItem: NotebookContentItem;
|
|
||||||
notebooksBasePath: string;
|
|
||||||
notebookClient: NotebookClientV2;
|
|
||||||
onUpdateKernelInfo: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NotebookComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
|
|
||||||
private onUpdateKernelInfo: () => void;
|
|
||||||
public parameters: any;
|
|
||||||
|
|
||||||
constructor(options: NotebookComponentAdapterOptions) {
|
|
||||||
super({
|
|
||||||
contentRef: selectors.contentRefByFilepath(options.notebookClient.getStore().getState(), {
|
|
||||||
filepath: options.contentItem.path,
|
|
||||||
}),
|
|
||||||
notebookClient: options.notebookClient,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.onUpdateKernelInfo = options.onUpdateKernelInfo;
|
|
||||||
|
|
||||||
if (!this.contentRef) {
|
|
||||||
this.contentRef = createContentRef();
|
|
||||||
const kernelRef = createKernelRef();
|
|
||||||
|
|
||||||
// Request fetching notebook content
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.fetchContent({
|
|
||||||
filepath: options.contentItem.path,
|
|
||||||
params: {},
|
|
||||||
kernelRef,
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected renderExtraComponent = (): JSX.Element => {
|
|
||||||
return <VirtualCommandBarComponent contentRef={this.contentRef} onRender={this.onUpdateKernelInfo} />;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,379 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { Link } from "@fluentui/react";
|
|
||||||
import { CellId, CellType, ImmutableNotebook } from "@nteract/commutable";
|
|
||||||
// Vendor modules
|
|
||||||
import { actions, AppState, ContentRef, KernelRef, NotebookContentRecord, selectors } from "@nteract/core";
|
|
||||||
import "@nteract/styles/editor-overrides.css";
|
|
||||||
import "@nteract/styles/global-variables.css";
|
|
||||||
import "codemirror/addon/hint/show-hint.css";
|
|
||||||
import "codemirror/lib/codemirror.css";
|
|
||||||
import { Notebook } from "Common/Constants";
|
|
||||||
import { useDialog } from "Explorer/Controls/Dialog";
|
|
||||||
import * as React from "react";
|
|
||||||
import { Provider } from "react-redux";
|
|
||||||
import "react-table/react-table.css";
|
|
||||||
import { AnyAction, Store } from "redux";
|
|
||||||
import { NotebookClientV2 } from "../NotebookClientV2";
|
|
||||||
import { NotebookContentProviderType, NotebookUtil } from "../NotebookUtil";
|
|
||||||
import * as NteractUtil from "../NTeractUtil";
|
|
||||||
import * as CdbActions from "./actions";
|
|
||||||
import { NotebookComponent } from "./NotebookComponent";
|
|
||||||
import "./NotebookComponent.less";
|
|
||||||
|
|
||||||
export interface NotebookComponentBootstrapperOptions {
|
|
||||||
notebookClient: NotebookClientV2;
|
|
||||||
contentRef: ContentRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IWrapModel {
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
last_modified: Date;
|
|
||||||
created: string;
|
|
||||||
content: unknown;
|
|
||||||
format: string;
|
|
||||||
mimetype: unknown;
|
|
||||||
size: number;
|
|
||||||
writeable: boolean;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NotebookComponentBootstrapper {
|
|
||||||
public contentRef: ContentRef;
|
|
||||||
protected renderExtraComponent: () => JSX.Element;
|
|
||||||
|
|
||||||
private notebookClient: NotebookClientV2;
|
|
||||||
|
|
||||||
constructor(options: NotebookComponentBootstrapperOptions) {
|
|
||||||
this.notebookClient = options.notebookClient;
|
|
||||||
this.contentRef = options.contentRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static wrapModelIntoContent(name: string, path: string, content: unknown): IWrapModel {
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
path,
|
|
||||||
last_modified: new Date(),
|
|
||||||
created: "",
|
|
||||||
content,
|
|
||||||
format: "json",
|
|
||||||
mimetype: undefined,
|
|
||||||
size: 0,
|
|
||||||
writeable: false,
|
|
||||||
type: "notebook",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderDefaultNotebookComponent(props: any): JSX.Element {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{this.renderExtraComponent && this.renderExtraComponent()}
|
|
||||||
{React.createElement<{ contentRef: ContentRef }>(NotebookComponent, { contentRef: this.contentRef, ...props })}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getContent(): { name: string; content: string | ImmutableNotebook } {
|
|
||||||
const record = this.getStore().getState().core.entities.contents.byRef.get(this.contentRef);
|
|
||||||
let content: string | ImmutableNotebook;
|
|
||||||
switch (record.model.type) {
|
|
||||||
case "notebook":
|
|
||||||
content = record.model.notebook;
|
|
||||||
break;
|
|
||||||
case "file":
|
|
||||||
content = record.model.text;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported model type ${record.model.type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: NotebookUtil.getName(record.filepath),
|
|
||||||
content,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public getNotebookPath(): string {
|
|
||||||
return this.getStore().getState().core.entities.contents.byRef.get(this.contentRef)?.filepath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setContent(name: string, content: unknown): void {
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.fetchContentFulfilled({
|
|
||||||
filepath: undefined,
|
|
||||||
model: NotebookComponentBootstrapper.wrapModelIntoContent(name, undefined, content),
|
|
||||||
kernelRef: undefined,
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We can overload the notebook renderer here
|
|
||||||
* @param renderer
|
|
||||||
* @props additional props
|
|
||||||
*/
|
|
||||||
public renderComponent(
|
|
||||||
renderer?: any, // TODO FIX THIS React.ComponentClass<{ contentRef: ContentRef; isReadOnly?: boolean }>,
|
|
||||||
props?: any,
|
|
||||||
): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Provider store={this.getStore()}>
|
|
||||||
{renderer
|
|
||||||
? React.createElement<{ contentRef: ContentRef }>(renderer, { contentRef: this.contentRef, ...props })
|
|
||||||
: this.renderDefaultNotebookComponent(props)}
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Notebook operations. See nteract/packages/connected-components/src/notebook-menu/index.tsx */
|
|
||||||
public notebookSave(): void {
|
|
||||||
if (
|
|
||||||
NotebookUtil.getContentProviderType(this.getNotebookPath()) ===
|
|
||||||
NotebookContentProviderType.JupyterContentProviderType
|
|
||||||
) {
|
|
||||||
useDialog.getState().showOkCancelModalDialog(
|
|
||||||
Notebook.saveNotebookModalTitle,
|
|
||||||
undefined,
|
|
||||||
"Save",
|
|
||||||
async () => {
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.save({
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
"Cancel",
|
|
||||||
undefined,
|
|
||||||
this.getSaveNotebookSubText(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.save({
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public notebookChangeKernel(kernelSpecName: string): void {
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.changeKernelByName({
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
kernelSpecName,
|
|
||||||
oldKernelRef: this.getCurrentKernelRef(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public notebookRunAndAdvance(): void {
|
|
||||||
this.getStore().dispatch(
|
|
||||||
CdbActions.executeFocusedCellAndFocusNext({
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public notebookRunAll(): void {
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.executeAllCells({
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public notebookInterruptKernel(): void {
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.interruptKernel({
|
|
||||||
kernelRef: this.getCurrentKernelRef(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public notebookKillKernel(): void {
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.killKernel({
|
|
||||||
restarting: false,
|
|
||||||
kernelRef: this.getCurrentKernelRef(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public notebookRestartKernel(): void {
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.restartKernel({
|
|
||||||
kernelRef: this.getCurrentKernelRef(),
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
outputHandling: "None",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public notebookClearAllOutputs(): void {
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.clearAllOutputs({
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public notebookInsertBelow(): void {
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.createCellBelow({
|
|
||||||
cellType: "code",
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public notebookChangeCellType(type: CellType): void {
|
|
||||||
const focusedCellId = this.getFocusedCellId();
|
|
||||||
if (!focusedCellId) {
|
|
||||||
console.error("No focused cell");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.changeCellType({
|
|
||||||
id: focusedCellId,
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
to: type,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public notebokCopy(): void {
|
|
||||||
const focusedCellId = this.getFocusedCellId();
|
|
||||||
if (!focusedCellId) {
|
|
||||||
console.error("No focused cell");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.copyCell({
|
|
||||||
id: focusedCellId,
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public notebookCut(): void {
|
|
||||||
const focusedCellId = this.getFocusedCellId();
|
|
||||||
if (!focusedCellId) {
|
|
||||||
console.error("No focused cell");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.cutCell({
|
|
||||||
id: focusedCellId,
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public notebookPaste(): void {
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.pasteCell({
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public notebookShutdown(): void {
|
|
||||||
const store = this.getStore();
|
|
||||||
const kernelRef = this.getCurrentKernelRef();
|
|
||||||
|
|
||||||
if (kernelRef) {
|
|
||||||
store.dispatch(
|
|
||||||
actions.killKernel({
|
|
||||||
restarting: false,
|
|
||||||
kernelRef,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
store.dispatch(
|
|
||||||
CdbActions.closeNotebook({
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public isContentDirty(): boolean {
|
|
||||||
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
|
|
||||||
if (!content) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Fix this typing here
|
|
||||||
return selectors.notebook.isDirty(content.model as never);
|
|
||||||
}
|
|
||||||
|
|
||||||
public isNotebookUntrusted(): boolean {
|
|
||||||
return NotebookUtil.isNotebookUntrusted(this.getStore().getState(), this.contentRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For display purposes, only return non-killed kernels
|
|
||||||
*/
|
|
||||||
public getCurrentKernelName(): string {
|
|
||||||
const currentKernel = selectors.kernel(this.getStore().getState(), { kernelRef: this.getCurrentKernelRef() });
|
|
||||||
return (currentKernel && currentKernel.status !== "killed" && currentKernel.kernelSpecName) || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the kernel name to select in the kernels dropdown
|
|
||||||
public getSelectedKernelName(): string {
|
|
||||||
const currentKernelName = this.getCurrentKernelName();
|
|
||||||
if (!currentKernelName) {
|
|
||||||
// if there's no live kernel, try to get the kernel name from the notebook metadata
|
|
||||||
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
|
|
||||||
const notebook = content && (content as NotebookContentRecord).model.notebook;
|
|
||||||
if (!notebook) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { kernelSpecName } = NotebookUtil.extractNewKernel("", notebook);
|
|
||||||
return kernelSpecName || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentKernelName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getActiveCellTypeStr(): string {
|
|
||||||
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
|
|
||||||
return NteractUtil.getCurrentCellType(content as NotebookContentRecord);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCurrentKernelRef(): KernelRef {
|
|
||||||
return selectors.kernelRefByContentRef(this.getStore().getState(), { contentRef: this.contentRef });
|
|
||||||
}
|
|
||||||
|
|
||||||
private getFocusedCellId(): CellId {
|
|
||||||
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
|
|
||||||
if (!content) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return selectors.notebook.cellFocused((content as NotebookContentRecord).model);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getStore(): Store<AppState, AnyAction> {
|
|
||||||
return this.notebookClient.getStore();
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSaveNotebookSubText(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p>{Notebook.saveNotebookModalContent}</p>
|
|
||||||
<br />
|
|
||||||
<p>
|
|
||||||
{Notebook.newNotebookModalContent2}
|
|
||||||
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
|
|
||||||
{Notebook.learnMore}
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import { FileType, IContent, IContentProvider, IGetParams, ServerConfig } from "@nteract/core";
|
|
||||||
import { Observable } from "rxjs";
|
|
||||||
import { AjaxResponse } from "rxjs/ajax";
|
|
||||||
import { GitHubContentProvider } from "../../../GitHub/GitHubContentProvider";
|
|
||||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
|
||||||
import { InMemoryContentProvider } from "./ContentProviders/InMemoryContentProvider";
|
|
||||||
import * as InMemoryContentProviderUtils from "./ContentProviders/InMemoryContentProviderUtils";
|
|
||||||
|
|
||||||
export class NotebookContentProvider implements IContentProvider {
|
|
||||||
constructor(
|
|
||||||
private inMemoryContentProvider: InMemoryContentProvider,
|
|
||||||
private gitHubContentProvider: GitHubContentProvider,
|
|
||||||
private jupyterContentProvider: IContentProvider,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public remove(serverConfig: ServerConfig, path: string): Observable<AjaxResponse> {
|
|
||||||
return this.getContentProvider(path).remove(serverConfig, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get(serverConfig: ServerConfig, path: string, params: Partial<IGetParams>): Observable<AjaxResponse> {
|
|
||||||
return this.getContentProvider(path).get(serverConfig, path, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
public update<FT extends FileType>(
|
|
||||||
serverConfig: ServerConfig,
|
|
||||||
path: string,
|
|
||||||
model: Partial<IContent<FT>>,
|
|
||||||
): Observable<AjaxResponse> {
|
|
||||||
return this.getContentProvider(path).update(serverConfig, path, model);
|
|
||||||
}
|
|
||||||
|
|
||||||
public create<FT extends FileType>(
|
|
||||||
serverConfig: ServerConfig,
|
|
||||||
path: string,
|
|
||||||
model: Partial<IContent<FT>> & { type: FT },
|
|
||||||
): Observable<AjaxResponse> {
|
|
||||||
return this.getContentProvider(path).create(serverConfig, path, model);
|
|
||||||
}
|
|
||||||
|
|
||||||
public save<FT extends FileType>(
|
|
||||||
serverConfig: ServerConfig,
|
|
||||||
path: string,
|
|
||||||
model: Partial<IContent<FT>>,
|
|
||||||
): Observable<AjaxResponse> {
|
|
||||||
return this.getContentProvider(path).save(serverConfig, path, model);
|
|
||||||
}
|
|
||||||
|
|
||||||
public listCheckpoints(serverConfig: ServerConfig, path: string): Observable<AjaxResponse> {
|
|
||||||
return this.getContentProvider(path).listCheckpoints(serverConfig, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public createCheckpoint(serverConfig: ServerConfig, path: string): Observable<AjaxResponse> {
|
|
||||||
return this.getContentProvider(path).createCheckpoint(serverConfig, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public deleteCheckpoint(serverConfig: ServerConfig, path: string, checkpointID: string): Observable<AjaxResponse> {
|
|
||||||
return this.getContentProvider(path).deleteCheckpoint(serverConfig, path, checkpointID);
|
|
||||||
}
|
|
||||||
|
|
||||||
public restoreFromCheckpoint(
|
|
||||||
serverConfig: ServerConfig,
|
|
||||||
path: string,
|
|
||||||
checkpointID: string,
|
|
||||||
): Observable<AjaxResponse> {
|
|
||||||
return this.getContentProvider(path).restoreFromCheckpoint(serverConfig, path, checkpointID);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getContentProvider(path: string): IContentProvider {
|
|
||||||
if (InMemoryContentProviderUtils.fromContentUri(path)) {
|
|
||||||
return this.inMemoryContentProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (GitHubUtils.fromContentUri(path)) {
|
|
||||||
return this.gitHubContentProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.jupyterContentProvider;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { AppState, ContentRef, selectors } from "@nteract/core";
|
|
||||||
import * as React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { NotebookUtil } from "../NotebookUtil";
|
|
||||||
import * as NteractUtil from "../NTeractUtil";
|
|
||||||
|
|
||||||
interface VirtualCommandBarComponentProps {
|
|
||||||
kernelSpecName: string;
|
|
||||||
kernelStatus: string;
|
|
||||||
currentCellType: string;
|
|
||||||
isNotebookUntrusted: boolean;
|
|
||||||
onRender: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
class VirtualCommandBarComponent extends React.Component<VirtualCommandBarComponentProps> {
|
|
||||||
constructor(props: VirtualCommandBarComponentProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: VirtualCommandBarComponentProps): boolean {
|
|
||||||
return (
|
|
||||||
this.props.kernelStatus !== nextProps.kernelStatus ||
|
|
||||||
this.props.kernelSpecName !== nextProps.kernelSpecName ||
|
|
||||||
this.props.currentCellType !== nextProps.currentCellType ||
|
|
||||||
this.props.isNotebookUntrusted !== nextProps.isNotebookUntrusted
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
|
||||||
this.props.onRender && this.props.onRender();
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InitialProps {
|
|
||||||
contentRef: ContentRef;
|
|
||||||
onRender: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redux
|
|
||||||
const makeMapStateToProps = (
|
|
||||||
initialState: AppState,
|
|
||||||
initialProps: InitialProps,
|
|
||||||
): ((state: AppState) => VirtualCommandBarComponentProps) => {
|
|
||||||
const { contentRef } = initialProps;
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
|
||||||
const content = selectors.content(state, { contentRef });
|
|
||||||
let kernelStatus, kernelSpecName, currentCellType;
|
|
||||||
|
|
||||||
if (!content || content.type !== "notebook") {
|
|
||||||
return {
|
|
||||||
kernelStatus,
|
|
||||||
kernelSpecName,
|
|
||||||
currentCellType,
|
|
||||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
|
|
||||||
} as VirtualCommandBarComponentProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
const kernelRef = content.model.kernelRef;
|
|
||||||
let kernel;
|
|
||||||
if (kernelRef) {
|
|
||||||
kernel = selectors.kernel(state, { kernelRef });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (kernel) {
|
|
||||||
kernelStatus = kernel.status;
|
|
||||||
kernelSpecName = kernel.kernelSpecName;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentCellType = NteractUtil.getCurrentCellType(content);
|
|
||||||
return {
|
|
||||||
kernelStatus,
|
|
||||||
kernelSpecName,
|
|
||||||
currentCellType,
|
|
||||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
|
|
||||||
onRender: initialProps.onRender,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps)(VirtualCommandBarComponent);
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { Observable, of } from "rxjs";
|
|
||||||
import { AjaxRequest, AjaxResponse } from "rxjs/ajax";
|
|
||||||
|
|
||||||
let fakeAjaxResponse: AjaxResponse = {
|
|
||||||
originalEvent: <Event>(<unknown>undefined),
|
|
||||||
xhr: new XMLHttpRequest(),
|
|
||||||
request: <AjaxRequest>(<unknown>null),
|
|
||||||
status: 200,
|
|
||||||
response: {},
|
|
||||||
responseText: "",
|
|
||||||
responseType: "json",
|
|
||||||
};
|
|
||||||
export const sessions = {
|
|
||||||
create: (): Observable<AjaxResponse> => of(fakeAjaxResponse),
|
|
||||||
__setResponse: (response: AjaxResponse) => {
|
|
||||||
fakeAjaxResponse = response;
|
|
||||||
},
|
|
||||||
createSpy: undefined as any,
|
|
||||||
};
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import { CellId } from "@nteract/commutable";
|
|
||||||
import { ContentRef } from "@nteract/core";
|
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import { SnapshotFragment, SnapshotRequest } from "./types";
|
|
||||||
|
|
||||||
export const CLOSE_NOTEBOOK = "CLOSE_NOTEBOOK";
|
|
||||||
export interface CloseNotebookAction {
|
|
||||||
type: "CLOSE_NOTEBOOK";
|
|
||||||
payload: {
|
|
||||||
contentRef: ContentRef;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const closeNotebook = (payload: { contentRef: ContentRef }): CloseNotebookAction => {
|
|
||||||
return {
|
|
||||||
type: CLOSE_NOTEBOOK,
|
|
||||||
payload,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT = "EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT";
|
|
||||||
export interface ExecuteFocusedCellAndFocusNextAction {
|
|
||||||
type: "EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT";
|
|
||||||
payload: {
|
|
||||||
contentRef: ContentRef;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const executeFocusedCellAndFocusNext = (payload: {
|
|
||||||
contentRef: ContentRef;
|
|
||||||
}): ExecuteFocusedCellAndFocusNextAction => {
|
|
||||||
return {
|
|
||||||
type: EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT,
|
|
||||||
payload,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const UPDATE_KERNEL_RESTART_DELAY = "UPDATE_KERNEL_RESTART_DELAY";
|
|
||||||
export interface UpdateKernelRestartDelayAction {
|
|
||||||
type: "UPDATE_KERNEL_RESTART_DELAY";
|
|
||||||
payload: {
|
|
||||||
delayMs: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UpdateKernelRestartDelay = (payload: { delayMs: number }): UpdateKernelRestartDelayAction => {
|
|
||||||
return {
|
|
||||||
type: UPDATE_KERNEL_RESTART_DELAY,
|
|
||||||
payload,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SET_HOVERED_CELL = "SET_HOVERED_CELL";
|
|
||||||
export interface SetHoveredCellAction {
|
|
||||||
type: "SET_HOVERED_CELL";
|
|
||||||
payload: {
|
|
||||||
cellId: CellId;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setHoveredCell = (payload: { cellId: CellId }): SetHoveredCellAction => {
|
|
||||||
return {
|
|
||||||
type: SET_HOVERED_CELL,
|
|
||||||
payload,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TRACE_NOTEBOOK_TELEMETRY = "TRACE_NOTEBOOK_TELEMETRY";
|
|
||||||
export interface TraceNotebookTelemetryAction {
|
|
||||||
type: "TRACE_NOTEBOOK_TELEMETRY";
|
|
||||||
payload: {
|
|
||||||
action: Action;
|
|
||||||
actionModifier?: string;
|
|
||||||
data?: any;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const traceNotebookTelemetry = (payload: {
|
|
||||||
action: Action;
|
|
||||||
actionModifier?: string;
|
|
||||||
data?: any;
|
|
||||||
}): TraceNotebookTelemetryAction => {
|
|
||||||
return {
|
|
||||||
type: TRACE_NOTEBOOK_TELEMETRY,
|
|
||||||
payload,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const STORE_CELL_OUTPUT_SNAPSHOT = "STORE_CELL_OUTPUT_SNAPSHOT";
|
|
||||||
export interface StoreCellOutputSnapshotAction {
|
|
||||||
type: "STORE_CELL_OUTPUT_SNAPSHOT";
|
|
||||||
payload: {
|
|
||||||
cellId: string;
|
|
||||||
snapshot: SnapshotFragment;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const storeCellOutputSnapshot = (payload: {
|
|
||||||
cellId: string;
|
|
||||||
snapshot: SnapshotFragment;
|
|
||||||
}): StoreCellOutputSnapshotAction => {
|
|
||||||
return {
|
|
||||||
type: STORE_CELL_OUTPUT_SNAPSHOT,
|
|
||||||
payload,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const STORE_NOTEBOOK_SNAPSHOT = "STORE_NOTEBOOK_SNAPSHOT";
|
|
||||||
export interface StoreNotebookSnapshotAction {
|
|
||||||
type: "STORE_NOTEBOOK_SNAPSHOT";
|
|
||||||
payload: {
|
|
||||||
imageSrc: string;
|
|
||||||
requestId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const storeNotebookSnapshot = (payload: {
|
|
||||||
imageSrc: string;
|
|
||||||
requestId: string;
|
|
||||||
}): StoreNotebookSnapshotAction => {
|
|
||||||
return {
|
|
||||||
type: STORE_NOTEBOOK_SNAPSHOT,
|
|
||||||
payload,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TAKE_NOTEBOOK_SNAPSHOT = "TAKE_NOTEBOOK_SNAPSHOT";
|
|
||||||
export interface TakeNotebookSnapshotAction {
|
|
||||||
type: "TAKE_NOTEBOOK_SNAPSHOT";
|
|
||||||
payload: SnapshotRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const takeNotebookSnapshot = (payload: SnapshotRequest): TakeNotebookSnapshotAction => {
|
|
||||||
return {
|
|
||||||
type: TAKE_NOTEBOOK_SNAPSHOT,
|
|
||||||
payload,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NOTEBOOK_SNAPSHOT_ERROR = "NOTEBOOK_SNAPSHOT_ERROR";
|
|
||||||
export interface NotebookSnapshotErrorAction {
|
|
||||||
type: "NOTEBOOK_SNAPSHOT_ERROR";
|
|
||||||
payload: {
|
|
||||||
error: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const notebookSnapshotError = (payload: { error: string }): NotebookSnapshotErrorAction => {
|
|
||||||
return {
|
|
||||||
type: NOTEBOOK_SNAPSHOT_ERROR,
|
|
||||||
payload,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import { AppState, ContentRef, selectors } from "@nteract/core";
|
|
||||||
import * as React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import styled from "styled-components";
|
|
||||||
import NotebookRenderer from "../../../NotebookRenderer/NotebookRenderer";
|
|
||||||
import * as TextFile from "./text-file";
|
|
||||||
|
|
||||||
const PaddedContainer = styled.div`
|
|
||||||
padding-left: var(--nt-spacing-l, 10px);
|
|
||||||
padding-top: var(--nt-spacing-m, 10px);
|
|
||||||
padding-right: var(--nt-spacing-m, 10px);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const JupyterExtensionContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
align-items: stretch;
|
|
||||||
height: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const JupyterExtensionChoiceContainer = styled.div`
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow: auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface FileProps {
|
|
||||||
type: "notebook" | "file" | "dummy";
|
|
||||||
contentRef: ContentRef;
|
|
||||||
mimetype?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class File extends React.PureComponent<FileProps> {
|
|
||||||
getChoice = () => {
|
|
||||||
let choice;
|
|
||||||
|
|
||||||
// notebooks don't report a mimetype so we'll use the content.type
|
|
||||||
if (this.props.type === "notebook") {
|
|
||||||
choice = <NotebookRenderer contentRef={this.props.contentRef} />;
|
|
||||||
} else if (this.props.type === "dummy") {
|
|
||||||
choice = undefined;
|
|
||||||
} else if (this.props.mimetype === undefined || !TextFile.handles(this.props.mimetype)) {
|
|
||||||
// This should not happen as we intercept mimetype upstream, but just in case
|
|
||||||
choice = (
|
|
||||||
<PaddedContainer>
|
|
||||||
<pre>
|
|
||||||
This file type cannot be rendered. Please download the file, in order to view it outside of Data Explorer.
|
|
||||||
</pre>
|
|
||||||
</PaddedContainer>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
choice = <TextFile.default contentRef={this.props.contentRef} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return choice;
|
|
||||||
};
|
|
||||||
|
|
||||||
render(): JSX.Element {
|
|
||||||
const choice = this.getChoice();
|
|
||||||
|
|
||||||
// Right now we only handle one kind of editor
|
|
||||||
// If/when we support more modes, we would case them off here
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<JupyterExtensionContainer>
|
|
||||||
<JupyterExtensionChoiceContainer>{choice}</JupyterExtensionChoiceContainer>
|
|
||||||
</JupyterExtensionContainer>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InitialProps {
|
|
||||||
contentRef: ContentRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since the contentRef stays unique for the duration of this file,
|
|
||||||
// we use the makeMapStateToProps pattern to optimize re-render
|
|
||||||
const makeMapStateToProps = (initialState: AppState, initialProps: InitialProps) => {
|
|
||||||
const { contentRef } = initialProps;
|
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
|
||||||
const content = selectors.content(state, initialProps);
|
|
||||||
|
|
||||||
return {
|
|
||||||
contentRef,
|
|
||||||
mimetype: content.mimetype,
|
|
||||||
type: content.type,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ConnectedFile = connect(makeMapStateToProps)(File);
|
|
||||||
|
|
||||||
export default ConnectedFile;
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
|
|
||||||
import { IMonacoProps as MonacoEditorProps } from "@nteract/monaco-editor";
|
|
||||||
import * as React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
import styled from "styled-components";
|
|
||||||
import * as StringUtils from "../../../../../Utils/StringUtils";
|
|
||||||
|
|
||||||
const EditorContainer = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.monaco {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface MappedStateProps {
|
|
||||||
mimetype: string;
|
|
||||||
text: string;
|
|
||||||
contentRef: ContentRef;
|
|
||||||
theme?: "light" | "dark";
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MappedDispatchProps {
|
|
||||||
handleChange: (value: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type TextFileProps = MappedStateProps & MappedDispatchProps;
|
|
||||||
|
|
||||||
interface TextFileState {
|
|
||||||
Editor: React.ComponentType<MonacoEditorProps>;
|
|
||||||
}
|
|
||||||
|
|
||||||
class EditorPlaceholder extends React.PureComponent<MonacoEditorProps> {
|
|
||||||
render(): JSX.Element {
|
|
||||||
// TODO: Show a little blocky placeholder
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TextFile extends React.PureComponent<TextFileProps, TextFileState> {
|
|
||||||
constructor(props: TextFileProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
Editor: EditorPlaceholder,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChange = (source: string) => {
|
|
||||||
this.props.handleChange(source);
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
import(/* webpackChunkName: "monaco-editor" */ "@nteract/monaco-editor").then((module) => {
|
|
||||||
this.setState({ Editor: module.default });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render(): JSX.Element {
|
|
||||||
const Editor = this.state.Editor;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EditorContainer className="nteract-editor" style={{ position: "static" }}>
|
|
||||||
<Editor
|
|
||||||
id={"no-cell-id-for-single-editor"}
|
|
||||||
contentRef={this.props.contentRef}
|
|
||||||
theme={this.props.theme === "dark" ? "vs-dark" : "vs"}
|
|
||||||
language={"plaintext"}
|
|
||||||
editorFocused
|
|
||||||
value={this.props.text}
|
|
||||||
onChange={this.handleChange.bind(this)}
|
|
||||||
/>
|
|
||||||
</EditorContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InitialProps {
|
|
||||||
contentRef: ContentRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeMapStateToTextFileProps(
|
|
||||||
initialState: AppState,
|
|
||||||
initialProps: InitialProps,
|
|
||||||
): (state: AppState) => MappedStateProps {
|
|
||||||
const { contentRef } = initialProps;
|
|
||||||
|
|
||||||
const mapStateToTextFileProps = (state: AppState) => {
|
|
||||||
const content = selectors.content(state, { contentRef });
|
|
||||||
if (!content || content.type !== "file") {
|
|
||||||
throw new Error("The text file component must have content");
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = content.model ? content.model.text : "";
|
|
||||||
|
|
||||||
return {
|
|
||||||
contentRef,
|
|
||||||
mimetype: content.mimetype !== undefined ? content.mimetype : "text/plain",
|
|
||||||
text,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return mapStateToTextFileProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeMapDispatchToTextFileProps = (
|
|
||||||
initialDispatch: Dispatch,
|
|
||||||
initialProps: InitialProps,
|
|
||||||
): ((dispatch: Dispatch) => MappedDispatchProps) => {
|
|
||||||
const { contentRef } = initialProps;
|
|
||||||
|
|
||||||
const mapDispatchToTextFileProps = (dispatch: Dispatch) => {
|
|
||||||
return {
|
|
||||||
handleChange: (source: string) => {
|
|
||||||
dispatch(
|
|
||||||
actions.updateFileText({
|
|
||||||
contentRef,
|
|
||||||
text: source,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return mapDispatchToTextFileProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedTextFile = connect<MappedStateProps, MappedDispatchProps, InitialProps, AppState>(
|
|
||||||
makeMapStateToTextFileProps,
|
|
||||||
makeMapDispatchToTextFileProps,
|
|
||||||
)(TextFile);
|
|
||||||
|
|
||||||
export function handles(mimetype: string) {
|
|
||||||
return (
|
|
||||||
!mimetype ||
|
|
||||||
StringUtils.startsWith(mimetype, "text/") ||
|
|
||||||
StringUtils.startsWith(mimetype, "application/javascript") ||
|
|
||||||
StringUtils.startsWith(mimetype, "application/json") ||
|
|
||||||
StringUtils.startsWith(mimetype, "application/x-ipynb+json")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ConnectedTextFile;
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
// Vendor modules
|
|
||||||
import { CellType, ImmutableNotebook } from "@nteract/commutable";
|
|
||||||
import { HeaderDataProps } from "@nteract/connected-components/lib/header-editor";
|
|
||||||
import {
|
|
||||||
AppState,
|
|
||||||
ContentRef,
|
|
||||||
HostRecord,
|
|
||||||
selectors,
|
|
||||||
actions,
|
|
||||||
DirectoryContentRecordProps,
|
|
||||||
DummyContentRecordProps,
|
|
||||||
FileContentRecordProps,
|
|
||||||
NotebookContentRecordProps,
|
|
||||||
} from "@nteract/core";
|
|
||||||
import { RecordOf } from "immutable";
|
|
||||||
import * as React from "react";
|
|
||||||
import { HotKeys, KeyMap } from "react-hotkeys";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
|
|
||||||
// Local modules
|
|
||||||
import { default as File } from "./file";
|
|
||||||
|
|
||||||
interface IContentsBaseProps {
|
|
||||||
contentRef: ContentRef;
|
|
||||||
error?: object | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IStateToProps {
|
|
||||||
headerData?: HeaderDataProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IDispatchFromProps {
|
|
||||||
handlers?: any;
|
|
||||||
onHeaderEditorChange?: (props: HeaderDataProps) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ContentsProps = IContentsBaseProps & IStateToProps & IDispatchFromProps;
|
|
||||||
|
|
||||||
class Contents extends React.PureComponent<ContentsProps> {
|
|
||||||
private keyMap: KeyMap = {
|
|
||||||
CHANGE_CELL_TYPE: ["ctrl+shift+y", "ctrl+shift+m", "meta+shift+y", "meta+shift+m"],
|
|
||||||
COPY_CELL: ["ctrl+shift+c", "meta+shift+c"],
|
|
||||||
CREATE_CELL_ABOVE: ["ctrl+shift+a", "meta+shift+a"],
|
|
||||||
CREATE_CELL_BELOW: ["ctrl+shift+b", "meta+shift+b"],
|
|
||||||
CUT_CELL: ["ctrl+shift+x", "meta+shift+x"],
|
|
||||||
DELETE_CELL: ["ctrl+shift+d", "meta+shift+d"],
|
|
||||||
EXECUTE_ALL_CELLS: ["alt+r a"],
|
|
||||||
INTERRUPT_KERNEL: ["alt+r i"],
|
|
||||||
KILL_KERNEL: ["alt+r k"],
|
|
||||||
OPEN: ["ctrl+o", "meta+o"],
|
|
||||||
PASTE_CELL: ["ctrl+shift+v"],
|
|
||||||
RESTART_KERNEL: ["alt+r r", "alt+r c", "alt+r a"],
|
|
||||||
SAVE: ["ctrl+s", "ctrl+shift+s", "meta+s", "meta+shift+s"],
|
|
||||||
};
|
|
||||||
|
|
||||||
render(): JSX.Element {
|
|
||||||
const { contentRef, handlers } = this.props;
|
|
||||||
|
|
||||||
if (!contentRef) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<HotKeys keyMap={this.keyMap} handlers={handlers} className="hotKeys">
|
|
||||||
<File contentRef={contentRef} />
|
|
||||||
</HotKeys>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeMapStateToProps: any = (initialState: AppState, initialProps: { contentRef: ContentRef }) => {
|
|
||||||
const host: HostRecord = initialState.app.host;
|
|
||||||
|
|
||||||
if (host.type !== "jupyter") {
|
|
||||||
throw new Error("this component only works with jupyter apps");
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState): Partial<ContentsProps> => {
|
|
||||||
const contentRef: ContentRef = initialProps.contentRef;
|
|
||||||
|
|
||||||
if (!contentRef) {
|
|
||||||
throw new Error("cant display without a contentRef");
|
|
||||||
}
|
|
||||||
|
|
||||||
const content:
|
|
||||||
| RecordOf<NotebookContentRecordProps>
|
|
||||||
| RecordOf<DummyContentRecordProps>
|
|
||||||
| RecordOf<FileContentRecordProps>
|
|
||||||
| RecordOf<DirectoryContentRecordProps>
|
|
||||||
| undefined = selectors.content(state, { contentRef });
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
return {
|
|
||||||
contentRef: undefined,
|
|
||||||
error: undefined,
|
|
||||||
headerData: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let headerData: HeaderDataProps = {
|
|
||||||
authors: [],
|
|
||||||
description: "",
|
|
||||||
tags: [],
|
|
||||||
title: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
// If a notebook, we need to read in the headerData if available
|
|
||||||
if (content.type === "notebook") {
|
|
||||||
const notebook: ImmutableNotebook = content.model.get("notebook");
|
|
||||||
const metadata: any = notebook.metadata.toJS();
|
|
||||||
const { authors = [], description = "", tags = [], title = "" } = metadata;
|
|
||||||
|
|
||||||
// Updates
|
|
||||||
headerData = Object.assign({}, headerData, {
|
|
||||||
authors,
|
|
||||||
description,
|
|
||||||
tags,
|
|
||||||
title,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
contentRef,
|
|
||||||
error: content.error,
|
|
||||||
headerData,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch, ownProps: ContentsProps): object => {
|
|
||||||
const { contentRef } = ownProps;
|
|
||||||
|
|
||||||
return {
|
|
||||||
onHeaderEditorChange: (props: HeaderDataProps) => {
|
|
||||||
return dispatch(
|
|
||||||
actions.overwriteMetadataFields({
|
|
||||||
...props,
|
|
||||||
contentRef: ownProps.contentRef,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
// `HotKeys` handlers object
|
|
||||||
// see: https://github.com/greena13/react-hotkeys#defining-handlers
|
|
||||||
handlers: {
|
|
||||||
CHANGE_CELL_TYPE: (event: KeyboardEvent) => {
|
|
||||||
const type: CellType = event.key === "Y" ? "code" : "markdown";
|
|
||||||
return dispatch(actions.changeCellType({ to: type, contentRef }));
|
|
||||||
},
|
|
||||||
COPY_CELL: () => dispatch(actions.copyCell({ contentRef })),
|
|
||||||
CREATE_CELL_ABOVE: () => dispatch(actions.createCellAbove({ cellType: "code", contentRef })),
|
|
||||||
CREATE_CELL_BELOW: () => dispatch(actions.createCellBelow({ cellType: "code", contentRef })),
|
|
||||||
CUT_CELL: () => dispatch(actions.cutCell({ contentRef })),
|
|
||||||
DELETE_CELL: () => dispatch(actions.deleteCell({ contentRef })),
|
|
||||||
EXECUTE_ALL_CELLS: () => dispatch(actions.executeAllCells({ contentRef })),
|
|
||||||
INTERRUPT_KERNEL: () => dispatch(actions.interruptKernel({})),
|
|
||||||
KILL_KERNEL: () => dispatch(actions.killKernel({ restarting: false })),
|
|
||||||
PASTE_CELL: () => dispatch(actions.pasteCell({ contentRef })),
|
|
||||||
RESTART_KERNEL: (event: KeyboardEvent) => {
|
|
||||||
const outputHandling: "None" | "Clear All" | "Run All" =
|
|
||||||
event.key === "r" ? "None" : event.key === "a" ? "Run All" : "Clear All";
|
|
||||||
return dispatch(actions.restartKernel({ outputHandling, contentRef }));
|
|
||||||
},
|
|
||||||
SAVE: () => dispatch(actions.save({ contentRef })),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, mapDispatchToProps)(Contents);
|
|
||||||
@@ -1,477 +0,0 @@
|
|||||||
import { makeNotebookRecord } from "@nteract/commutable";
|
|
||||||
import { actions, state } from "@nteract/core";
|
|
||||||
import * as Immutable from "immutable";
|
|
||||||
import { StateObservable } from "redux-observable";
|
|
||||||
import { Subject, of } from "rxjs";
|
|
||||||
import { toArray } from "rxjs/operators";
|
|
||||||
import * as sinon from "sinon";
|
|
||||||
|
|
||||||
import { NotebookUtil } from "../NotebookUtil";
|
|
||||||
import { launchWebSocketKernelEpic } from "./epics";
|
|
||||||
import { CdbAppState, makeCdbRecord } from "./types";
|
|
||||||
|
|
||||||
import { sessions } from "rx-jupyter";
|
|
||||||
|
|
||||||
describe("Extract kernel from notebook", () => {
|
|
||||||
it("Reads metadata kernelspec first", () => {
|
|
||||||
const fakeNotebook = makeNotebookRecord({
|
|
||||||
metadata: Immutable.Map({
|
|
||||||
kernelspec: {
|
|
||||||
display_name: "Python 3",
|
|
||||||
language: "python",
|
|
||||||
name: "python3",
|
|
||||||
},
|
|
||||||
language_info: {
|
|
||||||
name: "python",
|
|
||||||
version: "3.7.3",
|
|
||||||
mimetype: "text/x-python",
|
|
||||||
codemirror_mode: {
|
|
||||||
name: "ipython",
|
|
||||||
version: 3,
|
|
||||||
},
|
|
||||||
pygments_lexer: "ipython3",
|
|
||||||
nbconvert_exporter: "python",
|
|
||||||
file_extension: ".py",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = NotebookUtil.extractNewKernel("blah", fakeNotebook);
|
|
||||||
expect(result.kernelSpecName).toEqual("python3");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Reads language info in metadata if kernelspec not present", () => {
|
|
||||||
const fakeNotebook = makeNotebookRecord({
|
|
||||||
metadata: Immutable.Map({
|
|
||||||
language_info: {
|
|
||||||
name: "python",
|
|
||||||
version: "3.7.3",
|
|
||||||
mimetype: "text/x-python",
|
|
||||||
codemirror_mode: {
|
|
||||||
name: "ipython",
|
|
||||||
version: 3,
|
|
||||||
},
|
|
||||||
pygments_lexer: "ipython3",
|
|
||||||
nbconvert_exporter: "python",
|
|
||||||
file_extension: ".py",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = NotebookUtil.extractNewKernel("blah", fakeNotebook);
|
|
||||||
expect(result.kernelSpecName).toEqual("python");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Returns nothing if no kernelspec nor language info is found in metadata", () => {
|
|
||||||
const fakeNotebook = makeNotebookRecord({
|
|
||||||
metadata: Immutable.Map({
|
|
||||||
blah: "this should be ignored",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = NotebookUtil.extractNewKernel("blah", fakeNotebook);
|
|
||||||
expect(result.kernelSpecName).toEqual(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
app: state.makeAppRecord({
|
|
||||||
host: state.makeJupyterHostRecord({
|
|
||||||
type: "jupyter",
|
|
||||||
token: "eh",
|
|
||||||
basePath: "/",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
comms: state.makeCommsRecord(),
|
|
||||||
config: Immutable.Map({}),
|
|
||||||
core: state.makeStateRecord({
|
|
||||||
kernelRef: "fake",
|
|
||||||
entities: state.makeEntitiesRecord({
|
|
||||||
contents: state.makeContentsRecord({
|
|
||||||
byRef: Immutable.Map({
|
|
||||||
fakeContentRef: state.makeNotebookContentRecord(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
kernels: state.makeKernelsRecord({
|
|
||||||
byRef: Immutable.Map({
|
|
||||||
fake: state.makeRemoteKernelRecord({
|
|
||||||
type: "websocket",
|
|
||||||
channels: new Subject<any>(),
|
|
||||||
kernelSpecName: "fancy",
|
|
||||||
id: "0",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
cdb: makeCdbRecord({
|
|
||||||
databaseAccountName: "dbAccountName",
|
|
||||||
defaultExperience: "defaultExperience",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("launchWebSocketKernelEpic", () => {
|
|
||||||
const createSpy = sinon.spy(sessions, "create");
|
|
||||||
|
|
||||||
const contentRef = "fakeContentRef";
|
|
||||||
const kernelRef = "fake";
|
|
||||||
|
|
||||||
it("launches remote kernels", async () => {
|
|
||||||
const state$ = new StateObservable(new Subject<CdbAppState>() as any, initialState);
|
|
||||||
|
|
||||||
const cwd = "/";
|
|
||||||
const kernelId = "123";
|
|
||||||
const kernelSpecName = "kernelspecname";
|
|
||||||
const sessionId = "sessionId";
|
|
||||||
|
|
||||||
const action$ = of(
|
|
||||||
actions.launchKernelByName({
|
|
||||||
contentRef,
|
|
||||||
kernelRef,
|
|
||||||
kernelSpecName,
|
|
||||||
cwd,
|
|
||||||
selectNextKernel: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
(sessions as any).__setResponse({
|
|
||||||
originalEvent: undefined,
|
|
||||||
xhr: new XMLHttpRequest(),
|
|
||||||
request: null,
|
|
||||||
status: 200,
|
|
||||||
response: {
|
|
||||||
id: sessionId,
|
|
||||||
path: "notebooks/Untitled7.ipynb",
|
|
||||||
name: "",
|
|
||||||
type: "notebook",
|
|
||||||
kernel: {
|
|
||||||
id: kernelId,
|
|
||||||
name: "kernel_launched",
|
|
||||||
last_activity: "2019-11-07T14:29:54.432454Z",
|
|
||||||
execution_state: "starting",
|
|
||||||
connections: 0,
|
|
||||||
},
|
|
||||||
notebook: {
|
|
||||||
path: "notebooks/Untitled7.ipynb",
|
|
||||||
name: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
responseText: null,
|
|
||||||
responseType: "json",
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseActions = await launchWebSocketKernelEpic(action$, state$).pipe(toArray()).toPromise();
|
|
||||||
|
|
||||||
expect(responseActions).toMatchObject([
|
|
||||||
{
|
|
||||||
type: actions.LAUNCH_KERNEL_SUCCESSFUL,
|
|
||||||
payload: {
|
|
||||||
contentRef,
|
|
||||||
kernelRef,
|
|
||||||
selectNextKernel: true,
|
|
||||||
kernel: {
|
|
||||||
info: null,
|
|
||||||
sessionId: sessionId,
|
|
||||||
type: "websocket",
|
|
||||||
kernelSpecName,
|
|
||||||
cwd,
|
|
||||||
id: kernelId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("launches any kernel with no kernelspecs in the state", async () => {
|
|
||||||
const state$ = new StateObservable(new Subject<CdbAppState>() as any, initialState);
|
|
||||||
|
|
||||||
const cwd = "/";
|
|
||||||
const kernelId = "123";
|
|
||||||
const kernelSpecName = "kernelspecname";
|
|
||||||
const sessionId = "sessionId";
|
|
||||||
|
|
||||||
const action$ = of(
|
|
||||||
actions.launchKernelByName({
|
|
||||||
contentRef,
|
|
||||||
kernelRef,
|
|
||||||
kernelSpecName,
|
|
||||||
cwd,
|
|
||||||
selectNextKernel: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
(sessions as any).__setResponse({
|
|
||||||
originalEvent: undefined,
|
|
||||||
xhr: new XMLHttpRequest(),
|
|
||||||
request: null,
|
|
||||||
status: 200,
|
|
||||||
response: {
|
|
||||||
id: sessionId,
|
|
||||||
path: "notebooks/Untitled7.ipynb",
|
|
||||||
name: "",
|
|
||||||
type: "notebook",
|
|
||||||
kernel: {
|
|
||||||
id: kernelId,
|
|
||||||
name: "kernel_launched",
|
|
||||||
last_activity: "2019-11-07T14:29:54.432454Z",
|
|
||||||
execution_state: "starting",
|
|
||||||
connections: 0,
|
|
||||||
},
|
|
||||||
notebook: {
|
|
||||||
path: "notebooks/Untitled7.ipynb",
|
|
||||||
name: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
responseText: null,
|
|
||||||
responseType: "json",
|
|
||||||
});
|
|
||||||
|
|
||||||
await launchWebSocketKernelEpic(action$, state$).pipe(toArray()).toPromise();
|
|
||||||
|
|
||||||
expect(createSpy.lastCall.args[1]).toMatchObject({
|
|
||||||
kernel: {
|
|
||||||
name: kernelSpecName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("launches no kernel if no kernel is specified and state has no kernelspecs", async () => {
|
|
||||||
const state$ = new StateObservable(new Subject<CdbAppState>() as any, initialState);
|
|
||||||
|
|
||||||
const cwd = "/";
|
|
||||||
const kernelId = "123";
|
|
||||||
const kernelSpecName = "kernelspecname";
|
|
||||||
const sessionId = "sessionId";
|
|
||||||
|
|
||||||
const action$ = of(
|
|
||||||
actions.launchKernelByName({
|
|
||||||
contentRef,
|
|
||||||
kernelRef,
|
|
||||||
kernelSpecName: undefined,
|
|
||||||
cwd,
|
|
||||||
selectNextKernel: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
(sessions as any).__setResponse({
|
|
||||||
originalEvent: undefined,
|
|
||||||
xhr: new XMLHttpRequest(),
|
|
||||||
request: null,
|
|
||||||
status: 200,
|
|
||||||
response: {
|
|
||||||
id: sessionId,
|
|
||||||
path: "notebooks/Untitled7.ipynb",
|
|
||||||
name: "",
|
|
||||||
type: "notebook",
|
|
||||||
kernel: {
|
|
||||||
id: kernelId,
|
|
||||||
name: "kernel_launched",
|
|
||||||
last_activity: "2019-11-07T14:29:54.432454Z",
|
|
||||||
execution_state: "starting",
|
|
||||||
connections: 0,
|
|
||||||
},
|
|
||||||
notebook: {
|
|
||||||
path: "notebooks/Untitled7.ipynb",
|
|
||||||
name: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
responseText: null,
|
|
||||||
responseType: "json",
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseActions = await launchWebSocketKernelEpic(action$, state$).pipe(toArray()).toPromise();
|
|
||||||
|
|
||||||
expect(responseActions).toMatchObject([
|
|
||||||
{
|
|
||||||
type: actions.LAUNCH_KERNEL_FAILED,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("emits an error if backend returns an error", async () => {
|
|
||||||
const state$ = new StateObservable(new Subject<CdbAppState>() as any, initialState);
|
|
||||||
|
|
||||||
const cwd = "/";
|
|
||||||
const action$ = of(
|
|
||||||
actions.launchKernelByName({
|
|
||||||
contentRef,
|
|
||||||
kernelRef,
|
|
||||||
kernelSpecName: undefined,
|
|
||||||
cwd,
|
|
||||||
selectNextKernel: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
(sessions as any).__setResponse({
|
|
||||||
originalEvent: undefined,
|
|
||||||
xhr: new XMLHttpRequest(),
|
|
||||||
request: null,
|
|
||||||
status: 500,
|
|
||||||
response: null,
|
|
||||||
responseText: null,
|
|
||||||
responseType: "json",
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseActions = await launchWebSocketKernelEpic(action$, state$).pipe(toArray()).toPromise();
|
|
||||||
|
|
||||||
expect(responseActions).toMatchObject([
|
|
||||||
{
|
|
||||||
type: actions.LAUNCH_KERNEL_FAILED,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Choose correct kernelspecs to launch", () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
// Initialize kernelspecs with 2 supported kernels
|
|
||||||
const createKernelSpecsRecord = (): Immutable.RecordOf<state.KernelspecsRecordProps> =>
|
|
||||||
state.makeKernelspecsRecord({
|
|
||||||
byRef: Immutable.Map({
|
|
||||||
kernelspecsref: state.makeKernelspecsByRefRecord({
|
|
||||||
defaultKernelName: "kernel2",
|
|
||||||
byName: Immutable.Map({
|
|
||||||
kernel1: state.makeKernelspec({
|
|
||||||
name: "kernel1",
|
|
||||||
argv: Immutable.List([]),
|
|
||||||
env: Immutable.Map(),
|
|
||||||
interruptMode: "interruptMode1",
|
|
||||||
language: "language1",
|
|
||||||
displayName: "Kernel One",
|
|
||||||
metadata: Immutable.Map(),
|
|
||||||
resources: Immutable.Map(),
|
|
||||||
}),
|
|
||||||
kernel2: state.makeKernelspec({
|
|
||||||
name: "kernel2",
|
|
||||||
argv: Immutable.List([]),
|
|
||||||
env: Immutable.Map(),
|
|
||||||
interruptMode: "interruptMode2",
|
|
||||||
language: "language2",
|
|
||||||
displayName: "Kernel Two",
|
|
||||||
metadata: Immutable.Map(),
|
|
||||||
resources: Immutable.Map(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
refs: Immutable.List(["kernelspecsref"]),
|
|
||||||
});
|
|
||||||
initialState.core = initialState.core
|
|
||||||
.setIn(["entities", "kernelspecs"], createKernelSpecsRecord())
|
|
||||||
.set("currentKernelspecsRef", "kernelspecsref");
|
|
||||||
|
|
||||||
// some fake response we don't care about
|
|
||||||
(sessions as any).__setResponse({
|
|
||||||
originalEvent: undefined,
|
|
||||||
xhr: new XMLHttpRequest(),
|
|
||||||
request: null,
|
|
||||||
status: 200,
|
|
||||||
response: {
|
|
||||||
id: "sessionId",
|
|
||||||
path: "notebooks/Untitled7.ipynb",
|
|
||||||
name: "",
|
|
||||||
type: "notebook",
|
|
||||||
kernel: {
|
|
||||||
id: "kernelId",
|
|
||||||
name: "kernel_launched",
|
|
||||||
last_activity: "2019-11-07T14:29:54.432454Z",
|
|
||||||
execution_state: "starting",
|
|
||||||
connections: 0,
|
|
||||||
},
|
|
||||||
notebook: {
|
|
||||||
path: "notebooks/Untitled7.ipynb",
|
|
||||||
name: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
responseText: null,
|
|
||||||
responseType: "json",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("launches supported kernel in kernelspecs", async () => {
|
|
||||||
const state$ = new StateObservable(new Subject<CdbAppState>() as any, initialState);
|
|
||||||
|
|
||||||
const action$ = of(
|
|
||||||
actions.launchKernelByName({
|
|
||||||
contentRef,
|
|
||||||
kernelRef,
|
|
||||||
kernelSpecName: "kernel2",
|
|
||||||
cwd: "cwd",
|
|
||||||
selectNextKernel: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await launchWebSocketKernelEpic(action$, state$).pipe(toArray()).toPromise();
|
|
||||||
expect(createSpy.lastCall.args[1]).toMatchObject({
|
|
||||||
kernel: {
|
|
||||||
name: "kernel2",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("launches undefined kernel uses default kernel from kernelspecs", async () => {
|
|
||||||
const state$ = new StateObservable(new Subject<CdbAppState>() as any, initialState);
|
|
||||||
|
|
||||||
const action$ = of(
|
|
||||||
actions.launchKernelByName({
|
|
||||||
contentRef,
|
|
||||||
kernelRef,
|
|
||||||
kernelSpecName: undefined,
|
|
||||||
cwd: "cwd",
|
|
||||||
selectNextKernel: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await launchWebSocketKernelEpic(action$, state$).pipe(toArray()).toPromise();
|
|
||||||
|
|
||||||
expect(createSpy.lastCall.args[1]).toMatchObject({
|
|
||||||
kernel: {
|
|
||||||
name: "kernel2",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("launches unsupported kernel uses default kernel from kernelspecs", async () => {
|
|
||||||
const state$ = new StateObservable(new Subject<CdbAppState>() as any, initialState);
|
|
||||||
|
|
||||||
const action$ = of(
|
|
||||||
actions.launchKernelByName({
|
|
||||||
contentRef,
|
|
||||||
kernelRef,
|
|
||||||
kernelSpecName: "This is an unknown kernelspec",
|
|
||||||
cwd: "cwd",
|
|
||||||
selectNextKernel: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await launchWebSocketKernelEpic(action$, state$).pipe(toArray()).toPromise();
|
|
||||||
|
|
||||||
expect(createSpy.lastCall.args[1]).toMatchObject({
|
|
||||||
kernel: {
|
|
||||||
name: "kernel2",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("launches unsupported kernel uses kernelspecs with similar name", async () => {
|
|
||||||
const state$ = new StateObservable(new Subject<CdbAppState>() as any, initialState);
|
|
||||||
|
|
||||||
const action$ = of(
|
|
||||||
actions.launchKernelByName({
|
|
||||||
contentRef,
|
|
||||||
kernelRef,
|
|
||||||
kernelSpecName: "ernel1",
|
|
||||||
cwd: "cwd",
|
|
||||||
selectNextKernel: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await launchWebSocketKernelEpic(action$, state$).pipe(toArray()).toPromise();
|
|
||||||
|
|
||||||
expect(createSpy.lastCall.args[1]).toMatchObject({
|
|
||||||
kernel: {
|
|
||||||
name: "kernel1",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,35 +0,0 @@
|
|||||||
// This replicates transform loading from:
|
|
||||||
// https://github.com/nteract/nteract/blob/master/applications/jupyter-extension/nteract_on_jupyter/app/contents/notebook.tsx
|
|
||||||
|
|
||||||
export default (props: { addTransform: (component: any) => void }) => {
|
|
||||||
import(/* webpackChunkName: "plotly" */ "@nteract/transform-plotly").then((module) => {
|
|
||||||
props.addTransform(module.default);
|
|
||||||
props.addTransform(module.PlotlyNullTransform);
|
|
||||||
});
|
|
||||||
|
|
||||||
import(/* webpackChunkName: "tabular-dataresource" */ "@nteract/data-explorer").then((module) => {
|
|
||||||
props.addTransform(module.default);
|
|
||||||
});
|
|
||||||
|
|
||||||
import(/* webpackChunkName: "jupyter-widgets" */ "@nteract/jupyter-widgets").then((module) => {
|
|
||||||
props.addTransform(module.WidgetDisplay);
|
|
||||||
});
|
|
||||||
|
|
||||||
import("@nteract/transform-model-debug").then((module) => {
|
|
||||||
props.addTransform(module.default);
|
|
||||||
});
|
|
||||||
|
|
||||||
import(/* webpackChunkName: "vega-transform" */ "@nteract/transform-vega").then((module) => {
|
|
||||||
props.addTransform(module.VegaLite1);
|
|
||||||
props.addTransform(module.VegaLite2);
|
|
||||||
props.addTransform(module.VegaLite3);
|
|
||||||
props.addTransform(module.VegaLite4);
|
|
||||||
props.addTransform(module.Vega2);
|
|
||||||
props.addTransform(module.Vega3);
|
|
||||||
props.addTransform(module.Vega4);
|
|
||||||
props.addTransform(module.Vega5);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: The geojson transform will likely need some work because of the basemap URL(s)
|
|
||||||
// import GeoJSONTransform from "@nteract/transform-geojson";
|
|
||||||
};
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import { actions, CoreRecord, reducers as nteractReducers } from "@nteract/core";
|
|
||||||
import { Action } from "redux";
|
|
||||||
import * as cdbActions from "./actions";
|
|
||||||
import { CdbRecord } from "./types";
|
|
||||||
|
|
||||||
export const coreReducer = (state: CoreRecord, action: Action) => {
|
|
||||||
let typedAction;
|
|
||||||
switch (action.type) {
|
|
||||||
case cdbActions.CLOSE_NOTEBOOK: {
|
|
||||||
typedAction = action as cdbActions.CloseNotebookAction;
|
|
||||||
return state.setIn(
|
|
||||||
["entities", "contents", "byRef"],
|
|
||||||
state.entities.contents.byRef.delete(typedAction.payload.contentRef),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case actions.CHANGE_KERNEL_BY_NAME: {
|
|
||||||
// Update content metadata
|
|
||||||
typedAction = action as actions.ChangeKernelByName;
|
|
||||||
const kernelSpecName = typedAction.payload.kernelSpecName;
|
|
||||||
|
|
||||||
if (!state.currentKernelspecsRef) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentKernelspecs = state.entities.kernelspecs.byRef.get(state.currentKernelspecsRef);
|
|
||||||
if (!currentKernelspecs) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find kernelspecs by name
|
|
||||||
const kernelspecs = currentKernelspecs.byName.get(kernelSpecName);
|
|
||||||
if (!kernelspecs) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = [
|
|
||||||
"entities",
|
|
||||||
"contents",
|
|
||||||
"byRef",
|
|
||||||
typedAction.payload.contentRef,
|
|
||||||
"model",
|
|
||||||
"notebook",
|
|
||||||
"metadata",
|
|
||||||
"kernelspec",
|
|
||||||
];
|
|
||||||
// Update metadata
|
|
||||||
return state
|
|
||||||
.setIn(path.concat("name"), kernelspecs.name)
|
|
||||||
.setIn(path.concat("displayName"), kernelspecs.displayName)
|
|
||||||
.setIn(path.concat("language"), kernelspecs.language);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nteractReducers.core(state as any, action as any);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cdbReducer = (state: CdbRecord, action: Action) => {
|
|
||||||
if (!state) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (action.type) {
|
|
||||||
case cdbActions.UPDATE_KERNEL_RESTART_DELAY: {
|
|
||||||
const typedAction = action as cdbActions.UpdateKernelRestartDelayAction;
|
|
||||||
return state.set("kernelRestartDelayMs", typedAction.payload.delayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
case cdbActions.SET_HOVERED_CELL: {
|
|
||||||
const typedAction = action as cdbActions.SetHoveredCellAction;
|
|
||||||
return state.set("hoveredCellId", typedAction.payload.cellId);
|
|
||||||
}
|
|
||||||
|
|
||||||
case cdbActions.STORE_CELL_OUTPUT_SNAPSHOT: {
|
|
||||||
const typedAction = action as cdbActions.StoreCellOutputSnapshotAction;
|
|
||||||
state.cellOutputSnapshots.set(typedAction.payload.cellId, typedAction.payload.snapshot);
|
|
||||||
// TODO Simpler datastructure to instantiate new Map?
|
|
||||||
return state.set("cellOutputSnapshots", new Map(state.cellOutputSnapshots));
|
|
||||||
}
|
|
||||||
|
|
||||||
case cdbActions.STORE_NOTEBOOK_SNAPSHOT: {
|
|
||||||
const typedAction = action as cdbActions.StoreNotebookSnapshotAction;
|
|
||||||
// Clear pending request
|
|
||||||
return state.set("notebookSnapshot", typedAction.payload).set("pendingSnapshotRequest", undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
case cdbActions.TAKE_NOTEBOOK_SNAPSHOT: {
|
|
||||||
const typedAction = action as cdbActions.TakeNotebookSnapshotAction;
|
|
||||||
// Clear previous snapshots
|
|
||||||
return state
|
|
||||||
.set("cellOutputSnapshots", new Map())
|
|
||||||
.set("notebookSnapshot", undefined)
|
|
||||||
.set("notebookSnapshotError", undefined)
|
|
||||||
.set("pendingSnapshotRequest", typedAction.payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
case cdbActions.NOTEBOOK_SNAPSHOT_ERROR: {
|
|
||||||
const typedAction = action as cdbActions.NotebookSnapshotErrorAction;
|
|
||||||
return state.set("notebookSnapshotError", typedAction.payload.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { getCoreEpics } from "./store";
|
|
||||||
import { epics } from "@nteract/core";
|
|
||||||
|
|
||||||
describe("configure redux store", () => {
|
|
||||||
it("configures store with correct epic if based on autoStartKernelOnNotebookOpen", () => {
|
|
||||||
// For now, assume launchKernelWhenNotebookSetEpic is the last epic
|
|
||||||
let filteredEpics = getCoreEpics(true);
|
|
||||||
expect(filteredEpics.pop()).toEqual(epics.launchKernelWhenNotebookSetEpic);
|
|
||||||
|
|
||||||
filteredEpics = getCoreEpics(false);
|
|
||||||
expect(filteredEpics.pop()).not.toEqual(epics.launchKernelWhenNotebookSetEpic);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import { AppState, epics as coreEpics, IContentProvider, reducers } from "@nteract/core";
|
|
||||||
import { configuration } from "@nteract/mythic-configuration";
|
|
||||||
import { makeConfigureStore } from "@nteract/myths";
|
|
||||||
import { stringifyError } from "Common/stringifyError";
|
|
||||||
import { AnyAction, compose, Dispatch, Middleware, MiddlewareAPI, Store } from "redux";
|
|
||||||
import { Epic } from "redux-observable";
|
|
||||||
import { Observable } from "rxjs";
|
|
||||||
import { catchError } from "rxjs/operators";
|
|
||||||
import { allEpics } from "./epics";
|
|
||||||
import { cdbReducer, coreReducer } from "./reducers";
|
|
||||||
import { CdbAppState } from "./types";
|
|
||||||
|
|
||||||
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
|
||||||
|
|
||||||
export default function configureStore(
|
|
||||||
initialState: Partial<CdbAppState>,
|
|
||||||
contentProvider: IContentProvider,
|
|
||||||
onTraceFailure: (title: string, message: string) => void,
|
|
||||||
customMiddlewares?: Middleware<{}, any, Dispatch<AnyAction>>[],
|
|
||||||
autoStartKernelOnNotebookOpen?: boolean,
|
|
||||||
): Store<CdbAppState, AnyAction> {
|
|
||||||
/**
|
|
||||||
* Catches errors in reducers
|
|
||||||
*/
|
|
||||||
const catchErrorMiddleware: Middleware =
|
|
||||||
<D extends Dispatch<AnyAction>, S extends AppState>({ dispatch, getState }: MiddlewareAPI<D, S>) =>
|
|
||||||
(next: Dispatch<AnyAction>) =>
|
|
||||||
<A extends AnyAction>(action: A): any => {
|
|
||||||
try {
|
|
||||||
next(action);
|
|
||||||
} catch (error) {
|
|
||||||
traceFailure("Reducer failure", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const protect = (epic: Epic) => {
|
|
||||||
return (action$: Observable<any>, state$: any, dependencies: any) =>
|
|
||||||
epic(action$ as any, state$, dependencies).pipe(
|
|
||||||
catchError((error, caught) => {
|
|
||||||
traceFailure("Epic failure", error);
|
|
||||||
return caught;
|
|
||||||
}) as any,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const traceFailure = (title: string, error: any) => {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
onTraceFailure(title, `${error.message} ${stringifyError(error.stack)}`);
|
|
||||||
console.error(error);
|
|
||||||
} else {
|
|
||||||
onTraceFailure(title, error.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const protectEpics = (epics: Epic[]): Epic[] => {
|
|
||||||
return epics.map((epic) => protect(epic)) as any;
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredCoreEpics = getCoreEpics(autoStartKernelOnNotebookOpen);
|
|
||||||
|
|
||||||
const mythConfigureStore = makeConfigureStore<CdbAppState>()({
|
|
||||||
packages: [configuration],
|
|
||||||
reducers: {
|
|
||||||
app: reducers.app,
|
|
||||||
core: coreReducer as any,
|
|
||||||
cdb: cdbReducer,
|
|
||||||
},
|
|
||||||
epics: protectEpics([...filteredCoreEpics, ...allEpics] as any),
|
|
||||||
epicDependencies: { contentProvider },
|
|
||||||
epicMiddleware: customMiddlewares.concat(catchErrorMiddleware),
|
|
||||||
enhancer: composeEnhancers,
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = mythConfigureStore(initialState as any);
|
|
||||||
|
|
||||||
// TODO Fix typing issue here: createStore() output type doesn't quite match AppState
|
|
||||||
// return store as Store<AppState, AnyAction>;
|
|
||||||
return store as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getCoreEpics = (autoStartKernelOnNotebookOpen: boolean): Epic[] => {
|
|
||||||
// This list needs to be consistent and in sync with core.allEpics until we figure
|
|
||||||
// out how to safely filter out the ones we are overriding here.
|
|
||||||
const filteredCoreEpics = [
|
|
||||||
coreEpics.executeCellEpic,
|
|
||||||
coreEpics.executeFocusedCellEpic,
|
|
||||||
coreEpics.executeCellAfterKernelLaunchEpic,
|
|
||||||
coreEpics.sendExecuteRequestEpic,
|
|
||||||
coreEpics.updateDisplayEpic,
|
|
||||||
coreEpics.executeAllCellsEpic,
|
|
||||||
coreEpics.commListenEpic,
|
|
||||||
coreEpics.interruptKernelEpic,
|
|
||||||
coreEpics.lazyLaunchKernelEpic,
|
|
||||||
coreEpics.killKernelEpic,
|
|
||||||
coreEpics.watchExecutionStateEpic,
|
|
||||||
coreEpics.restartKernelEpic,
|
|
||||||
coreEpics.fetchKernelspecsEpic,
|
|
||||||
coreEpics.fetchContentEpic,
|
|
||||||
coreEpics.updateContentEpic,
|
|
||||||
coreEpics.saveContentEpic,
|
|
||||||
coreEpics.publishToBookstore,
|
|
||||||
coreEpics.publishToBookstoreAfterSave,
|
|
||||||
coreEpics.sendInputReplyEpic,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (autoStartKernelOnNotebookOpen) {
|
|
||||||
filteredCoreEpics.push(coreEpics.launchKernelWhenNotebookSetEpic);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredCoreEpics as any;
|
|
||||||
};
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import { CellId } from "@nteract/commutable";
|
|
||||||
import { AppState } from "@nteract/core";
|
|
||||||
import { MessageType } from "@nteract/messaging";
|
|
||||||
import * as Immutable from "immutable";
|
|
||||||
import { Notebook } from "../../../Common/Constants";
|
|
||||||
|
|
||||||
export interface SnapshotFragment {
|
|
||||||
image: HTMLImageElement;
|
|
||||||
boundingClientRect: DOMRect;
|
|
||||||
requestId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SnapshotRequest = NotebookSnapshotRequest | CellSnapshotRequest;
|
|
||||||
interface NotebookSnapshotRequestBase {
|
|
||||||
requestId: string;
|
|
||||||
aspectRatio: number;
|
|
||||||
notebookContentRef: string; // notebook redux contentRef
|
|
||||||
downloadFilename?: string; // Optional: will download as a file
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NotebookSnapshotRequest extends NotebookSnapshotRequestBase {
|
|
||||||
type: "notebook";
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CellSnapshotRequest extends NotebookSnapshotRequestBase {
|
|
||||||
type: "celloutput";
|
|
||||||
cellId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CdbRecordProps {
|
|
||||||
databaseAccountName: string | undefined;
|
|
||||||
defaultExperience: string | undefined;
|
|
||||||
kernelRestartDelayMs: number;
|
|
||||||
hoveredCellId: CellId | undefined;
|
|
||||||
cellOutputSnapshots: Map<string, SnapshotFragment>;
|
|
||||||
notebookSnapshot?: { imageSrc: string; requestId: string };
|
|
||||||
pendingSnapshotRequest?: SnapshotRequest;
|
|
||||||
notebookSnapshotError?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CdbRecord = Immutable.RecordOf<CdbRecordProps>;
|
|
||||||
|
|
||||||
export interface CdbAppState extends AppState {
|
|
||||||
cdb: CdbRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeCdbRecord = Immutable.Record<CdbRecordProps>({
|
|
||||||
databaseAccountName: undefined,
|
|
||||||
defaultExperience: undefined,
|
|
||||||
kernelRestartDelayMs: Notebook.kernelRestartInitialDelayMs,
|
|
||||||
hoveredCellId: undefined,
|
|
||||||
cellOutputSnapshots: new Map(),
|
|
||||||
notebookSnapshot: undefined,
|
|
||||||
pendingSnapshotRequest: undefined,
|
|
||||||
notebookSnapshotError: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface JupyterMessage<MT extends MessageType = MessageType, C = any> {
|
|
||||||
header: JupyterMessageHeader<MT>;
|
|
||||||
parent_header:
|
|
||||||
| JupyterMessageHeader<any>
|
|
||||||
| {
|
|
||||||
msg_id?: string;
|
|
||||||
};
|
|
||||||
metadata: object;
|
|
||||||
content: C;
|
|
||||||
channel: string;
|
|
||||||
buffers?: Uint8Array | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface JupyterMessageHeader<MT extends MessageType = MessageType> {
|
|
||||||
msg_id: string;
|
|
||||||
username: string;
|
|
||||||
date: string;
|
|
||||||
msg_type: MT;
|
|
||||||
version: string;
|
|
||||||
session: string;
|
|
||||||
token: string;
|
|
||||||
}
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
import { stringifyNotebook } from "@nteract/commutable";
|
|
||||||
import { FileType, IContent, IContentProvider, IEmptyContent, ServerConfig } from "@nteract/core";
|
|
||||||
import { cloneDeep } from "lodash";
|
|
||||||
import { AjaxResponse } from "rxjs/ajax";
|
|
||||||
import * as StringUtils from "../../Utils/StringUtils";
|
|
||||||
import * as FileSystemUtil from "./FileSystemUtil";
|
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
|
||||||
import { NotebookUtil } from "./NotebookUtil";
|
|
||||||
import { useNotebook } from "./useNotebook";
|
|
||||||
|
|
||||||
export class NotebookContentClient {
|
|
||||||
constructor(private contentProvider: IContentProvider) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This updates the item and points all the children's parent to this item
|
|
||||||
* @param item
|
|
||||||
*/
|
|
||||||
public async updateItemChildren(item: NotebookContentItem): Promise<NotebookContentItem> {
|
|
||||||
const subItems = await this.fetchNotebookFiles(item.path);
|
|
||||||
const clonedItem = cloneDeep(item);
|
|
||||||
subItems.forEach((subItem) => (subItem.parent = clonedItem));
|
|
||||||
clonedItem.children = subItems;
|
|
||||||
|
|
||||||
return clonedItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Delete this function when ResourceTreeAdapter is removed.
|
|
||||||
public async updateItemChildrenInPlace(item: NotebookContentItem): Promise<void> {
|
|
||||||
return this.fetchNotebookFiles(item.path).then((subItems) => {
|
|
||||||
item.children = subItems;
|
|
||||||
subItems.forEach((subItem) => (subItem.parent = item));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param parent parent folder
|
|
||||||
*/
|
|
||||||
public async createNewNotebookFile(
|
|
||||||
parent: NotebookContentItem,
|
|
||||||
isGithubTree?: boolean,
|
|
||||||
): Promise<NotebookContentItem> {
|
|
||||||
if (!parent || parent.type !== NotebookContentItemType.Directory) {
|
|
||||||
throw new Error(`Parent must be a directory: ${parent}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = "notebook";
|
|
||||||
return this.contentProvider
|
|
||||||
.create<"notebook">(this.getServerConfig(), parent.path, { type })
|
|
||||||
.toPromise()
|
|
||||||
.then((xhr: AjaxResponse) => {
|
|
||||||
if (typeof xhr.response === "string") {
|
|
||||||
throw new Error(`jupyter server response invalid: ${xhr.response}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (xhr.response.type !== type) {
|
|
||||||
throw new Error(`jupyter server response not for notebook: ${xhr.response}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const notebookFile = xhr.response;
|
|
||||||
|
|
||||||
const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type);
|
|
||||||
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
|
|
||||||
// TODO: delete when ResourceTreeAdapter is removed
|
|
||||||
if (parent.children) {
|
|
||||||
item.parent = parent;
|
|
||||||
parent.children.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async deleteContentItem(item: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
|
|
||||||
const path = await this.deleteNotebookFile(item.path);
|
|
||||||
useNotebook.getState().deleteNotebookItem(item, isGithubTree);
|
|
||||||
|
|
||||||
// TODO: Delete once old resource tree is removed
|
|
||||||
if (!path || path !== item.path) {
|
|
||||||
throw new Error("No path provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.parent && item.parent.children) {
|
|
||||||
// Remove deleted child
|
|
||||||
const newChildren = item.parent.children.filter((child) => child.path !== path);
|
|
||||||
item.parent.children = newChildren;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param name file name
|
|
||||||
* @param content file content string
|
|
||||||
* @param parent parent folder
|
|
||||||
*/
|
|
||||||
public async uploadFileAsync(
|
|
||||||
name: string,
|
|
||||||
content: string,
|
|
||||||
parent: NotebookContentItem,
|
|
||||||
isGithubTree?: boolean,
|
|
||||||
): Promise<NotebookContentItem> {
|
|
||||||
if (!parent || parent.type !== NotebookContentItemType.Directory) {
|
|
||||||
throw new Error(`Parent must be a directory: ${parent}`);
|
|
||||||
}
|
|
||||||
const filepath = NotebookUtil.getFilePath(parent.path, name);
|
|
||||||
if (await this.checkIfFilepathExists(filepath)) {
|
|
||||||
throw new Error(`File already exists: ${filepath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const model: Partial<IContent<"file">> = {
|
|
||||||
content,
|
|
||||||
format: "text",
|
|
||||||
name,
|
|
||||||
type: "file",
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.contentProvider
|
|
||||||
.save(this.getServerConfig(), filepath, model)
|
|
||||||
.toPromise()
|
|
||||||
.then((xhr: AjaxResponse) => {
|
|
||||||
const notebookFile = xhr.response;
|
|
||||||
const item = NotebookUtil.createNotebookContentItem(notebookFile.name, notebookFile.path, notebookFile.type);
|
|
||||||
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
|
|
||||||
// TODO: delete when ResourceTreeAdapter is removed
|
|
||||||
if (parent.children) {
|
|
||||||
item.parent = parent;
|
|
||||||
parent.children.push(item);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkIfFilepathExists(filepath: string): Promise<boolean> {
|
|
||||||
const parentDirPath = NotebookUtil.getParentPath(filepath);
|
|
||||||
if (parentDirPath) {
|
|
||||||
const items = await this.fetchNotebookFiles(parentDirPath);
|
|
||||||
return items.some((value) => FileSystemUtil.isPathEqual(value.path, filepath));
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param sourcePath
|
|
||||||
* @param targetName is not prefixed with path
|
|
||||||
*/
|
|
||||||
public renameNotebook(
|
|
||||||
item: NotebookContentItem,
|
|
||||||
targetName: string,
|
|
||||||
isGithubTree?: boolean,
|
|
||||||
): Promise<NotebookContentItem> {
|
|
||||||
const sourcePath = item.path;
|
|
||||||
// Match extension
|
|
||||||
if (sourcePath.indexOf(".") !== -1) {
|
|
||||||
const extension = `.${sourcePath.split(".").pop()}`;
|
|
||||||
if (!StringUtils.endsWith(targetName, extension)) {
|
|
||||||
targetName += extension;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const targetPath = NotebookUtil.replaceName(sourcePath, targetName);
|
|
||||||
return this.contentProvider
|
|
||||||
.update<"file" | "notebook" | "directory">(this.getServerConfig(), sourcePath, { path: targetPath })
|
|
||||||
.toPromise()
|
|
||||||
.then((xhr) => {
|
|
||||||
if (typeof xhr.response === "string") {
|
|
||||||
throw new Error(`jupyter server response invalid: ${xhr.response}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (xhr.response.type !== "file" && xhr.response.type !== "notebook" && xhr.response.type !== "directory") {
|
|
||||||
throw new Error(`jupyter server response not for notebook/file/directory: ${xhr.response}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const notebookFile = xhr.response;
|
|
||||||
item.name = notebookFile.name;
|
|
||||||
item.path = notebookFile.path;
|
|
||||||
item.timestamp = NotebookUtil.getCurrentTimestamp();
|
|
||||||
|
|
||||||
useNotebook.getState().updateNotebookItem(item, isGithubTree);
|
|
||||||
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param parent
|
|
||||||
* @param newDirectoryName basename of the new directory
|
|
||||||
*/
|
|
||||||
public async createDirectory(
|
|
||||||
parent: NotebookContentItem,
|
|
||||||
newDirectoryName: string,
|
|
||||||
isGithubTree?: boolean,
|
|
||||||
): Promise<NotebookContentItem> {
|
|
||||||
if (parent.type !== NotebookContentItemType.Directory) {
|
|
||||||
throw new Error(`Parent is not a directory: ${parent.path}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetPath = `${parent.path}/${newDirectoryName}`;
|
|
||||||
|
|
||||||
// Reject if already exists
|
|
||||||
if (await this.checkIfFilepathExists(targetPath)) {
|
|
||||||
throw new Error(`Directory already exists: ${targetPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = "directory";
|
|
||||||
return this.contentProvider
|
|
||||||
.save<"directory">(this.getServerConfig(), targetPath, { type, path: targetPath })
|
|
||||||
.toPromise()
|
|
||||||
.then((xhr: AjaxResponse) => {
|
|
||||||
if (typeof xhr.response === "string") {
|
|
||||||
throw new Error(`jupyter server response invalid: ${xhr.response}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (xhr.response.type !== type) {
|
|
||||||
throw new Error(`jupyter server response not for creating directory: ${xhr.response}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dir = xhr.response;
|
|
||||||
const item = NotebookUtil.createNotebookContentItem(dir.name, dir.path, dir.type);
|
|
||||||
useNotebook.getState().insertNotebookItem(parent, cloneDeep(item), isGithubTree);
|
|
||||||
// TODO: delete when ResourceTreeAdapter is removed
|
|
||||||
item.parent = parent;
|
|
||||||
parent.children?.push(item);
|
|
||||||
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async readFileContent(filePath: string): Promise<string> {
|
|
||||||
const xhr = await this.contentProvider.get(this.getServerConfig(), filePath, { content: 1 }).toPromise();
|
|
||||||
const content = (xhr.response as any).content;
|
|
||||||
if (!content) {
|
|
||||||
throw new Error("No content read");
|
|
||||||
}
|
|
||||||
|
|
||||||
const format = (xhr.response as any).format;
|
|
||||||
switch (format) {
|
|
||||||
case "text":
|
|
||||||
return content;
|
|
||||||
case "base64":
|
|
||||||
return atob(content);
|
|
||||||
case "json":
|
|
||||||
return stringifyNotebook(content);
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported content format ${format}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private deleteNotebookFile(path: string): Promise<string> {
|
|
||||||
return this.contentProvider
|
|
||||||
.remove(this.getServerConfig(), path)
|
|
||||||
.toPromise()
|
|
||||||
.then(() => path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert rx-jupyter type to our type
|
|
||||||
* @param type
|
|
||||||
*/
|
|
||||||
public static getType(type: FileType): NotebookContentItemType {
|
|
||||||
switch (type) {
|
|
||||||
case "directory":
|
|
||||||
return NotebookContentItemType.Directory;
|
|
||||||
case "notebook":
|
|
||||||
return NotebookContentItemType.Notebook;
|
|
||||||
case "file":
|
|
||||||
return NotebookContentItemType.File;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown file type: ${type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fetchNotebookFiles(path: string): Promise<NotebookContentItem[]> {
|
|
||||||
return this.contentProvider
|
|
||||||
.get(this.getServerConfig(), path, {
|
|
||||||
type: "directory",
|
|
||||||
})
|
|
||||||
.toPromise()
|
|
||||||
.then((xhr) => {
|
|
||||||
if (xhr.status !== 200) {
|
|
||||||
throw new Error(JSON.stringify(xhr.response));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof xhr.response === "string") {
|
|
||||||
throw new Error(`jupyter server response invalid: ${xhr.response}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (xhr.response.type !== "directory") {
|
|
||||||
throw new Error(`jupyter server response not for directory: ${xhr.response}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const list = xhr.response.content as IEmptyContent<FileType>[];
|
|
||||||
return list.map(
|
|
||||||
(item: IEmptyContent<FileType>): NotebookContentItem => ({
|
|
||||||
name: item.name,
|
|
||||||
path: item.path,
|
|
||||||
type: NotebookUtil.getType(item.type),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getServerConfig(): ServerConfig {
|
|
||||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
|
||||||
return {
|
|
||||||
endpoint: notebookServerInfo?.notebookServerEndpoint,
|
|
||||||
token: notebookServerInfo?.authToken,
|
|
||||||
crossDomain: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,37 +2,22 @@
|
|||||||
* Contains all notebook related stuff meant to be dynamically loaded by explorer
|
* Contains all notebook related stuff meant to be dynamically loaded by explorer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ImmutableNotebook } from "@nteract/commutable";
|
|
||||||
import type { IContentProvider } from "@nteract/core";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { contents } from "rx-jupyter";
|
import { HttpStatusCodes } from "../../Common/Constants";
|
||||||
import { Areas, HttpStatusCodes } from "../../Common/Constants";
|
|
||||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import { GitHubClient } from "../../GitHub/GitHubClient";
|
import { GitHubClient } from "../../GitHub/GitHubClient";
|
||||||
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
|
|
||||||
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
|
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
|
||||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||||
import { JunoClient } from "../../Juno/JunoClient";
|
import { JunoClient } from "../../Juno/JunoClient";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { useDialog } from "../Controls/Dialog";
|
import { useDialog } from "../Controls/Dialog";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
|
|
||||||
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||||
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||||
import { InMemoryContentProvider } from "./NotebookComponent/ContentProviders/InMemoryContentProvider";
|
|
||||||
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
|
|
||||||
import { NotebookContainerClient } from "./NotebookContainerClient";
|
import { NotebookContainerClient } from "./NotebookContainerClient";
|
||||||
import { NotebookContentClient } from "./NotebookContentClient";
|
|
||||||
import { SchemaAnalyzerNotebook } from "./SchemaAnalyzer/SchemaAnalyzerUtils";
|
|
||||||
import { useNotebook } from "./useNotebook";
|
import { useNotebook } from "./useNotebook";
|
||||||
|
|
||||||
type NotebookPaneContent = string | ImmutableNotebook;
|
|
||||||
|
|
||||||
export type { NotebookPaneContent };
|
|
||||||
|
|
||||||
export interface NotebookManagerOptions {
|
export interface NotebookManagerOptions {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
resourceTree: ResourceTreeAdapter;
|
resourceTree: ResourceTreeAdapter;
|
||||||
@@ -44,12 +29,8 @@ export default class NotebookManager {
|
|||||||
private params: NotebookManagerOptions;
|
private params: NotebookManagerOptions;
|
||||||
public junoClient: JunoClient;
|
public junoClient: JunoClient;
|
||||||
|
|
||||||
public notebookContentProvider: IContentProvider;
|
|
||||||
public notebookClient: NotebookContainerClient;
|
public notebookClient: NotebookContainerClient;
|
||||||
public notebookContentClient: NotebookContentClient;
|
|
||||||
|
|
||||||
private inMemoryContentProvider: InMemoryContentProvider;
|
|
||||||
private gitHubContentProvider: GitHubContentProvider;
|
|
||||||
public gitHubOAuthService: GitHubOAuthService;
|
public gitHubOAuthService: GitHubOAuthService;
|
||||||
public gitHubClient: GitHubClient;
|
public gitHubClient: GitHubClient;
|
||||||
|
|
||||||
@@ -60,30 +41,10 @@ export default class NotebookManager {
|
|||||||
this.gitHubOAuthService = new GitHubOAuthService(this.junoClient);
|
this.gitHubOAuthService = new GitHubOAuthService(this.junoClient);
|
||||||
this.gitHubClient = new GitHubClient(this.onGitHubClientError);
|
this.gitHubClient = new GitHubClient(this.onGitHubClientError);
|
||||||
|
|
||||||
this.inMemoryContentProvider = new InMemoryContentProvider({
|
|
||||||
[SchemaAnalyzerNotebook.path]: {
|
|
||||||
readonly: true,
|
|
||||||
content: SchemaAnalyzerNotebook,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.gitHubContentProvider = new GitHubContentProvider({
|
|
||||||
gitHubClient: this.gitHubClient,
|
|
||||||
promptForCommitMsg: this.promptForCommitMsg,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.notebookContentProvider = new NotebookContentProvider(
|
|
||||||
this.inMemoryContentProvider,
|
|
||||||
this.gitHubContentProvider,
|
|
||||||
contents.JupyterContentProvider,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.notebookClient = new NotebookContainerClient(() =>
|
this.notebookClient = new NotebookContainerClient(() =>
|
||||||
this.params.container.initNotebooks(userContext?.databaseAccount),
|
this.params.container.initNotebooks(userContext?.databaseAccount),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider);
|
|
||||||
|
|
||||||
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
|
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
|
||||||
this.gitHubClient.setToken(token?.access_token);
|
this.gitHubClient.setToken(token?.access_token);
|
||||||
if (this?.gitHubOAuthService.isLoggedIn()) {
|
if (this?.gitHubOAuthService.isLoggedIn()) {
|
||||||
@@ -121,22 +82,6 @@ export default class NotebookManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public openCopyNotebookPane(name: string, content: string): void {
|
|
||||||
const { container } = this.params;
|
|
||||||
useSidePanel
|
|
||||||
.getState()
|
|
||||||
.openSidePanel(
|
|
||||||
"Copy Notebook",
|
|
||||||
<CopyNotebookPane
|
|
||||||
container={container}
|
|
||||||
junoClient={this.junoClient}
|
|
||||||
gitHubOAuthService={this.gitHubOAuthService}
|
|
||||||
name={name}
|
|
||||||
content={content}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Octokit's error handler uses any
|
// Octokit's error handler uses any
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
private onGitHubClientError = (error: any): void => {
|
private onGitHubClientError = (error: any): void => {
|
||||||
@@ -167,36 +112,4 @@ export default class NotebookManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private promptForCommitMsg = (title: string, primaryButtonLabel: string) => {
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
|
||||||
let commitMsg = "Committed from Azure Cosmos DB Notebooks";
|
|
||||||
useDialog.getState().showOkCancelModalDialog(
|
|
||||||
title || "Commit",
|
|
||||||
undefined,
|
|
||||||
primaryButtonLabel || "Commit",
|
|
||||||
() => {
|
|
||||||
TelemetryProcessor.trace(Action.NotebooksGitHubCommit, ActionModifiers.Mark, {
|
|
||||||
dataExplorerArea: Areas.Notebook,
|
|
||||||
});
|
|
||||||
resolve(commitMsg);
|
|
||||||
},
|
|
||||||
"Cancel",
|
|
||||||
() => reject(new Error("Commit dialog canceled")),
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
label: "Commit message",
|
|
||||||
autoAdjustHeight: true,
|
|
||||||
multiline: true,
|
|
||||||
defaultValue: commitMsg,
|
|
||||||
rows: 3,
|
|
||||||
onChange: (_: unknown, newValue: string) => {
|
|
||||||
commitMsg = newValue;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
!commitMsg,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
import { createGlobalStyle } from "styled-components";
|
|
||||||
|
|
||||||
const AzureTheme = createGlobalStyle`
|
|
||||||
:root {
|
|
||||||
/* --theme-primary-bg-hover: #0078d4;
|
|
||||||
--theme-primary-bg-focus: #0078d4;
|
|
||||||
--theme-primary-shadow-hover: #0078d4; */
|
|
||||||
|
|
||||||
--theme-app-bg: white;
|
|
||||||
--theme-app-fg: var(--nt-color-midnight);
|
|
||||||
--theme-app-border: var(--nt-color-grey-light);
|
|
||||||
|
|
||||||
--theme-primary-bg: var(--nt-color-grey-lightest);
|
|
||||||
--theme-primary-bg-hover: var(--nt-color-grey-lighter);
|
|
||||||
--theme-primary-bg-focus: var(--nt-color-grey-light);
|
|
||||||
|
|
||||||
--theme-primary-fg: var(--nt-color-midnight-light);
|
|
||||||
--theme-primary-fg-hover: var(--nt-color-midnight);
|
|
||||||
--theme-primary-fg-focus: var(--theme-app-fg);
|
|
||||||
|
|
||||||
--theme-secondary-bg: var(--theme-primary-bg);
|
|
||||||
--theme-secondary-bg-hover: var(--theme-primary-bg-hover);
|
|
||||||
--theme-secondary-bg-focus: var(--theme-primary-bg-focus);
|
|
||||||
|
|
||||||
--theme-secondary-fg: var(--nt-color-midnight-lighter);
|
|
||||||
--theme-secondary-fg-hover: var(--nt-color-midnight-light);
|
|
||||||
--theme-secondary-fg-focus: var(--theme-primary-fg);
|
|
||||||
|
|
||||||
/* --theme-primary-shadow-hover: 0px 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
--theme-primary-shadow-focus: 0px 2px 4px rgba(0, 0, 0, 0.1); */
|
|
||||||
|
|
||||||
--theme-title-bar-bg: var(--theme-primary-bg-hover);
|
|
||||||
|
|
||||||
--theme-menu-bg: var(--theme-primary-bg);
|
|
||||||
--theme-menu-bg-hover: var(--theme-primary-bg-hover);
|
|
||||||
--theme-menu-bg-focus: var(--theme-primary-bg-focus);
|
|
||||||
/* --theme-menu-shadow: var(--theme-primary-shadow-hover); */
|
|
||||||
|
|
||||||
--theme-menu-fg: var(--theme-app-fg);
|
|
||||||
--theme-menu-fg-hover: var(--theme-app-fg);
|
|
||||||
--theme-menu-fg-focus: var(--theme-app-fg);
|
|
||||||
|
|
||||||
--theme-cell-bg: var(--theme-app-bg);
|
|
||||||
/* --theme-cell-shadow-hover: var(--theme-primary-shadow-hover); */
|
|
||||||
/* --theme-cell-shadow-focus: var(--theme-primary-shadow-focus); */
|
|
||||||
|
|
||||||
--theme-cell-prompt-bg: var(--theme-primary-bg);
|
|
||||||
--theme-cell-prompt-bg-hover: var(--theme-primary-bg-hover);
|
|
||||||
--theme-cell-prompt-bg-focus: var(--theme-primary-bg-focus);
|
|
||||||
|
|
||||||
--theme-cell-prompt-fg: var(--theme-secondary-fg);
|
|
||||||
--theme-cell-prompt-fg-hover: var(--theme-secondary-fg-hover);
|
|
||||||
--theme-cell-prompt-fg-focus: var(--theme-secondary-fg-focus);
|
|
||||||
|
|
||||||
--theme-cell-toolbar-bg: var(--theme-primary-bg);
|
|
||||||
--theme-cell-toolbar-bg-hover: var(--theme-primary-bg-hover);
|
|
||||||
--theme-cell-toolbar-bg-focus: var(--theme-primary-bg-focus);
|
|
||||||
|
|
||||||
--theme-cell-toolbar-fg: var(--theme-secondary-fg);
|
|
||||||
--theme-cell-toolbar-fg-hover: var(--theme-secondary-fg-hover);
|
|
||||||
--theme-cell-toolbar-fg-focus: var(--theme-secondary-fg-focus);
|
|
||||||
|
|
||||||
--theme-cell-menu-bg: var(--theme-primary-bg);
|
|
||||||
--theme-cell-menu-bg-hover: var(--theme-primary-bg-hover);
|
|
||||||
--theme-cell-menu-bg-focus: var(--theme-primary-bg-focus);
|
|
||||||
|
|
||||||
--theme-cell-menu-fg: var(--theme-primary-fg);
|
|
||||||
--theme-cell-menu-fg-hover: var(--theme-primary-fg-hover);
|
|
||||||
--theme-cell-menu-fg-focus: var(--theme-primary-fg-focus);
|
|
||||||
|
|
||||||
--theme-cell-input-bg: var(--theme-secondary-bg);
|
|
||||||
--theme-cell-input-fg: var(--theme-app-fg);
|
|
||||||
|
|
||||||
--theme-cell-output-bg: var(--theme-app-bg);
|
|
||||||
--theme-cell-output-fg: var(--theme-primary-fg);
|
|
||||||
|
|
||||||
--theme-cell-creator-bg: var(--theme-app-bg);
|
|
||||||
|
|
||||||
--theme-cell-creator-fg: var(--theme-secondary-fg);
|
|
||||||
--theme-cell-creator-fg-hover: var(--theme-secondary-fg-hover);
|
|
||||||
--theme-cell-creator-fg-focus: var(--theme-secondary-fg-focus);
|
|
||||||
|
|
||||||
--theme-pager-bg: #fafafa;
|
|
||||||
|
|
||||||
--cm-background: #fafafa;
|
|
||||||
--cm-color: black;
|
|
||||||
|
|
||||||
--cm-gutter-bg: white;
|
|
||||||
|
|
||||||
--cm-comment: #a86;
|
|
||||||
--cm-keyword: blue;
|
|
||||||
--cm-string: #a22;
|
|
||||||
--cm-builtin: #077;
|
|
||||||
--cm-special: #0aa;
|
|
||||||
--cm-variable: black;
|
|
||||||
--cm-number: #3a3;
|
|
||||||
--cm-meta: #555;
|
|
||||||
--cm-link: #3a3;
|
|
||||||
--cm-operator: black;
|
|
||||||
--cm-def: black;
|
|
||||||
|
|
||||||
--cm-activeline-bg: #e8f2ff;
|
|
||||||
--cm-matchingbracket-outline: grey;
|
|
||||||
--cm-matchingbracket-color: black;
|
|
||||||
|
|
||||||
--cm-hint-color: var(--cm-color);
|
|
||||||
--cm-hint-color-active: var(--cm-color);
|
|
||||||
--cm-hint-bg: var(--theme-app-bg);
|
|
||||||
--cm-hint-bg-active: #abd1ff;
|
|
||||||
|
|
||||||
--status-bar: #eeedee;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export { AzureTheme };
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
.NotebookReadOnlyRender {
|
|
||||||
.nteract-cell-container {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell {
|
|
||||||
padding: 0.5px;
|
|
||||||
border: 1px solid #ffffff;
|
|
||||||
border-left: 3px solid #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-scroll {
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-lines {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror {
|
|
||||||
height: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-scroll,
|
|
||||||
.CodeMirror-linenumber,
|
|
||||||
.CodeMirror-gutters {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell:hover {
|
|
||||||
border: 1px solid #0078d4;
|
|
||||||
border-left: 3px solid #0078d4;
|
|
||||||
|
|
||||||
.CodeMirror-scroll,
|
|
||||||
.CodeMirror-linenumber,
|
|
||||||
.CodeMirror-gutters {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs {
|
|
||||||
border-top: 1px solid #d7d7d7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-md-cell {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs {
|
|
||||||
padding: 10px;
|
|
||||||
border-top: 1px solid #ffffff;
|
|
||||||
|
|
||||||
pre {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border: none;
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-md-cell {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell:hover.nteract-md-cell {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { actions, ContentRef } from "@nteract/core";
|
|
||||||
import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
|
|
||||||
import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror";
|
|
||||||
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
|
|
||||||
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
|
|
||||||
import * as React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
import { userContext } from "../../../UserContext";
|
|
||||||
import loadTransform from "../NotebookComponent/loadTransform";
|
|
||||||
import { AzureTheme } from "./AzureTheme";
|
|
||||||
import "./base.css";
|
|
||||||
import "./default.css";
|
|
||||||
import MarkdownCell from "./markdown-cell";
|
|
||||||
import "./NotebookReadOnlyRenderer.less";
|
|
||||||
import SandboxOutputs from "./outputs/SandboxOutputs";
|
|
||||||
|
|
||||||
export interface NotebookRendererProps {
|
|
||||||
contentRef: ContentRef;
|
|
||||||
hideInputs?: boolean;
|
|
||||||
hidePrompts?: boolean;
|
|
||||||
addTransform: (component: React.ComponentType & { MIMETYPE: string }) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the class that uses nteract to render a read-only notebook.
|
|
||||||
*/
|
|
||||||
class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
|
||||||
componentDidMount() {
|
|
||||||
if (!userContext.features.sandboxNotebookOutputs) {
|
|
||||||
loadTransform(this.props as NotebookRendererProps);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderPrompt(id: string, contentRef: string): JSX.Element {
|
|
||||||
if (this.props.hidePrompts) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Prompt id={id} contentRef={contentRef}>
|
|
||||||
{(props: PassedPromptProps) => {
|
|
||||||
if (props.status === "busy") {
|
|
||||||
return <React.Fragment>{"[*]"}</React.Fragment>;
|
|
||||||
}
|
|
||||||
if (props.status === "queued") {
|
|
||||||
return <React.Fragment>{"[…]"}</React.Fragment>;
|
|
||||||
}
|
|
||||||
if (typeof props.executionCount === "number") {
|
|
||||||
return <React.Fragment>{`[${props.executionCount}]`}</React.Fragment>;
|
|
||||||
}
|
|
||||||
return <React.Fragment>{"[ ]"}</React.Fragment>;
|
|
||||||
}}
|
|
||||||
</Prompt>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<div className="NotebookReadOnlyRender">
|
|
||||||
<Cells contentRef={this.props.contentRef}>
|
|
||||||
{{
|
|
||||||
code: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
|
|
||||||
<CodeCell id={id} contentRef={contentRef}>
|
|
||||||
{{
|
|
||||||
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
|
|
||||||
outputs: userContext.features.sandboxNotebookOutputs
|
|
||||||
? () => <SandboxOutputs id={id} contentRef={contentRef} />
|
|
||||||
: undefined,
|
|
||||||
editor: {
|
|
||||||
codemirror: (props: PassedEditorProps) =>
|
|
||||||
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} editorType="codemirror" />,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
</CodeCell>
|
|
||||||
),
|
|
||||||
markdown: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
|
|
||||||
<MarkdownCell id={id} contentRef={contentRef} cell_type="markdown">
|
|
||||||
{{
|
|
||||||
editor: {},
|
|
||||||
}}
|
|
||||||
</MarkdownCell>
|
|
||||||
),
|
|
||||||
raw: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
|
|
||||||
<RawCell id={id} contentRef={contentRef} cell_type="raw">
|
|
||||||
{{
|
|
||||||
editor: {
|
|
||||||
codemirror: (props: PassedEditorProps) =>
|
|
||||||
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} editorType="codemirror" />,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
</RawCell>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
</Cells>
|
|
||||||
<AzureTheme />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererProps) => {
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => {
|
|
||||||
return {
|
|
||||||
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
|
|
||||||
return dispatch(
|
|
||||||
actions.addTransform({
|
|
||||||
mediaType: transform.MIMETYPE,
|
|
||||||
component: transform,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return mapDispatchToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(undefined, makeMapDispatchToProps)(NotebookReadOnlyRenderer);
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
// CommandBar
|
|
||||||
@HoverColor: #d7d7d7;
|
|
||||||
@HighlightColor: #0078d4;
|
|
||||||
|
|
||||||
.NotebookRendererContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.NotebookRenderer {
|
|
||||||
overflow: auto;
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
.nteract-cells {
|
|
||||||
padding-top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-container {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
|
|
||||||
.nteract-cell {
|
|
||||||
padding: 0.5px;
|
|
||||||
border: 1px solid #ffffff;
|
|
||||||
border-left: 3px solid #ffffff;
|
|
||||||
|
|
||||||
.CellContextMenuButton {
|
|
||||||
position: sticky;
|
|
||||||
z-index: 1;
|
|
||||||
top: 0px;
|
|
||||||
right: 0px;
|
|
||||||
margin: 0px 0px 0px -100%;
|
|
||||||
float: right;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-scroll {
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-scroll,
|
|
||||||
.CodeMirror-linenumber,
|
|
||||||
.CodeMirror-gutters {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror {
|
|
||||||
height: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell:hover {
|
|
||||||
border: 1px solid @HoverColor;
|
|
||||||
border-left: 3px solid @HoverColor;
|
|
||||||
|
|
||||||
.CellContextMenuButton {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-container.selected {
|
|
||||||
.nteract-cell {
|
|
||||||
border: 1px solid @HighlightColor;
|
|
||||||
border-left: 3px solid @HighlightColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// White background when hovered or selected
|
|
||||||
.nteract-cell:hover,
|
|
||||||
.nteract-cell-container.selected .nteract-cell {
|
|
||||||
.CodeMirror-scroll,
|
|
||||||
.CodeMirror-linenumber,
|
|
||||||
.CodeMirror-gutters {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-linenumber {
|
|
||||||
color: #015cda;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs {
|
|
||||||
border-top: 1px solid @HoverColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-md-cell {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs {
|
|
||||||
padding: 10px;
|
|
||||||
border-top: 1px solid #ffffff;
|
|
||||||
|
|
||||||
pre {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border: none;
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-md-cell {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell:hover.nteract-md-cell {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-md-cell .ntreact-cell-source {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Undo tree.less
|
|
||||||
.expanded::before {
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
.monaco-editor .monaco-list .main {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
import { CellId } from "@nteract/commutable";
|
|
||||||
import { CellType } from "@nteract/commutable/src";
|
|
||||||
import { actions, ContentRef, selectors } from "@nteract/core";
|
|
||||||
import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
|
|
||||||
import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror";
|
|
||||||
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
|
|
||||||
import * as React from "react";
|
|
||||||
import { DndProvider } from "react-dnd";
|
|
||||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
import { userContext } from "../../../UserContext";
|
|
||||||
import * as cdbActions from "../NotebookComponent/actions";
|
|
||||||
import loadTransform from "../NotebookComponent/loadTransform";
|
|
||||||
import { CdbAppState, SnapshotFragment, SnapshotRequest } from "../NotebookComponent/types";
|
|
||||||
import { NotebookUtil } from "../NotebookUtil";
|
|
||||||
import SecurityWarningBar from "../SecurityWarningBar/SecurityWarningBar";
|
|
||||||
import { AzureTheme } from "./AzureTheme";
|
|
||||||
import "./base.css";
|
|
||||||
import CellCreator from "./decorators/CellCreator";
|
|
||||||
import CellLabeler from "./decorators/CellLabeler";
|
|
||||||
import HoverableCell from "./decorators/HoverableCell";
|
|
||||||
import KeyboardShortcuts from "./decorators/kbd-shortcuts";
|
|
||||||
import "./default.css";
|
|
||||||
import MarkdownCell from "./markdown-cell";
|
|
||||||
import "./NotebookRenderer.less";
|
|
||||||
import SandboxOutputs from "./outputs/SandboxOutputs";
|
|
||||||
import Prompt from "./Prompt";
|
|
||||||
import { promptContent } from "./PromptContent";
|
|
||||||
import StatusBar from "./StatusBar";
|
|
||||||
import CellToolbar from "./Toolbar";
|
|
||||||
|
|
||||||
export interface NotebookRendererBaseProps {
|
|
||||||
contentRef: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NotebookRendererDispatchProps {
|
|
||||||
storeNotebookSnapshot: (imageSrc: string, requestId: string) => void;
|
|
||||||
notebookSnapshotError: (error: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
pendingSnapshotRequest: SnapshotRequest;
|
|
||||||
cellOutputSnapshots: Map<string, SnapshotFragment>;
|
|
||||||
notebookSnapshot: { imageSrc: string; requestId: string };
|
|
||||||
nbCodeCells: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type NotebookRendererProps = NotebookRendererBaseProps & NotebookRendererDispatchProps & StateProps;
|
|
||||||
|
|
||||||
const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, children: React.ReactNode) => {
|
|
||||||
const Cell = () => (
|
|
||||||
// TODO Draggable and HijackScroll not working anymore. Fix or remove when reworking MarkdownCell.
|
|
||||||
// <DraggableCell id={id} contentRef={contentRef}>
|
|
||||||
// <HijackScroll id={id} contentRef={contentRef}>
|
|
||||||
<CellCreator id={id} contentRef={contentRef}>
|
|
||||||
<CellLabeler id={id} contentRef={contentRef}>
|
|
||||||
<HoverableCell id={id} contentRef={contentRef}>
|
|
||||||
{children}
|
|
||||||
</HoverableCell>
|
|
||||||
</CellLabeler>
|
|
||||||
</CellCreator>
|
|
||||||
// </HijackScroll>
|
|
||||||
// </DraggableCell>
|
|
||||||
);
|
|
||||||
|
|
||||||
Cell.defaultProps = { cell_type };
|
|
||||||
return <Cell />;
|
|
||||||
};
|
|
||||||
|
|
||||||
class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
|
||||||
private notebookRendererRef = React.createRef<HTMLDivElement>();
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (!userContext.features.sandboxNotebookOutputs) {
|
|
||||||
loadTransform(this.props as any);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidUpdate(): Promise<void> {
|
|
||||||
// Take a snapshot if there's a pending request and all the outputs are also saved
|
|
||||||
if (
|
|
||||||
this.props.pendingSnapshotRequest &&
|
|
||||||
this.props.pendingSnapshotRequest.type === "notebook" &&
|
|
||||||
this.props.pendingSnapshotRequest.notebookContentRef === this.props.contentRef &&
|
|
||||||
(!this.props.notebookSnapshot ||
|
|
||||||
this.props.pendingSnapshotRequest.requestId !== this.props.notebookSnapshot.requestId) &&
|
|
||||||
this.props.cellOutputSnapshots.size === this.props.nbCodeCells
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
// Use Html2Canvas because it is much more reliable and fast than dom-to-file
|
|
||||||
const result = await NotebookUtil.takeScreenshotHtml2Canvas(
|
|
||||||
this.notebookRendererRef.current,
|
|
||||||
this.props.pendingSnapshotRequest.aspectRatio,
|
|
||||||
[...this.props.cellOutputSnapshots.values()],
|
|
||||||
this.props.pendingSnapshotRequest.downloadFilename,
|
|
||||||
);
|
|
||||||
this.props.storeNotebookSnapshot(result.imageSrc, this.props.pendingSnapshotRequest.requestId);
|
|
||||||
} catch (error) {
|
|
||||||
this.props.notebookSnapshotError(error.message);
|
|
||||||
} finally {
|
|
||||||
this.setState({ processedSnapshotRequest: undefined });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="NotebookRendererContainer">
|
|
||||||
<SecurityWarningBar contentRef={this.props.contentRef} />
|
|
||||||
<div className="NotebookRenderer" ref={this.notebookRendererRef}>
|
|
||||||
<DndProvider backend={HTML5Backend}>
|
|
||||||
<KeyboardShortcuts contentRef={this.props.contentRef}>
|
|
||||||
<Cells contentRef={this.props.contentRef}>
|
|
||||||
{{
|
|
||||||
code: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) =>
|
|
||||||
decorate(
|
|
||||||
id,
|
|
||||||
contentRef,
|
|
||||||
"code",
|
|
||||||
<CodeCell id={id} contentRef={contentRef} cell_type="code">
|
|
||||||
{{
|
|
||||||
editor: {
|
|
||||||
codemirror: (props: PassedEditorProps) => (
|
|
||||||
<CodeMirrorEditor {...props} editorType="codemirror" />
|
|
||||||
),
|
|
||||||
},
|
|
||||||
prompt: ({ id, contentRef }: { id: CellId; contentRef: ContentRef }) => (
|
|
||||||
<Prompt id={id} contentRef={contentRef} isHovered={false}>
|
|
||||||
{promptContent}
|
|
||||||
</Prompt>
|
|
||||||
),
|
|
||||||
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
|
|
||||||
outputs: userContext.features.sandboxNotebookOutputs
|
|
||||||
? () => <SandboxOutputs id={id} contentRef={contentRef} />
|
|
||||||
: undefined,
|
|
||||||
}}
|
|
||||||
</CodeCell>,
|
|
||||||
),
|
|
||||||
markdown: ({ id, contentRef }: { id: any; contentRef: ContentRef }) =>
|
|
||||||
decorate(
|
|
||||||
id,
|
|
||||||
contentRef,
|
|
||||||
"markdown",
|
|
||||||
<MarkdownCell id={id} contentRef={contentRef} cell_type="markdown">
|
|
||||||
{{
|
|
||||||
editor: {
|
|
||||||
codemirror: (props: PassedEditorProps) => (
|
|
||||||
<CodeMirrorEditor {...props} editorType="codemirror" />
|
|
||||||
),
|
|
||||||
},
|
|
||||||
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
|
|
||||||
}}
|
|
||||||
</MarkdownCell>,
|
|
||||||
),
|
|
||||||
|
|
||||||
raw: ({ id, contentRef }: { id: any; contentRef: ContentRef }) =>
|
|
||||||
decorate(
|
|
||||||
id,
|
|
||||||
contentRef,
|
|
||||||
"raw",
|
|
||||||
<RawCell id={id} contentRef={contentRef} cell_type="raw">
|
|
||||||
{{
|
|
||||||
editor: {
|
|
||||||
codemirror: (props: PassedEditorProps) => (
|
|
||||||
<CodeMirrorEditor {...props} editorType="codemirror" />
|
|
||||||
),
|
|
||||||
},
|
|
||||||
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
|
|
||||||
}}
|
|
||||||
</RawCell>,
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
</Cells>
|
|
||||||
</KeyboardShortcuts>
|
|
||||||
<AzureTheme />
|
|
||||||
</DndProvider>
|
|
||||||
</div>
|
|
||||||
<StatusBar contentRef={this.props.contentRef} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeMapStateToProps = (
|
|
||||||
initialState: CdbAppState,
|
|
||||||
ownProps: NotebookRendererProps,
|
|
||||||
): ((state: CdbAppState) => StateProps) => {
|
|
||||||
const mapStateToProps = (state: CdbAppState): StateProps => {
|
|
||||||
const { contentRef } = ownProps;
|
|
||||||
const model = selectors.model(state, { contentRef });
|
|
||||||
|
|
||||||
let nbCodeCells;
|
|
||||||
if (model && model.type === "notebook") {
|
|
||||||
nbCodeCells = NotebookUtil.findCodeCellWithDisplay(model.notebook).length;
|
|
||||||
}
|
|
||||||
const { pendingSnapshotRequest, cellOutputSnapshots, notebookSnapshot } = state.cdb;
|
|
||||||
return { pendingSnapshotRequest, cellOutputSnapshots, notebookSnapshot, nbCodeCells };
|
|
||||||
};
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererBaseProps) => {
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => {
|
|
||||||
return {
|
|
||||||
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) =>
|
|
||||||
dispatch(
|
|
||||||
actions.addTransform({
|
|
||||||
mediaType: transform.MIMETYPE,
|
|
||||||
component: transform,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
storeNotebookSnapshot: (imageSrc: string, requestId: string) =>
|
|
||||||
dispatch(cdbActions.storeNotebookSnapshot({ imageSrc, requestId })),
|
|
||||||
notebookSnapshotError: (error: string) => dispatch(cdbActions.notebookSnapshotError({ error })),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return mapDispatchToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, makeMapDispatchToProps)(BaseNotebookRenderer);
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
@import "../../../../less/Common/Constants";
|
|
||||||
|
|
||||||
.runCellButton {
|
|
||||||
max-height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
position: sticky;
|
|
||||||
z-index: 300;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
.ms-Button-flexContainer {
|
|
||||||
align-items: start;
|
|
||||||
padding-top: 11px;
|
|
||||||
|
|
||||||
.ms-Button-icon {
|
|
||||||
color: #0078D4;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.disabledRunCellButton {
|
|
||||||
.runCellButton .ms-Button-flexContainer .ms-Button-icon {
|
|
||||||
color: @BaseMediumHigh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.greyStopButton {
|
|
||||||
.runCellButton .ms-Button-flexContainer .ms-Button-icon {
|
|
||||||
color: @BaseMediumHigh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ms-Spinner .ms-Spinner-circle {
|
|
||||||
border-top-color: @BaseMediumHigh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import { actions, ContentRef, selectors } from "@nteract/core";
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as cdbActions from "../NotebookComponent/actions";
|
|
||||||
import { CdbAppState } from "../NotebookComponent/types";
|
|
||||||
import { NotebookUtil } from "../NotebookUtil";
|
|
||||||
|
|
||||||
export interface PassedPromptProps {
|
|
||||||
id: string;
|
|
||||||
contentRef: ContentRef;
|
|
||||||
status?: string;
|
|
||||||
executionCount?: number;
|
|
||||||
isHovered?: boolean;
|
|
||||||
isRunDisabled?: boolean;
|
|
||||||
runCell?: () => void;
|
|
||||||
stopCell?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ComponentProps {
|
|
||||||
id: string;
|
|
||||||
contentRef: ContentRef;
|
|
||||||
isHovered?: boolean;
|
|
||||||
isNotebookUntrusted?: boolean;
|
|
||||||
children: (props: PassedPromptProps) => React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
status?: string;
|
|
||||||
executionCount?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DispatchProps {
|
|
||||||
executeCell: () => void;
|
|
||||||
stopExecution: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = StateProps & DispatchProps & ComponentProps;
|
|
||||||
|
|
||||||
export class PromptPure extends React.Component<Props> {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="nteract-cell-prompt">
|
|
||||||
{this.props.children({
|
|
||||||
id: this.props.id,
|
|
||||||
contentRef: this.props.contentRef,
|
|
||||||
status: this.props.status,
|
|
||||||
executionCount: this.props.executionCount,
|
|
||||||
runCell: this.props.executeCell,
|
|
||||||
stopCell: this.props.stopExecution,
|
|
||||||
isHovered: this.props.isHovered,
|
|
||||||
isRunDisabled: this.props.isNotebookUntrusted,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeMapStateToProps = (_state: CdbAppState, ownProps: ComponentProps): ((state: CdbAppState) => StateProps) => {
|
|
||||||
const mapStateToProps = (state: CdbAppState) => {
|
|
||||||
const { contentRef, id } = ownProps;
|
|
||||||
const model = selectors.model(state, { contentRef });
|
|
||||||
|
|
||||||
let status;
|
|
||||||
let executionCount;
|
|
||||||
|
|
||||||
if (model && model.type === "notebook") {
|
|
||||||
status = model.transient.getIn(["cellMap", id, "status"]);
|
|
||||||
const cell = selectors.notebook.cellById(model, { id });
|
|
||||||
if (cell) {
|
|
||||||
executionCount = cell.get("execution_count", undefined);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isHovered = state.cdb.hoveredCellId === id;
|
|
||||||
|
|
||||||
return {
|
|
||||||
status,
|
|
||||||
executionCount,
|
|
||||||
isHovered,
|
|
||||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (
|
|
||||||
dispatch: Dispatch,
|
|
||||||
{ id, contentRef }: { id: string; contentRef: ContentRef },
|
|
||||||
): DispatchProps => ({
|
|
||||||
executeCell: () => {
|
|
||||||
dispatch(actions.executeCell({ id, contentRef }));
|
|
||||||
dispatch(
|
|
||||||
cdbActions.traceNotebookTelemetry({
|
|
||||||
action: Action.ExecuteCellPromptBtn,
|
|
||||||
actionModifier: ActionModifiers.Mark,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
stopExecution: () => dispatch(actions.interruptKernel({})),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, mapDispatchToProps)(PromptPure);
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { shallow } from "enzyme";
|
|
||||||
import { PassedPromptProps } from "./Prompt";
|
|
||||||
import { promptContent } from "./PromptContent";
|
|
||||||
|
|
||||||
describe("PromptContent", () => {
|
|
||||||
it("renders for busy status", () => {
|
|
||||||
const props: PassedPromptProps = {
|
|
||||||
id: "id",
|
|
||||||
contentRef: "contentRef",
|
|
||||||
status: "busy",
|
|
||||||
};
|
|
||||||
const wrapper = shallow(promptContent(props));
|
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders when hovered", () => {
|
|
||||||
const props: PassedPromptProps = {
|
|
||||||
id: "id",
|
|
||||||
contentRef: "contentRef",
|
|
||||||
isHovered: true,
|
|
||||||
};
|
|
||||||
const wrapper = shallow(promptContent(props));
|
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import { IconButton, Spinner, SpinnerSize } from "@fluentui/react";
|
|
||||||
import * as React from "react";
|
|
||||||
import { NotebookUtil } from "../NotebookUtil";
|
|
||||||
import { PassedPromptProps } from "./Prompt";
|
|
||||||
import "./Prompt.less";
|
|
||||||
|
|
||||||
export const promptContent = (props: PassedPromptProps): JSX.Element => {
|
|
||||||
if (props.status === "busy") {
|
|
||||||
const stopButtonText = "Stop cell execution";
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{ position: "sticky", width: "100%", maxHeight: "100%", left: 0, top: 0, zIndex: 300 }}
|
|
||||||
className={props.isHovered ? "" : "greyStopButton"}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
className="runCellButton"
|
|
||||||
iconProps={{ iconName: "CircleStopSolid" }}
|
|
||||||
title={stopButtonText}
|
|
||||||
ariaLabel={stopButtonText}
|
|
||||||
onClick={props.stopCell}
|
|
||||||
style={{ position: "absolute" }}
|
|
||||||
/>
|
|
||||||
<Spinner size={SpinnerSize.large} style={{ position: "absolute", width: "100%", paddingTop: 5 }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (props.isHovered) {
|
|
||||||
const playButtonText = props.isRunDisabled ? NotebookUtil.UntrustedNotebookRunHint : "Run cell";
|
|
||||||
return (
|
|
||||||
<div className={props.isRunDisabled ? "disabledRunCellButton" : ""}>
|
|
||||||
<IconButton
|
|
||||||
className="runCellButton"
|
|
||||||
iconProps={{ iconName: "MSNVideosSolid" }}
|
|
||||||
title={playButtonText}
|
|
||||||
ariaLabel={playButtonText}
|
|
||||||
disabled={props.isRunDisabled}
|
|
||||||
onClick={props.runCell}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return <div style={{ paddingTop: 7 }}>{promptText(props)}</div>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate what text goes inside the prompt based on the props to the prompt
|
|
||||||
*/
|
|
||||||
const promptText = (props: PassedPromptProps): string => {
|
|
||||||
if (props.status === "busy") {
|
|
||||||
return "[*]";
|
|
||||||
}
|
|
||||||
if (props.status === "queued") {
|
|
||||||
return "[…]";
|
|
||||||
}
|
|
||||||
if (typeof props.executionCount === "number") {
|
|
||||||
return `[${props.executionCount}]`;
|
|
||||||
}
|
|
||||||
return "[ ]";
|
|
||||||
};
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { shallow } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
import { StatusBar } from "./StatusBar";
|
|
||||||
|
|
||||||
describe("StatusBar", () => {
|
|
||||||
test("can render on a dummyNotebook", () => {
|
|
||||||
const lastSaved = new Date();
|
|
||||||
const kernelSpecDisplayName = "python3";
|
|
||||||
|
|
||||||
const component = shallow(
|
|
||||||
<StatusBar kernelStatus="kernel status" lastSaved={lastSaved} kernelSpecDisplayName={kernelSpecDisplayName} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(component).not.toBeNull();
|
|
||||||
});
|
|
||||||
test("Update if kernelSpecDisplayName has changed", () => {
|
|
||||||
const lastSaved = new Date();
|
|
||||||
const kernelSpecDisplayName = "python3";
|
|
||||||
|
|
||||||
const component = shallow(
|
|
||||||
<StatusBar kernelStatus="kernel status" lastSaved={lastSaved} kernelSpecDisplayName={kernelSpecDisplayName} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
const shouldUpdate = component.instance().shouldComponentUpdate(
|
|
||||||
{
|
|
||||||
lastSaved,
|
|
||||||
kernelSpecDisplayName: "javascript",
|
|
||||||
kernelStatus: "kernelStatus",
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
expect(shouldUpdate).toBe(true);
|
|
||||||
});
|
|
||||||
test("update if kernelStatus has changed", () => {
|
|
||||||
const lastSaved = new Date();
|
|
||||||
const kernelSpecDisplayName = "python3";
|
|
||||||
|
|
||||||
const component = shallow(
|
|
||||||
<StatusBar kernelStatus="kernel status" lastSaved={lastSaved} kernelSpecDisplayName={kernelSpecDisplayName} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
const shouldUpdate = component.instance().shouldComponentUpdate(
|
|
||||||
{
|
|
||||||
lastSaved: new Date(),
|
|
||||||
kernelSpecDisplayName: "python3",
|
|
||||||
kernelStatus: "kernelStatus",
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
expect(shouldUpdate).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import { AppState, ContentRef, selectors } from "@nteract/core";
|
|
||||||
import distanceInWordsToNow from "date-fns/distance_in_words_to_now";
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import styled from "styled-components";
|
|
||||||
import { StyleConstants } from "../../../Common/StyleConstants";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
lastSaved?: Date | null;
|
|
||||||
kernelSpecDisplayName: string;
|
|
||||||
kernelStatus: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NOT_CONNECTED = "not connected";
|
|
||||||
|
|
||||||
export const LeftStatus = styled.div`
|
|
||||||
float: left;
|
|
||||||
display: block;
|
|
||||||
padding-left: 10px;
|
|
||||||
`;
|
|
||||||
export const RightStatus = styled.div`
|
|
||||||
float: right;
|
|
||||||
padding-right: 10px;
|
|
||||||
display: block;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Bar = styled.div`
|
|
||||||
padding: 8px 0px 2px;
|
|
||||||
border-top: 1px solid ${StyleConstants.BaseMedium};
|
|
||||||
border-left: 1px solid ${StyleConstants.BaseMedium};
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 0.5em;
|
|
||||||
background: var(--status-bar);
|
|
||||||
z-index: 99;
|
|
||||||
@media print {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const BarContainer = styled.div`
|
|
||||||
padding-left: 4px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export class StatusBar extends React.Component<Props> {
|
|
||||||
shouldComponentUpdate(nextProps: Props): boolean {
|
|
||||||
if (this.props.lastSaved !== nextProps.lastSaved || this.props.kernelStatus !== nextProps.kernelStatus) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const name = this.props.kernelSpecDisplayName || "Loading...";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BarContainer>
|
|
||||||
<Bar data-test="notebookStatusBar">
|
|
||||||
<RightStatus>
|
|
||||||
{this.props.lastSaved ? (
|
|
||||||
<p data-test="saveStatus"> Last saved {distanceInWordsToNow(this.props.lastSaved)} </p>
|
|
||||||
) : (
|
|
||||||
<p> Not saved yet </p>
|
|
||||||
)}
|
|
||||||
</RightStatus>
|
|
||||||
<LeftStatus>
|
|
||||||
<p data-test="kernelStatus">
|
|
||||||
{name} | {this.props.kernelStatus}
|
|
||||||
</p>
|
|
||||||
</LeftStatus>
|
|
||||||
</Bar>
|
|
||||||
</BarContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InitialProps {
|
|
||||||
contentRef: ContentRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeMapStateToProps = (_initialState: AppState, initialProps: InitialProps): ((state: AppState) => Props) => {
|
|
||||||
const { contentRef } = initialProps;
|
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
|
||||||
const content = selectors.content(state, { contentRef });
|
|
||||||
|
|
||||||
if (!content || content.type !== "notebook") {
|
|
||||||
return {
|
|
||||||
kernelStatus: NOT_CONNECTED,
|
|
||||||
kernelSpecDisplayName: "no kernel",
|
|
||||||
lastSaved: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const kernelRef = content.model.kernelRef;
|
|
||||||
let kernel;
|
|
||||||
if (kernelRef) {
|
|
||||||
kernel = selectors.kernel(state, { kernelRef });
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastSaved = content && content.lastSaved ? content.lastSaved : undefined;
|
|
||||||
|
|
||||||
const kernelStatus = kernel?.status || NOT_CONNECTED;
|
|
||||||
|
|
||||||
// TODO: We need kernels associated to the kernelspec they came from
|
|
||||||
// so we can pluck off the display_name and provide it here
|
|
||||||
let kernelSpecDisplayName = " ";
|
|
||||||
if (kernelStatus === NOT_CONNECTED) {
|
|
||||||
kernelSpecDisplayName = "no kernel";
|
|
||||||
} else if (kernel?.kernelSpecName) {
|
|
||||||
kernelSpecDisplayName = kernel.kernelSpecName;
|
|
||||||
} else if (content && content.type === "notebook") {
|
|
||||||
// TODO Fix typing here
|
|
||||||
kernelSpecDisplayName = selectors.notebook.displayName(content.model as never) || " ";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
kernelSpecDisplayName,
|
|
||||||
kernelStatus,
|
|
||||||
lastSaved,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps)(StatusBar);
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
import { ContextualMenuItemType, DirectionalHint, IconButton, IContextualMenuItem } from "@fluentui/react";
|
|
||||||
import { CellId, CellType, ImmutableCodeCell } from "@nteract/commutable";
|
|
||||||
import { actions, AppState, DocumentRecordProps } from "@nteract/core";
|
|
||||||
import * as selectors from "@nteract/selectors";
|
|
||||||
import { CellToolbarContext } from "@nteract/stateful-components";
|
|
||||||
import { ContentRef } from "@nteract/types";
|
|
||||||
import { RecordOf } from "immutable";
|
|
||||||
import * as React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import * as cdbActions from "../NotebookComponent/actions";
|
|
||||||
import { SnapshotRequest } from "../NotebookComponent/types";
|
|
||||||
import { NotebookUtil } from "../NotebookUtil";
|
|
||||||
|
|
||||||
export interface ComponentProps {
|
|
||||||
contentRef: ContentRef;
|
|
||||||
id: CellId;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DispatchProps {
|
|
||||||
executeCell: () => void;
|
|
||||||
insertCodeCellAbove: () => void;
|
|
||||||
insertCodeCellBelow: () => void;
|
|
||||||
insertTextCellAbove: () => void;
|
|
||||||
insertTextCellBelow: () => void;
|
|
||||||
moveCell: (destinationId: CellId, above: boolean) => void;
|
|
||||||
clearOutputs: () => void;
|
|
||||||
deleteCell: () => void;
|
|
||||||
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: string) => void;
|
|
||||||
takeNotebookSnapshot: (payload: SnapshotRequest) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
cellType: CellType;
|
|
||||||
cellIdAbove: CellId;
|
|
||||||
cellIdBelow: CellId;
|
|
||||||
hasCodeOutput: boolean;
|
|
||||||
isNotebookUntrusted: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & StateProps> {
|
|
||||||
static contextType = CellToolbarContext;
|
|
||||||
|
|
||||||
render(): JSX.Element {
|
|
||||||
let items: IContextualMenuItem[] = [];
|
|
||||||
const isNotebookUntrusted = this.props.isNotebookUntrusted;
|
|
||||||
const runTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined;
|
|
||||||
|
|
||||||
if (this.props.cellType === "code") {
|
|
||||||
items = items.concat([
|
|
||||||
{
|
|
||||||
key: "Run",
|
|
||||||
text: "Run",
|
|
||||||
title: runTooltip,
|
|
||||||
disabled: isNotebookUntrusted,
|
|
||||||
onClick: () => {
|
|
||||||
this.props.executeCell();
|
|
||||||
this.props.traceNotebookTelemetry(Action.NotebooksExecuteCellFromMenu, ActionModifiers.Mark);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "Clear Outputs",
|
|
||||||
text: "Clear Outputs",
|
|
||||||
onClick: () => {
|
|
||||||
this.props.clearOutputs();
|
|
||||||
this.props.traceNotebookTelemetry(Action.NotebooksClearOutputsFromMenu, ActionModifiers.Mark);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (this.props.hasCodeOutput) {
|
|
||||||
items.push({
|
|
||||||
key: "Export output to image",
|
|
||||||
text: "Export output to image",
|
|
||||||
onClick: () => {
|
|
||||||
this.props.takeNotebookSnapshot({
|
|
||||||
requestId: new Date().getTime().toString(),
|
|
||||||
aspectRatio: undefined,
|
|
||||||
type: "celloutput",
|
|
||||||
cellId: this.props.id,
|
|
||||||
notebookContentRef: this.props.contentRef,
|
|
||||||
downloadFilename: `celloutput-${this.props.contentRef}_${this.props.id}.png`,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
key: "Divider",
|
|
||||||
itemType: ContextualMenuItemType.Divider,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
items = items.concat([
|
|
||||||
{
|
|
||||||
key: "Divider2",
|
|
||||||
itemType: ContextualMenuItemType.Divider,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "Insert Code Cell Above",
|
|
||||||
text: "Insert Code Cell Above",
|
|
||||||
onClick: () => {
|
|
||||||
this.props.insertCodeCellAbove();
|
|
||||||
this.props.traceNotebookTelemetry(Action.NotebooksInsertCodeCellAboveFromMenu, ActionModifiers.Mark);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "Insert Code Cell Below",
|
|
||||||
text: "Insert Code Cell Below",
|
|
||||||
onClick: () => {
|
|
||||||
this.props.insertCodeCellBelow();
|
|
||||||
this.props.traceNotebookTelemetry(Action.NotebooksInsertCodeCellBelowFromMenu, ActionModifiers.Mark);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "Insert Text Cell Above",
|
|
||||||
text: "Insert Text Cell Above",
|
|
||||||
onClick: () => {
|
|
||||||
this.props.insertTextCellAbove();
|
|
||||||
this.props.traceNotebookTelemetry(Action.NotebooksInsertTextCellAboveFromMenu, ActionModifiers.Mark);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "Insert Text Cell Below",
|
|
||||||
text: "Insert Text Cell Below",
|
|
||||||
onClick: () => {
|
|
||||||
this.props.insertTextCellBelow();
|
|
||||||
this.props.traceNotebookTelemetry(Action.NotebooksInsertTextCellBelowFromMenu, ActionModifiers.Mark);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "Divider3",
|
|
||||||
itemType: ContextualMenuItemType.Divider,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const moveItems: IContextualMenuItem[] = [];
|
|
||||||
if (this.props.cellIdAbove !== undefined) {
|
|
||||||
moveItems.push({
|
|
||||||
key: "Move Cell Up",
|
|
||||||
text: "Move Cell Up",
|
|
||||||
onClick: () => {
|
|
||||||
this.props.moveCell(this.props.cellIdAbove, true);
|
|
||||||
this.props.traceNotebookTelemetry(Action.NotebooksMoveCellUpFromMenu, ActionModifiers.Mark);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.cellIdBelow !== undefined) {
|
|
||||||
moveItems.push({
|
|
||||||
key: "Move Cell Down",
|
|
||||||
text: "Move Cell Down",
|
|
||||||
onClick: () => {
|
|
||||||
this.props.moveCell(this.props.cellIdBelow, false);
|
|
||||||
this.props.traceNotebookTelemetry(Action.NotebooksMoveCellDownFromMenu, ActionModifiers.Mark);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moveItems.length > 0) {
|
|
||||||
moveItems.push({
|
|
||||||
key: "Divider4",
|
|
||||||
itemType: ContextualMenuItemType.Divider,
|
|
||||||
});
|
|
||||||
items = items.concat(moveItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
key: "Delete Cell",
|
|
||||||
text: "Delete Cell",
|
|
||||||
onClick: () => {
|
|
||||||
this.props.deleteCell();
|
|
||||||
this.props.traceNotebookTelemetry(Action.DeleteCellFromMenu, ActionModifiers.Mark);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const menuItemLabel = "More";
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
name="More"
|
|
||||||
className="CellContextMenuButton"
|
|
||||||
ariaLabel={menuItemLabel}
|
|
||||||
menuIconProps={{
|
|
||||||
iconName: menuItemLabel,
|
|
||||||
styles: { root: { fontSize: "18px", fontWeight: "bold" } },
|
|
||||||
}}
|
|
||||||
menuProps={{
|
|
||||||
isBeakVisible: false,
|
|
||||||
directionalHint: DirectionalHint.bottomRightEdge,
|
|
||||||
items,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = (
|
|
||||||
dispatch: Dispatch,
|
|
||||||
{ id, contentRef }: { id: CellId; contentRef: ContentRef },
|
|
||||||
): DispatchProps => ({
|
|
||||||
executeCell: () => dispatch(actions.executeCell({ id, contentRef })),
|
|
||||||
insertCodeCellAbove: () => dispatch(actions.createCellAbove({ id, contentRef, cellType: "code" })),
|
|
||||||
insertCodeCellBelow: () => dispatch(actions.createCellBelow({ id, contentRef, cellType: "code" })),
|
|
||||||
insertTextCellAbove: () => dispatch(actions.createCellAbove({ id, contentRef, cellType: "markdown" })),
|
|
||||||
insertTextCellBelow: () => dispatch(actions.createCellBelow({ id, contentRef, cellType: "markdown" })),
|
|
||||||
moveCell: (destinationId: CellId, above: boolean) =>
|
|
||||||
dispatch(actions.moveCell({ id, contentRef, destinationId, above })),
|
|
||||||
clearOutputs: () => dispatch(actions.clearOutputs({ id, contentRef })),
|
|
||||||
deleteCell: () => dispatch(actions.deleteCell({ id, contentRef })),
|
|
||||||
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: string) =>
|
|
||||||
dispatch(cdbActions.traceNotebookTelemetry({ action, actionModifier, data })),
|
|
||||||
takeNotebookSnapshot: (request: SnapshotRequest) => dispatch(cdbActions.takeNotebookSnapshot(request)),
|
|
||||||
});
|
|
||||||
|
|
||||||
const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state: AppState) => StateProps) => {
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
|
||||||
const cell = selectors.cell.cellFromState(state, { id: ownProps.id, contentRef: ownProps.contentRef });
|
|
||||||
const cellType = cell.cell_type;
|
|
||||||
const model = selectors.model(state, { contentRef: ownProps.contentRef });
|
|
||||||
const cellOrder = selectors.notebook.cellOrder(model as RecordOf<DocumentRecordProps>);
|
|
||||||
const cellIndex = cellOrder.indexOf(ownProps.id);
|
|
||||||
const cellIdAbove = cellIndex ? cellOrder.get(cellIndex - 1, undefined) : undefined;
|
|
||||||
const cellIdBelow = cellIndex !== undefined ? cellOrder.get(cellIndex + 1, undefined) : undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
cellType,
|
|
||||||
cellIdAbove,
|
|
||||||
cellIdBelow,
|
|
||||||
hasCodeOutput: cellType === "code" && NotebookUtil.hasCodeCellOutput(cell as ImmutableCodeCell),
|
|
||||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, ownProps.contentRef),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, mapDispatchToProps)(BaseToolbar);
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`PromptContent renders for busy status 1`] = `
|
|
||||||
<div
|
|
||||||
className="greyStopButton"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"left": 0,
|
|
||||||
"maxHeight": "100%",
|
|
||||||
"position": "sticky",
|
|
||||||
"top": 0,
|
|
||||||
"width": "100%",
|
|
||||||
"zIndex": 300,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CustomizedIconButton
|
|
||||||
ariaLabel="Stop cell execution"
|
|
||||||
className="runCellButton"
|
|
||||||
iconProps={
|
|
||||||
{
|
|
||||||
"iconName": "CircleStopSolid",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"position": "absolute",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
title="Stop cell execution"
|
|
||||||
/>
|
|
||||||
<StyledSpinnerBase
|
|
||||||
size={3}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"paddingTop": 5,
|
|
||||||
"position": "absolute",
|
|
||||||
"width": "100%",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`PromptContent renders when hovered 1`] = `
|
|
||||||
<div
|
|
||||||
className=""
|
|
||||||
>
|
|
||||||
<CustomizedIconButton
|
|
||||||
ariaLabel="Run cell"
|
|
||||||
className="runCellButton"
|
|
||||||
iconProps={
|
|
||||||
{
|
|
||||||
"iconName": "MSNVideosSolid",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
title="Run cell"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
.nteract-cell-prompt {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 22px;
|
|
||||||
/* For creating a buffer area for <Prompt blank /> */
|
|
||||||
min-height: 22px;
|
|
||||||
width: var(--prompt-width, 50px);
|
|
||||||
padding: 2px 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs {
|
|
||||||
padding: 10px 10px 10px calc(var(--prompt-width, 50px) + 10px);
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow-y: hidden;
|
|
||||||
outline: none;
|
|
||||||
/* When expanded, this is overtaken to 100% */
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs code {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs pre {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
font-size: 14px;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs img {
|
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell {
|
|
||||||
position: relative;
|
|
||||||
transition: all 0.1s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cells {
|
|
||||||
padding-bottom: 10px;
|
|
||||||
padding: var(--nt-spacing-m, 10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-input .nteract-cell-source {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Adaptation for the R kernel's inline lists **/
|
|
||||||
.nteract-cell-outputs .list-inline li {
|
|
||||||
display: inline;
|
|
||||||
padding-right: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-input {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
.nteract-cell-input.invisible {
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
.nteract-cell-input .nteract-cell-prompt {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* for nested paragraphs in block quotes */
|
|
||||||
.nteract-cell-outputs blockquote p {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
.nteract-cell-outputs dd {
|
|
||||||
display: block;
|
|
||||||
-webkit-margin-start: 40px;
|
|
||||||
}
|
|
||||||
.nteract-cell-outputs dl {
|
|
||||||
display: block;
|
|
||||||
-webkit-margin-before: 1__qem;
|
|
||||||
-webkit-margin-after: 1em;
|
|
||||||
-webkit-margin-start: 0;
|
|
||||||
-webkit-margin-end: 0;
|
|
||||||
}
|
|
||||||
.nteract-cell-outputs dt {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.nteract-cell-outputs dl {
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.nteract-cell-outputs dt {
|
|
||||||
font-weight: bold;
|
|
||||||
float: left;
|
|
||||||
width: 20%;
|
|
||||||
/* adjust the width; make sure the total of both is 100% */
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.nteract-cell-outputs dd {
|
|
||||||
float: left;
|
|
||||||
width: 80%;
|
|
||||||
/* adjust the width; make sure the total of both is 100% */
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs kbd {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.1em 0.5em;
|
|
||||||
margin: 0 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs th {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs th,
|
|
||||||
.nteract-cell-outputs td,
|
|
||||||
/* for legacy output handling */
|
|
||||||
.nteract-cell-outputs .th,
|
|
||||||
.nteract-cell-outputs .td {
|
|
||||||
padding: 0.5em 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs blockquote {
|
|
||||||
padding: 0.75em 0.5em 0.75em 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs blockquote::before {
|
|
||||||
display: block;
|
|
||||||
height: 0;
|
|
||||||
margin-left: -0.95em;
|
|
||||||
}
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
import { actions, selectors, ContentRef, AppState } from "@nteract/core";
|
|
||||||
import { CellType } from "@nteract/commutable";
|
|
||||||
import * as React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
|
|
||||||
import styled from "styled-components";
|
|
||||||
import AddCodeCellIcon from "../../../../../images/notebook/add-code-cell.svg";
|
|
||||||
import AddTextCellIcon from "../../../../../images/notebook/add-text-cell.svg";
|
|
||||||
|
|
||||||
interface ComponentProps {
|
|
||||||
id: string;
|
|
||||||
contentRef: ContentRef;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
isFirstCell: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DispatchProps {
|
|
||||||
createCellAppend: (payload: { cellType: CellType; contentRef: ContentRef }) => void;
|
|
||||||
createCellAbove: (payload: { cellType: CellType; id?: string; contentRef: ContentRef }) => void;
|
|
||||||
createCellBelow: (payload: { cellType: CellType; id?: string; source: string; contentRef: ContentRef }) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CellCreatorMenu = styled.div`
|
|
||||||
display: none;
|
|
||||||
pointer-events: all;
|
|
||||||
position: relative;
|
|
||||||
top: 0px;
|
|
||||||
/**
|
|
||||||
* Now that the cell-creator is added as a decorator we need
|
|
||||||
* this x-index to ensure that it is always shown on the top
|
|
||||||
* of other cells.
|
|
||||||
*/
|
|
||||||
z-index: 50;
|
|
||||||
|
|
||||||
button:first-child {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
width: 109px;
|
|
||||||
height: 24px;
|
|
||||||
padding: 0px 4px;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 16px;
|
|
||||||
|
|
||||||
border: 1px solid #0078d4;
|
|
||||||
outline: none;
|
|
||||||
background: var(--theme-cell-creator-bg);
|
|
||||||
color: #0078d4;
|
|
||||||
}
|
|
||||||
|
|
||||||
button span {
|
|
||||||
color: var(--theme-cell-creator-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
button span:hover {
|
|
||||||
color: var(--theme-cell-creator-fg-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.octicon {
|
|
||||||
transition: color 0.5s;
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 12px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Divider = styled.div`
|
|
||||||
display: none;
|
|
||||||
position: relative;
|
|
||||||
top: 12px;
|
|
||||||
height: 1px;
|
|
||||||
width: 100%;
|
|
||||||
border-top: 1px solid rgba(204, 204, 204, 0.8);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CreatorHoverMask = styled.div`
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
overflow: visible;
|
|
||||||
height: 0px;
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const CreatorHoverRegion = styled.div`
|
|
||||||
position: relative;
|
|
||||||
overflow: visible;
|
|
||||||
top: 5px;
|
|
||||||
height: 30px;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&:hover ${CellCreatorMenu} {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover ${Divider} {
|
|
||||||
display: inherit;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FirstCreatorContainer = styled.div`
|
|
||||||
height: 20px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface CellCreatorProps {
|
|
||||||
above: boolean;
|
|
||||||
createCell: (type: "markdown" | "code", above: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PureCellCreator extends React.PureComponent<CellCreatorProps> {
|
|
||||||
createMarkdownCell = () => {
|
|
||||||
this.props.createCell("markdown", this.props.above);
|
|
||||||
};
|
|
||||||
|
|
||||||
createCodeCell = () => {
|
|
||||||
this.props.createCell("code", this.props.above);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<CreatorHoverMask>
|
|
||||||
<CreatorHoverRegion>
|
|
||||||
<Divider />
|
|
||||||
<CellCreatorMenu>
|
|
||||||
<button onClick={this.createCodeCell} className="add-code-cell">
|
|
||||||
<span className="octicon">
|
|
||||||
<img src={AddCodeCellIcon} alt="Add code cell" />
|
|
||||||
</span>
|
|
||||||
Add code
|
|
||||||
</button>
|
|
||||||
<button onClick={this.createMarkdownCell} className="add-text-cell">
|
|
||||||
<span className="octicon">
|
|
||||||
<img src={AddTextCellIcon} alt="Add text cell" />
|
|
||||||
</span>
|
|
||||||
Add text
|
|
||||||
</button>
|
|
||||||
</CellCreatorMenu>
|
|
||||||
</CreatorHoverRegion>
|
|
||||||
</CreatorHoverMask>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CellCreator extends React.PureComponent<ComponentProps & DispatchProps & StateProps> {
|
|
||||||
createCell = (type: "code" | "markdown", above: boolean): void => {
|
|
||||||
const { createCellBelow, createCellAppend, createCellAbove, id, contentRef } = this.props;
|
|
||||||
|
|
||||||
if (id === undefined || typeof id !== "string") {
|
|
||||||
createCellAppend({ cellType: type, contentRef });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
above
|
|
||||||
? createCellAbove({ cellType: type, id, contentRef })
|
|
||||||
: createCellBelow({ cellType: type, id, source: "", contentRef });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
{this.props.isFirstCell && (
|
|
||||||
<FirstCreatorContainer>
|
|
||||||
<PureCellCreator above={true} createCell={this.createCell} />
|
|
||||||
</FirstCreatorContainer>
|
|
||||||
)}
|
|
||||||
{this.props.children}
|
|
||||||
<PureCellCreator above={false} createCell={this.createCell} />
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState, ownProps: ComponentProps) => {
|
|
||||||
const { id, contentRef } = ownProps;
|
|
||||||
const model = selectors.model(state, { contentRef });
|
|
||||||
let isFirstCell = false;
|
|
||||||
|
|
||||||
if (model && model.type === "notebook") {
|
|
||||||
const cellOrder = selectors.notebook.cellOrder(model);
|
|
||||||
const cellIndex = cellOrder.findIndex((cellId) => cellId === id);
|
|
||||||
isFirstCell = cellIndex === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFirstCell,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
|
||||||
createCellAbove: (payload: { cellType: CellType; id?: string; contentRef: ContentRef }) =>
|
|
||||||
dispatch(actions.createCellAbove(payload)),
|
|
||||||
createCellAppend: (payload: { cellType: CellType; contentRef: ContentRef }) =>
|
|
||||||
dispatch(actions.createCellAppend(payload)),
|
|
||||||
createCellBelow: (payload: { cellType: CellType; id?: string; source: string; contentRef: ContentRef }) =>
|
|
||||||
dispatch(actions.createCellBelow(payload)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(CellCreator);
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
@import "../../../../../less/Common/Constants.less";
|
|
||||||
|
|
||||||
.CellLabeler .CellLabel {
|
|
||||||
margin-left: 5px;
|
|
||||||
font-family: @DataExplorerFont;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { AppState, ContentRef, DocumentRecordProps, selectors } from "@nteract/core";
|
|
||||||
import { RecordOf } from "immutable";
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import "./CellLabeler.less";
|
|
||||||
|
|
||||||
interface ComponentProps {
|
|
||||||
id: string;
|
|
||||||
contentRef: ContentRef; // TODO: Make this per contentRef?
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
cellIndex: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays "Cell <index>"
|
|
||||||
*/
|
|
||||||
class CellLabeler extends React.Component<ComponentProps & StateProps> {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="CellLabeler">
|
|
||||||
<div className="CellLabel">Cell {this.props.cellIndex + 1}</div>
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeMapStateToProps = (_state: AppState, ownProps: ComponentProps): ((state: AppState) => StateProps) => {
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
|
||||||
const model = selectors.model(state, { contentRef: ownProps.contentRef });
|
|
||||||
const cellOrder = selectors.notebook.cellOrder(model as RecordOf<DocumentRecordProps>);
|
|
||||||
const cellIndex = cellOrder.indexOf(ownProps.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
cellIndex,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, undefined)(CellLabeler);
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { ContentRef } from "@nteract/core";
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
import * as actions from "../../NotebookComponent/actions";
|
|
||||||
|
|
||||||
interface ComponentProps {
|
|
||||||
id: string;
|
|
||||||
contentRef: ContentRef; // TODO: Make this per contentRef?
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DispatchProps {
|
|
||||||
hover: () => void;
|
|
||||||
unHover: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HoverableCell sets the hovered cell
|
|
||||||
*/
|
|
||||||
class HoverableCell extends React.Component<ComponentProps & DispatchProps> {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className="HoverableCell" onMouseEnter={this.props.hover} onMouseLeave={this.props.unHover}>
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch, { id }: { id: string }): DispatchProps => ({
|
|
||||||
hover: () => dispatch(actions.setHoveredCell({ cellId: id })),
|
|
||||||
unHover: () => dispatch(actions.setHoveredCell({ cellId: undefined })),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(undefined, mapDispatchToProps)(HoverableCell);
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
import { actions, ContentRef } from "@nteract/core";
|
|
||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
ConnectDragPreview,
|
|
||||||
ConnectDragSource,
|
|
||||||
ConnectDropTarget,
|
|
||||||
DragSource,
|
|
||||||
DragSourceConnector,
|
|
||||||
DragSourceMonitor,
|
|
||||||
DropTarget,
|
|
||||||
DropTargetConnector,
|
|
||||||
DropTargetMonitor,
|
|
||||||
} from "react-dnd";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
import styled, { StyledComponent } from "styled-components";
|
|
||||||
|
|
||||||
/**
|
|
||||||
The cell drag preview image is just a little stylized version of
|
|
||||||
|
|
||||||
[ ]
|
|
||||||
|
|
||||||
It matches nteract's default light theme
|
|
||||||
|
|
||||||
*/
|
|
||||||
const cellDragPreviewImage = [
|
|
||||||
"data:image/png;base64,",
|
|
||||||
"iVBORw0KGgoAAAANSUhEUgAAADsAAAAzCAYAAAApdnDeAAAAAXNSR0IArs4c6QAA",
|
|
||||||
"AwNJREFUaAXtmlFL3EAUhe9MZptuoha3rLWgYC0W+lj/T3+26INvXbrI2oBdE9km",
|
|
||||||
"O9Nzxu1S0LI70AQScyFmDDfkfvdMZpNwlCCccwq7f21MaVM4FPtkU0o59RdoJBMx",
|
|
||||||
"WZINBg+DQWGKCAk+2kIKFh9JlSzLYVmOilEpR1Kh/iUbQFiNQTSbzWJrbYJximOJ",
|
|
||||||
"cSaulpVRoqh4K8JhjprIVJWqFlCpQNG51roYj8cLjJcGf5RMZWC1TYw1o2LxcEmy",
|
|
||||||
"0jeEo3ZFWVHIx0ji4eeKHFOx8l4sVVVZnBE6tWLHq7xO7FY86YpPeVjeo5y61tlR",
|
|
||||||
"JyhXEOQhF/lw6BGWixHvUWXVTpdgyUMu8q1h/ZJbqQhdiLsESx4FLvL9gcV6q3Cs",
|
|
||||||
"0liq2IHuBHjItYIV3rMvJnrYrkrdK9sr24EO9NO4AyI+i/CilOXbTi1xeXXFTyAS",
|
|
||||||
"GSOfzs42XmM+v5fJ5JvP29/fl8PDw43nhCbUpuzFxYXs7OxKmqZb1WQGkc/P80K+",
|
|
||||||
"T6dbnROaVJuyfPY+Pj7aup7h66HP/1Uu5O7u59bnhSTWpmxIEU3l9rBNdbrp6/TK",
|
|
||||||
"Nt3xpq7XK9tUp5u+Tm2/s/jYJdfX12LwBHVycrKRK89zmeJhYnZ7K3Fcz3e/2mDP",
|
|
||||||
"z7/waZEf8zaC+gSkKa3l4OBA3uztbXdOYFZtsKcfToNKSZNUPp6GnRN0AST3C1Ro",
|
|
||||||
"x9qS3yvbFqVC6+yVDe1YW/J7ZduiVGidvbKhHWtLfq9sW5QKrdMri9cxB6OFhQmO",
|
|
||||||
"TrDuBHjIRT5CEZZj0i7xOkYnWGeCPOQiHqC8lc/R60cLnNPuvjOkns7dk4t8/Jfv",
|
|
||||||
"s46mRlWqQiudxebVV3gAj7C9hXsmgZeztnfe/91YODEr3IoF/JY/sE2gbGaVLci3",
|
|
||||||
"hh0tRtWNvsm16JmNcOs6N9dW72LP7yOtWbEhjAUkZ+icoJ5HbE6+NSxMjKWe6cKb",
|
|
||||||
"GkUWgMwiFbXSlRpFkXelUlF4F70rVd7Bd4oZ/LL8xiDmtPV2Nwyf2zOlTfHERY7i",
|
|
||||||
"Haa1+w2+iFqx0aIgvgAAAABJRU5ErkJggg==",
|
|
||||||
].join("");
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
focusCell: (payload: any) => void;
|
|
||||||
id: string;
|
|
||||||
moveCell: (payload: any) => void;
|
|
||||||
children: React.ReactNode;
|
|
||||||
contentRef: ContentRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DnDSourceProps {
|
|
||||||
connectDragPreview: ConnectDragPreview;
|
|
||||||
connectDragSource: ConnectDragSource;
|
|
||||||
isDragging: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DnDTargetProps {
|
|
||||||
connectDropTarget: ConnectDropTarget;
|
|
||||||
isOver: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
hoverUpperHalf: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cellSource = {
|
|
||||||
beginDrag(props: Props) {
|
|
||||||
return {
|
|
||||||
id: props.id,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const DragHandle = styled.div.attrs({
|
|
||||||
role: "presentation",
|
|
||||||
})`
|
|
||||||
position: absolute;
|
|
||||||
z-index: 200;
|
|
||||||
width: var(--prompt-width, 50px);
|
|
||||||
height: 20px;
|
|
||||||
cursor: move;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface DragAreaProps {
|
|
||||||
isDragging: boolean;
|
|
||||||
isOver: boolean;
|
|
||||||
hoverUpperHalf: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DragArea = styled.div.attrs<DragAreaProps>((props) => ({
|
|
||||||
style: {
|
|
||||||
opacity: props.isDragging ? 0.25 : 1,
|
|
||||||
borderTop: props.isOver && props.hoverUpperHalf ? "3px lightgray solid" : "3px transparent solid",
|
|
||||||
borderBottom: props.isOver && !props.hoverUpperHalf ? "3px lightgray solid" : "3px transparent solid",
|
|
||||||
},
|
|
||||||
}))`
|
|
||||||
padding: 10px;
|
|
||||||
margin-top: -15px;
|
|
||||||
` as StyledComponent<"div", any, DragAreaProps, never>; // Somehow setting the type on `attrs` isn't propagating properly;
|
|
||||||
|
|
||||||
// This is the div that DragHandle's absolute position will anchor
|
|
||||||
const DragHandleAnchor = styled.div`
|
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function isDragUpper(props: Props, monitor: DropTargetMonitor, el: HTMLElement): boolean {
|
|
||||||
const hoverBoundingRect = el.getBoundingClientRect();
|
|
||||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
|
||||||
|
|
||||||
const clientOffset = monitor.getClientOffset();
|
|
||||||
const hoverClientY = clientOffset!.y - hoverBoundingRect.top;
|
|
||||||
|
|
||||||
return hoverClientY < hoverMiddleY;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const cellTarget = {
|
|
||||||
drop(props: Props, monitor: DropTargetMonitor, component: any): void {
|
|
||||||
if (monitor) {
|
|
||||||
const hoverUpperHalf = isDragUpper(props, monitor, component.el);
|
|
||||||
const item: Props = monitor.getItem();
|
|
||||||
// DropTargetSpec monitor definition could be undefined. we'll need a check for monitor in order to pass validation.
|
|
||||||
props.moveCell({
|
|
||||||
id: item.id,
|
|
||||||
destinationId: props.id,
|
|
||||||
above: hoverUpperHalf,
|
|
||||||
contentRef: props.contentRef,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
hover(props: Props, monitor: DropTargetMonitor, component: any): void {
|
|
||||||
if (monitor) {
|
|
||||||
component.setState({
|
|
||||||
hoverUpperHalf: isDragUpper(props, monitor, component.el),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function collectSource(
|
|
||||||
connect: DragSourceConnector,
|
|
||||||
monitor: DragSourceMonitor,
|
|
||||||
): {
|
|
||||||
connectDragSource: ConnectDragSource;
|
|
||||||
isDragging: boolean;
|
|
||||||
connectDragPreview: ConnectDragPreview;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
connectDragSource: connect.dragSource(),
|
|
||||||
isDragging: monitor.isDragging(),
|
|
||||||
connectDragPreview: connect.dragPreview(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectTarget(
|
|
||||||
connect: DropTargetConnector,
|
|
||||||
monitor: DropTargetMonitor,
|
|
||||||
): {
|
|
||||||
connectDropTarget: ConnectDropTarget;
|
|
||||||
isOver: boolean;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
connectDropTarget: connect.dropTarget(),
|
|
||||||
isOver: monitor.isOver(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DraggableCellView extends React.Component<Props & DnDSourceProps & DnDTargetProps, State> {
|
|
||||||
el?: HTMLDivElement | null;
|
|
||||||
|
|
||||||
state = {
|
|
||||||
hoverUpperHalf: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
const connectDragPreview = this.props.connectDragPreview;
|
|
||||||
const img = new (window as any).Image();
|
|
||||||
|
|
||||||
img.src = cellDragPreviewImage;
|
|
||||||
|
|
||||||
img.onload = /*dragImageLoaded*/ () => {
|
|
||||||
connectDragPreview(img);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
selectCell = () => {
|
|
||||||
const { focusCell, id, contentRef } = this.props;
|
|
||||||
focusCell({ id, contentRef });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return this.props.connectDropTarget(
|
|
||||||
// Sadly connectDropTarget _has_ to take a React element for a DOM element (no styled-divs)
|
|
||||||
<div>
|
|
||||||
<DragArea
|
|
||||||
isDragging={this.props.isDragging}
|
|
||||||
hoverUpperHalf={this.state.hoverUpperHalf}
|
|
||||||
isOver={this.props.isOver}
|
|
||||||
ref={(el) => {
|
|
||||||
this.el = el;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DragHandleAnchor>
|
|
||||||
{this.props.connectDragSource(
|
|
||||||
// Same thing with connectDragSource... It also needs a React Element that matches a DOM element
|
|
||||||
<div>
|
|
||||||
<DragHandle onClick={this.selectCell} />
|
|
||||||
</div>,
|
|
||||||
)}
|
|
||||||
{this.props.children}
|
|
||||||
</DragHandleAnchor>
|
|
||||||
</DragArea>
|
|
||||||
</div>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = DragSource<Props, DnDSourceProps>("CELL", cellSource, collectSource);
|
|
||||||
const target = DropTarget<Props, DnDTargetProps>("CELL", cellTarget, collectTarget);
|
|
||||||
|
|
||||||
export const makeMapDispatchToProps = (initialDispatch: Dispatch) => {
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
|
||||||
moveCell: (payload: actions.MoveCell["payload"]) => dispatch(actions.moveCell(payload)),
|
|
||||||
focusCell: (payload: actions.FocusCell["payload"]) => dispatch(actions.focusCell(payload)),
|
|
||||||
});
|
|
||||||
return mapDispatchToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(null, makeMapDispatchToProps)(source(target(DraggableCellView)));
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
/* eslint jsx-a11y/no-static-element-interactions: 0 */
|
|
||||||
/* eslint jsx-a11y/click-events-have-key-events: 0 */
|
|
||||||
|
|
||||||
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
|
|
||||||
interface ComponentProps {
|
|
||||||
id: string;
|
|
||||||
contentRef: ContentRef;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
focused: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DispatchProps {
|
|
||||||
selectCell: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = ComponentProps & DispatchProps & StateProps;
|
|
||||||
|
|
||||||
export class HijackScroll extends React.Component<Props> {
|
|
||||||
el: HTMLDivElement | null = null;
|
|
||||||
|
|
||||||
scrollIntoViewIfNeeded(prevFocused?: boolean): void {
|
|
||||||
// Check if the element is being hovered over.
|
|
||||||
const hovered = this.el && this.el.parentElement && this.el.parentElement.querySelector(":hover") === this.el;
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.props.focused &&
|
|
||||||
prevFocused !== this.props.focused &&
|
|
||||||
// Don't scroll into view if already hovered over, this prevents
|
|
||||||
// accidentally selecting text within the codemirror area
|
|
||||||
!hovered
|
|
||||||
) {
|
|
||||||
if (this.el && "scrollIntoViewIfNeeded" in this.el) {
|
|
||||||
// This is only valid in Chrome, WebKit
|
|
||||||
(this.el as any).scrollIntoViewIfNeeded();
|
|
||||||
} else if (this.el) {
|
|
||||||
// Make a best guess effort for older platforms
|
|
||||||
this.el.scrollIntoView();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
|
||||||
this.scrollIntoViewIfNeeded(prevProps.focused);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
this.scrollIntoViewIfNeeded();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onClick={this.props.selectCell}
|
|
||||||
role="presentation"
|
|
||||||
ref={(el) => {
|
|
||||||
this.el = el;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeMapStateToProps = (_initialState: AppState, ownProps: ComponentProps) => {
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
|
||||||
const { id, contentRef } = ownProps;
|
|
||||||
const model = selectors.model(state, { contentRef });
|
|
||||||
let focused = false;
|
|
||||||
|
|
||||||
if (model && model.type === "notebook") {
|
|
||||||
focused = model.cellFocused === id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
focused,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeMapDispatchToProps = (_initialDispatch: Dispatch, ownProps: ComponentProps) => {
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
|
||||||
selectCell: () => dispatch(actions.focusCell({ id: ownProps.id, contentRef: ownProps.contentRef })),
|
|
||||||
});
|
|
||||||
return mapDispatchToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, makeMapDispatchToProps)(HijackScroll);
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
import { CellId } from "@nteract/commutable";
|
|
||||||
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
|
|
||||||
import Immutable from "immutable";
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
import { NotebookUtil } from "../../../NotebookUtil";
|
|
||||||
|
|
||||||
interface ComponentProps {
|
|
||||||
contentRef: ContentRef;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
cellMap: Immutable.Map<string, any>;
|
|
||||||
cellOrder: Immutable.List<string>;
|
|
||||||
focusedCell?: string | null;
|
|
||||||
isNotebookUntrusted: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DispatchProps {
|
|
||||||
executeFocusedCell: (payload: { contentRef: ContentRef }) => void;
|
|
||||||
focusNextCell: (payload: { id?: CellId; createCellIfUndefined: boolean; contentRef: ContentRef }) => void;
|
|
||||||
focusNextCellEditor: (payload: { id?: CellId; contentRef: ContentRef }) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = ComponentProps & StateProps & DispatchProps;
|
|
||||||
|
|
||||||
export class KeyboardShortcuts extends React.Component<Props> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
this.keyDown = this.keyDown.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: Props) {
|
|
||||||
const newContentRef = this.props.contentRef !== nextProps.contentRef;
|
|
||||||
const newFocusedCell = this.props.focusedCell !== nextProps.focusedCell;
|
|
||||||
const newCellOrder = this.props.cellOrder && this.props.cellOrder.size !== nextProps.cellOrder.size;
|
|
||||||
return newContentRef || newFocusedCell || newCellOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
document.addEventListener("keydown", this.keyDown);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
|
||||||
document.removeEventListener("keydown", this.keyDown);
|
|
||||||
}
|
|
||||||
|
|
||||||
keyDown(e: KeyboardEvent): void {
|
|
||||||
// If enter is not pressed, do nothing
|
|
||||||
if (e.key !== "Enter") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
executeFocusedCell,
|
|
||||||
focusNextCell,
|
|
||||||
focusNextCellEditor,
|
|
||||||
contentRef,
|
|
||||||
cellOrder,
|
|
||||||
focusedCell,
|
|
||||||
cellMap,
|
|
||||||
isNotebookUntrusted,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (isNotebookUntrusted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ctrlKeyPressed = e.ctrlKey;
|
|
||||||
// Allow cmd + enter (macOS) to operate like ctrl + enter
|
|
||||||
if (process.platform === "darwin") {
|
|
||||||
ctrlKeyPressed = (e.metaKey || e.ctrlKey) && !(e.metaKey && e.ctrlKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
const shiftXORctrl = (e.shiftKey || ctrlKeyPressed) && !(e.shiftKey && ctrlKeyPressed);
|
|
||||||
if (!shiftXORctrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (focusedCell) {
|
|
||||||
// NOTE: Order matters here because we need it to execute _before_ we
|
|
||||||
// focus the next cell
|
|
||||||
executeFocusedCell({ contentRef });
|
|
||||||
|
|
||||||
if (e.shiftKey) {
|
|
||||||
/** Get the next cell and check if it is a markdown cell. */
|
|
||||||
const focusedCellIndex = cellOrder.indexOf(focusedCell);
|
|
||||||
const nextCellId = cellOrder.get(focusedCellIndex + 1);
|
|
||||||
const nextCell = nextCellId ? cellMap.get(nextCellId) : undefined;
|
|
||||||
|
|
||||||
/** Always focus the next cell. */
|
|
||||||
focusNextCell({
|
|
||||||
id: undefined,
|
|
||||||
createCellIfUndefined: true,
|
|
||||||
contentRef,
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Only focus the next editor if it is a code cell or a cell
|
|
||||||
* created at the bottom of the notebook. */
|
|
||||||
if (nextCell === undefined || (nextCell && nextCell.get("cell_type") === "code")) {
|
|
||||||
focusNextCellEditor({ id: focusedCell, contentRef });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <React.Fragment>{this.props.children}</React.Fragment>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeMapStateToProps = (_state: AppState, ownProps: ComponentProps) => {
|
|
||||||
const { contentRef } = ownProps;
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
|
||||||
const model = selectors.model(state, { contentRef });
|
|
||||||
|
|
||||||
let cellOrder = Immutable.List();
|
|
||||||
let cellMap = Immutable.Map<string, any>();
|
|
||||||
let focusedCell;
|
|
||||||
|
|
||||||
if (model && model.type === "notebook") {
|
|
||||||
cellOrder = model.notebook.cellOrder;
|
|
||||||
cellMap = selectors.notebook.cellMap(model);
|
|
||||||
focusedCell = selectors.notebook.cellFocused(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
cellOrder,
|
|
||||||
cellMap,
|
|
||||||
focusedCell,
|
|
||||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, contentRef),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapDispatchToProps = (dispatch: Dispatch) => ({
|
|
||||||
executeFocusedCell: (payload: { contentRef: ContentRef }) => dispatch(actions.executeFocusedCell(payload)),
|
|
||||||
focusNextCell: (payload: { id?: CellId; createCellIfUndefined: boolean; contentRef: ContentRef }) =>
|
|
||||||
dispatch(actions.focusNextCell(payload)),
|
|
||||||
focusNextCellEditor: (payload: { id?: CellId; contentRef: ContentRef }) =>
|
|
||||||
dispatch(actions.focusNextCellEditor(payload)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, mapDispatchToProps)(KeyboardShortcuts);
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
.nteract-cell-prompt {
|
|
||||||
font-family: monospace;
|
|
||||||
color: var(--theme-cell-prompt-fg, black);
|
|
||||||
background-color: var(--theme-cell-prompt-bg, #fafafa);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-pagers {
|
|
||||||
background-color: var(--theme-pager-bg, #fafafa);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs a {
|
|
||||||
color: var(--link-color-unvisited, blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs a:visited {
|
|
||||||
color: var(--link-color-visited, blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs code {
|
|
||||||
font-family: "Source Code Pro", monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs kbd {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #fff inset;
|
|
||||||
background-color: #f7f7f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs th,
|
|
||||||
.nteract-cell-outputs td,
|
|
||||||
/* for legacy output handling */
|
|
||||||
.nteract-cell-outputs .th,
|
|
||||||
.nteract-cell-outputs .td {
|
|
||||||
border: 1px solid var(--theme-app-border, #cbcbcb);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs blockquote {
|
|
||||||
padding: 0.75em 0.5em 0.75em 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs blockquote::before {
|
|
||||||
display: block;
|
|
||||||
height: 0;
|
|
||||||
margin-left: -0.95em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-input .nteract-cell-source {
|
|
||||||
background-color: var(--theme-cell-input-bg, #fafafa);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cells {
|
|
||||||
font-family: "Source Sans Pro", Helvetica Neue, Helvetica, sans-serif;
|
|
||||||
font-size: 16px;
|
|
||||||
background-color: var(--theme-app-bg);
|
|
||||||
color: var(--theme-app-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell {
|
|
||||||
background: var(--theme-cell-bg, white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-container.selected .nteract-cell-prompt {
|
|
||||||
background-color: var(--theme-cell-prompt-bg-focus, hsl(0, 0%, 90%));
|
|
||||||
color: var(--theme-cell-prompt-fg-focus, hsl(0, 0%, 51%));
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-container:hover:not(.selected) .nteract-cell-prompt,
|
|
||||||
.nteract-cell-container:active:not(.selected) .nteract-cell-prompt {
|
|
||||||
background-color: var(--theme-cell-prompt-bg-hover, hsl(0, 0%, 94%));
|
|
||||||
color: var(--theme-cell-prompt-fg-hover, hsl(0, 0%, 15%));
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-outputs {
|
|
||||||
background-color: var(--theme-cell-output-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-container.selected .nteract-cell-outputs {
|
|
||||||
background-color: var(--theme-cell-output-bg-focus);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-container:hover:not(.selected) .nteract-cell-outputs,
|
|
||||||
.nteract-cell-container:active:not(.selected) .nteract-cell-outputs {
|
|
||||||
background-color: var(--theme-cell-output-bg-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell:focus .nteract-cell-prompt {
|
|
||||||
background-color: var(--theme-cell-prompt-bg-focus, hsl(0, 0%, 90%));
|
|
||||||
color: var(--theme-cell-prompt-fg-focus, hsl(0, 0%, 51%));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
/* make sure all cells look the same in print regarless of focus */
|
|
||||||
.nteract-cell-container .nteract-cell-prompt,
|
|
||||||
.nteract-cell-container.selected .nteract-cell-prompt,
|
|
||||||
.nteract-cell-container:focus .nteract-cell-prompt,
|
|
||||||
.nteract-cell-container:hover:not(.selected) .nteract-cell-prompt {
|
|
||||||
background-color: var(--theme-cell-prompt-bg, white);
|
|
||||||
color: var(--theme-cell-prompt-fg, black);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-toolbar {
|
|
||||||
opacity: 0.4;
|
|
||||||
transition: opacity 0.4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-container:not(.selected) .nteract-cell-toolbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-container:hover:not(.selected) .nteract-cell-toolbar,
|
|
||||||
.nteract-cell-container.selected .nteract-cell-toolbar {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-toolbar > div {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-toolbar:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
.nteract-cell-toolbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-toolbar button {
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
width: 22px;
|
|
||||||
height: 20px;
|
|
||||||
padding: 0px 4px;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-toolbar span {
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1;
|
|
||||||
color: var(--theme-cell-toolbar-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-toolbar button span:hover {
|
|
||||||
color: var(--theme-cell-toolbar-fg-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-toolbar .octicon {
|
|
||||||
transition: color 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-toolbar span.spacer {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin: 1px 5px 3px 5px;
|
|
||||||
height: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell-toolbar {
|
|
||||||
z-index: 9;
|
|
||||||
position: sticky; /* keep visible with large code cells that need scrolling */
|
|
||||||
float: right;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 34px;
|
|
||||||
margin: 0 0 0 -100%; /* allow code cell to completely overlap (underlap?) */
|
|
||||||
padding: 0 0 0 50px; /* give users extra room to move their mouse to the
|
|
||||||
toolbar without causing the cell to go out of
|
|
||||||
focus/hide the toolbar before they get there */
|
|
||||||
}
|
|
||||||
|
|
||||||
.nteract-cell.hidden .nteract-cell-toolbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
// TODO The purpose of importing this source file https://github.com/nteract/nteract/blob/main/packages/stateful-components/src/cells/markdown-cell.tsx
|
|
||||||
// into our source is to be able to overwrite the version of react-markdown which has this fix ("escape html to false")
|
|
||||||
// https://github.com/nteract/markdown/commit/e19c7cc590a4379fc507f67a7b4228363b9d8631 without having to upgrade
|
|
||||||
// @nteract/stateful-component which causes runtime issues.
|
|
||||||
|
|
||||||
import { ImmutableCell } from "@nteract/commutable/src";
|
|
||||||
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
|
|
||||||
import { MarkdownPreviewer } from "@nteract/markdown";
|
|
||||||
import { defineConfigOption } from "@nteract/mythic-configuration";
|
|
||||||
import { Source as BareSource } from "@nteract/presentational-components";
|
|
||||||
import Editor, { EditorSlots } from "@nteract/stateful-components/lib/inputs/editor";
|
|
||||||
import React from "react";
|
|
||||||
import { ReactMarkdownProps } from "react-markdown";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
const { selector: markdownConfig } = defineConfigOption({
|
|
||||||
key: "markdownOptions",
|
|
||||||
label: "Markdown Editor Options",
|
|
||||||
defaultValue: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
interface NamedMDCellSlots {
|
|
||||||
editor?: EditorSlots;
|
|
||||||
toolbar?: () => JSX.Element;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ComponentProps {
|
|
||||||
id: string;
|
|
||||||
contentRef: ContentRef;
|
|
||||||
cell_type?: "markdown";
|
|
||||||
children?: NamedMDCellSlots;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
isCellFocused: boolean;
|
|
||||||
isEditorFocused: boolean;
|
|
||||||
cell?: ImmutableCell;
|
|
||||||
markdownOptions: ReactMarkdownProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DispatchProps {
|
|
||||||
focusAboveCell: () => void;
|
|
||||||
focusBelowCell: () => void;
|
|
||||||
focusEditor: () => void;
|
|
||||||
unfocusEditor: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add missing style to make the editor show https://github.com/nteract/nteract/commit/7fa580011578350e56deac81359f6294fdfcad20#diff-07829a1908e4bf98d4420f868a1c6f890b95d77297b9805c9590d2dba11e80ce
|
|
||||||
export const Source = styled(BareSource)`
|
|
||||||
width: 100%;
|
|
||||||
width: -webkit-fill-available;
|
|
||||||
width: -moz-available;
|
|
||||||
`;
|
|
||||||
export class PureMarkdownCell extends React.Component<ComponentProps & DispatchProps & StateProps> {
|
|
||||||
render() {
|
|
||||||
const { contentRef, id, cell, children } = this.props;
|
|
||||||
|
|
||||||
const { isEditorFocused, isCellFocused, markdownOptions } = this.props;
|
|
||||||
|
|
||||||
const { focusAboveCell, focusBelowCell, focusEditor, unfocusEditor } = this.props;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We don't set the editor slots as defaults to support dynamic imports
|
|
||||||
* Users can continue to add the editorSlots as children
|
|
||||||
*/
|
|
||||||
const editor = children?.editor;
|
|
||||||
const toolbar = children?.toolbar;
|
|
||||||
|
|
||||||
const source = cell ? cell.get("source", "") : "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="nteract-md-cell nteract-cell">
|
|
||||||
<div className="nteract-cell-row">
|
|
||||||
<div className="nteract-cell-gutter">{toolbar && toolbar()}</div>
|
|
||||||
<div className="nteract-cell-body">
|
|
||||||
<MarkdownPreviewer
|
|
||||||
focusAbove={focusAboveCell}
|
|
||||||
focusBelow={focusBelowCell}
|
|
||||||
focusEditor={focusEditor}
|
|
||||||
cellFocused={isCellFocused}
|
|
||||||
editorFocused={isEditorFocused}
|
|
||||||
unfocusEditor={unfocusEditor}
|
|
||||||
source={source}
|
|
||||||
markdownOptions={markdownOptions}
|
|
||||||
>
|
|
||||||
<Source className="nteract-cell-source">
|
|
||||||
<Editor id={id} contentRef={contentRef}>
|
|
||||||
{editor}
|
|
||||||
</Editor>
|
|
||||||
</Source>
|
|
||||||
</MarkdownPreviewer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeMapStateToProps = (
|
|
||||||
initialState: AppState,
|
|
||||||
ownProps: ComponentProps,
|
|
||||||
): ((state: AppState) => StateProps) => {
|
|
||||||
const { id, contentRef } = ownProps;
|
|
||||||
const mapStateToProps = (state: AppState): StateProps => {
|
|
||||||
const model = selectors.model(state, { contentRef });
|
|
||||||
let isCellFocused = false;
|
|
||||||
let isEditorFocused = false;
|
|
||||||
let cell;
|
|
||||||
|
|
||||||
if (model && model.type === "notebook") {
|
|
||||||
cell = selectors.notebook.cellById(model, { id });
|
|
||||||
isCellFocused = model.cellFocused === id;
|
|
||||||
isEditorFocused = model.editorFocused === id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const markdownOptionsDefaults = {
|
|
||||||
linkTarget: "_blank",
|
|
||||||
};
|
|
||||||
const currentMarkdownOptions = markdownConfig(state);
|
|
||||||
|
|
||||||
const markdownOptions = Object.assign({}, markdownOptionsDefaults, currentMarkdownOptions);
|
|
||||||
|
|
||||||
return {
|
|
||||||
cell,
|
|
||||||
isCellFocused,
|
|
||||||
isEditorFocused,
|
|
||||||
markdownOptions,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeMapDispatchToProps = (
|
|
||||||
initialDispatch: Dispatch,
|
|
||||||
ownProps: ComponentProps,
|
|
||||||
): ((dispatch: Dispatch) => DispatchProps) => {
|
|
||||||
const { id, contentRef } = ownProps;
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
|
|
||||||
focusAboveCell: () => {
|
|
||||||
dispatch(actions.focusPreviousCell({ id, contentRef }));
|
|
||||||
dispatch(actions.focusPreviousCellEditor({ id, contentRef }));
|
|
||||||
},
|
|
||||||
focusBelowCell: () => {
|
|
||||||
dispatch(actions.focusNextCell({ id, createCellIfUndefined: true, contentRef }));
|
|
||||||
dispatch(actions.focusNextCellEditor({ id, contentRef }));
|
|
||||||
},
|
|
||||||
focusEditor: () => dispatch(actions.focusCellEditor({ id, contentRef })),
|
|
||||||
unfocusEditor: () => dispatch(actions.focusCellEditor({ id: undefined, contentRef })),
|
|
||||||
});
|
|
||||||
|
|
||||||
return mapDispatchToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MarkdownCell = connect(makeMapStateToProps, makeMapDispatchToProps)(PureMarkdownCell);
|
|
||||||
|
|
||||||
export default MarkdownCell;
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
import { JSONObject } from "@nteract/commutable";
|
|
||||||
import { outputToJS } from "@nteract/commutable/lib/v4";
|
|
||||||
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
|
|
||||||
import IframeResizer from "iframe-resizer-react";
|
|
||||||
import Immutable from "immutable";
|
|
||||||
import postRobot from "post-robot";
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
import { CellOutputViewerProps, SnapshotResponse } from "../../../../CellOutputViewer/CellOutputViewer";
|
|
||||||
import * as cdbActions from "../../NotebookComponent/actions";
|
|
||||||
import { CdbAppState, SnapshotFragment, SnapshotRequest } from "../../NotebookComponent/types";
|
|
||||||
|
|
||||||
// Adapted from https://github.com/nteract/nteract/blob/main/packages/stateful-components/src/outputs/index.tsx
|
|
||||||
// to add support for sandboxing using <iframe>
|
|
||||||
|
|
||||||
interface ComponentProps {
|
|
||||||
id: string;
|
|
||||||
contentRef: ContentRef;
|
|
||||||
outputsContainerClassName?: string;
|
|
||||||
outputClassName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
hidden: boolean;
|
|
||||||
expanded: boolean;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
outputs: Immutable.List<any>;
|
|
||||||
|
|
||||||
pendingSnapshotRequest: SnapshotRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DispatchProps {
|
|
||||||
onMetadataChange?: (metadata: JSONObject, mediaType: string, index?: number) => void;
|
|
||||||
storeNotebookSnapshot: (imageSrc: string, requestId: string) => void;
|
|
||||||
storeSnapshotFragment: (cellId: string, snapshotFragment: SnapshotFragment) => void;
|
|
||||||
notebookSnapshotError: (error: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type SandboxOutputsProps = ComponentProps & StateProps & DispatchProps;
|
|
||||||
|
|
||||||
export class SandboxOutputs extends React.Component<SandboxOutputsProps> {
|
|
||||||
private childWindow: Window;
|
|
||||||
private nodeRef = React.createRef<HTMLDivElement>();
|
|
||||||
|
|
||||||
constructor(props: SandboxOutputsProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
processedSnapshotRequest: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render(): JSX.Element {
|
|
||||||
// Using min-width to set the width of the iFrame, works around an issue in iOS that can prevent the iFrame from sizing correctly.
|
|
||||||
return this.props.outputs && this.props.outputs.size > 0 ? (
|
|
||||||
<div ref={this.nodeRef}>
|
|
||||||
<IframeResizer
|
|
||||||
checkOrigin={false}
|
|
||||||
loading="lazy"
|
|
||||||
heightCalculationMethod="taggedElement"
|
|
||||||
onLoad={(event) => this.handleFrameLoad(event)}
|
|
||||||
src="./cellOutputViewer.html"
|
|
||||||
style={{ height: "1px", width: "1px", minWidth: "100%", border: "none" }}
|
|
||||||
sandbox="allow-downloads allow-popups allow-forms allow-pointer-lock allow-scripts allow-popups-to-escape-sandbox"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
|
|
||||||
this.childWindow = (event.target as HTMLIFrameElement).contentWindow;
|
|
||||||
this.sendPropsToFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
sendPropsToFrame(): void {
|
|
||||||
if (!this.childWindow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props: CellOutputViewerProps = {
|
|
||||||
id: this.props.id,
|
|
||||||
contentRef: this.props.contentRef,
|
|
||||||
outputsContainerClassName: `nteract-cell-outputs ${this.props.hidden ? "hidden" : ""} ${
|
|
||||||
this.props.expanded ? "expanded" : ""
|
|
||||||
} ${this.props.outputsContainerClassName}`,
|
|
||||||
outputClassName: this.props.outputClassName,
|
|
||||||
outputs: this.props.outputs.toArray().map((output) => outputToJS(output)),
|
|
||||||
onMetadataChange: this.props.onMetadataChange,
|
|
||||||
};
|
|
||||||
|
|
||||||
postRobot.send(this.childWindow, "props", props);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
this.sendPropsToFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidUpdate(prevProps: SandboxOutputsProps): Promise<void> {
|
|
||||||
this.sendPropsToFrame();
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.props.pendingSnapshotRequest &&
|
|
||||||
prevProps.pendingSnapshotRequest !== this.props.pendingSnapshotRequest &&
|
|
||||||
this.props.pendingSnapshotRequest.notebookContentRef === this.props.contentRef &&
|
|
||||||
this.nodeRef?.current
|
|
||||||
) {
|
|
||||||
const boundingClientRect = this.nodeRef.current.getBoundingClientRect();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data } = (await postRobot.send(
|
|
||||||
this.childWindow,
|
|
||||||
"snapshotRequest",
|
|
||||||
this.props.pendingSnapshotRequest,
|
|
||||||
)) as { data: SnapshotResponse };
|
|
||||||
if (this.props.pendingSnapshotRequest.type === "notebook") {
|
|
||||||
if (data.imageSrc === undefined) {
|
|
||||||
this.props.storeSnapshotFragment(this.props.id, {
|
|
||||||
image: undefined,
|
|
||||||
boundingClientRect: boundingClientRect,
|
|
||||||
requestId: data.requestId,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const image = new Image();
|
|
||||||
image.src = data.imageSrc;
|
|
||||||
image.onload = () => {
|
|
||||||
this.props.storeSnapshotFragment(this.props.id, {
|
|
||||||
image,
|
|
||||||
boundingClientRect: boundingClientRect,
|
|
||||||
requestId: data.requestId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
} else if (this.props.pendingSnapshotRequest.type === "celloutput") {
|
|
||||||
this.props.storeNotebookSnapshot(data.imageSrc, this.props.pendingSnapshotRequest.requestId);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.props.notebookSnapshotError(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeMapStateToProps = (
|
|
||||||
initialState: AppState,
|
|
||||||
ownProps: ComponentProps,
|
|
||||||
): ((state: AppState) => StateProps) => {
|
|
||||||
const mapStateToProps = (state: CdbAppState): StateProps => {
|
|
||||||
let outputs = Immutable.List();
|
|
||||||
let hidden = false;
|
|
||||||
let expanded = false;
|
|
||||||
|
|
||||||
const { contentRef, id } = ownProps;
|
|
||||||
const model = selectors.model(state, { contentRef });
|
|
||||||
|
|
||||||
if (model && model.type === "notebook") {
|
|
||||||
const cell = selectors.notebook.cellById(model, { id });
|
|
||||||
if (cell) {
|
|
||||||
outputs = cell.get("outputs", Immutable.List());
|
|
||||||
hidden = cell.cell_type === "code" && cell.getIn(["metadata", "jupyter", "outputs_hidden"]);
|
|
||||||
expanded = cell.cell_type === "code" && cell.getIn(["metadata", "collapsed"]) === false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine whether to take a snapshot or not
|
|
||||||
let pendingSnapshotRequest = state.cdb.pendingSnapshotRequest;
|
|
||||||
if (
|
|
||||||
pendingSnapshotRequest &&
|
|
||||||
pendingSnapshotRequest.type === "celloutput" &&
|
|
||||||
pendingSnapshotRequest.cellId !== id
|
|
||||||
) {
|
|
||||||
pendingSnapshotRequest = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { outputs, hidden, expanded, pendingSnapshotRequest };
|
|
||||||
};
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const makeMapDispatchToProps = (
|
|
||||||
initialDispath: Dispatch,
|
|
||||||
ownProps: ComponentProps,
|
|
||||||
): ((dispatch: Dispatch) => DispatchProps) => {
|
|
||||||
const { id, contentRef } = ownProps;
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => {
|
|
||||||
return {
|
|
||||||
onMetadataChange: (metadata: JSONObject, mediaType: string, index?: number) => {
|
|
||||||
dispatch(
|
|
||||||
actions.updateOutputMetadata({
|
|
||||||
id,
|
|
||||||
contentRef,
|
|
||||||
metadata,
|
|
||||||
index: index || 0,
|
|
||||||
mediaType,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
storeSnapshotFragment: (cellId: string, snapshot: SnapshotFragment) =>
|
|
||||||
dispatch(cdbActions.storeCellOutputSnapshot({ cellId, snapshot })),
|
|
||||||
storeNotebookSnapshot: (imageSrc: string, requestId: string) =>
|
|
||||||
dispatch(cdbActions.storeNotebookSnapshot({ imageSrc, requestId })),
|
|
||||||
notebookSnapshotError: (error: string) => dispatch(cdbActions.notebookSnapshotError({ error })),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return mapDispatchToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect<StateProps, DispatchProps, ComponentProps, AppState>(
|
|
||||||
makeMapStateToProps,
|
|
||||||
makeMapDispatchToProps,
|
|
||||||
)(SandboxOutputs);
|
|
||||||
@@ -1,13 +1,3 @@
|
|||||||
import {
|
|
||||||
CodeCellParams,
|
|
||||||
ImmutableNotebook,
|
|
||||||
makeCodeCell,
|
|
||||||
makeMarkdownCell,
|
|
||||||
makeNotebookRecord,
|
|
||||||
MarkdownCellParams,
|
|
||||||
MediaBundle,
|
|
||||||
} from "@nteract/commutable";
|
|
||||||
import { List, Map } from "immutable";
|
|
||||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
import { NotebookUtil } from "./NotebookUtil";
|
import { NotebookUtil } from "./NotebookUtil";
|
||||||
|
|
||||||
@@ -19,55 +9,6 @@ const notebookPath = `${folderPath}/${notebookName}`;
|
|||||||
const gitHubFolderUri = GitHubUtils.toContentUri("owner", "repo", "branch", folderPath);
|
const gitHubFolderUri = GitHubUtils.toContentUri("owner", "repo", "branch", folderPath);
|
||||||
const gitHubFileUri = GitHubUtils.toContentUri("owner", "repo", "branch", filePath);
|
const gitHubFileUri = GitHubUtils.toContentUri("owner", "repo", "branch", filePath);
|
||||||
const gitHubNotebookUri = GitHubUtils.toContentUri("owner", "repo", "branch", notebookPath);
|
const gitHubNotebookUri = GitHubUtils.toContentUri("owner", "repo", "branch", notebookPath);
|
||||||
const notebookRecord = makeNotebookRecord({
|
|
||||||
cellOrder: List.of("0", "1", "2", "3"),
|
|
||||||
cellMap: Map({
|
|
||||||
"0": makeMarkdownCell({
|
|
||||||
cell_type: "markdown",
|
|
||||||
source: "abc",
|
|
||||||
metadata: undefined,
|
|
||||||
} as MarkdownCellParams),
|
|
||||||
"1": makeCodeCell({
|
|
||||||
cell_type: "code",
|
|
||||||
execution_count: undefined,
|
|
||||||
metadata: undefined,
|
|
||||||
source: "print(5)",
|
|
||||||
outputs: List.of({
|
|
||||||
name: "stdout",
|
|
||||||
output_type: "stream",
|
|
||||||
text: "5",
|
|
||||||
}),
|
|
||||||
} as CodeCellParams),
|
|
||||||
"2": makeCodeCell({
|
|
||||||
cell_type: "code",
|
|
||||||
execution_count: undefined,
|
|
||||||
metadata: undefined,
|
|
||||||
source: 'display(HTML("<h1>Sample html</h1>"))',
|
|
||||||
outputs: List.of({
|
|
||||||
data: Object.freeze({
|
|
||||||
"text/html": "<h1>Sample output</h1>",
|
|
||||||
"text/plain": "<IPython.core.display.HTML object>",
|
|
||||||
} as MediaBundle),
|
|
||||||
output_type: "display_data",
|
|
||||||
metadata: undefined,
|
|
||||||
}),
|
|
||||||
} as CodeCellParams),
|
|
||||||
"3": makeCodeCell({
|
|
||||||
cell_type: "code",
|
|
||||||
execution_count: undefined,
|
|
||||||
metadata: undefined,
|
|
||||||
source: 'print("hello world")',
|
|
||||||
outputs: List.of({
|
|
||||||
name: "stdout",
|
|
||||||
output_type: "stream",
|
|
||||||
text: "hello world",
|
|
||||||
}),
|
|
||||||
} as CodeCellParams),
|
|
||||||
}),
|
|
||||||
nbformat_minor: 2,
|
|
||||||
nbformat: 2,
|
|
||||||
metadata: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("NotebookUtil", () => {
|
describe("NotebookUtil", () => {
|
||||||
describe("isNotebookFile", () => {
|
describe("isNotebookFile", () => {
|
||||||
@@ -127,11 +68,4 @@ describe("NotebookUtil", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("findFirstCodeCellWithDisplay", () => {
|
|
||||||
it("works for Notebook file", () => {
|
|
||||||
const notebookObject = notebookRecord as ImmutableNotebook;
|
|
||||||
expect(NotebookUtil.findCodeCellWithDisplay(notebookObject)[0]).toEqual("1");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,25 +1,12 @@
|
|||||||
import { ImmutableCodeCell, ImmutableNotebook } from "@nteract/commutable";
|
|
||||||
import { AppState, selectors } from "@nteract/core";
|
|
||||||
import domtoimage from "dom-to-image";
|
|
||||||
import Html2Canvas from "html2canvas";
|
|
||||||
import path from "path";
|
|
||||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
import * as StringUtils from "../../Utils/StringUtils";
|
import * as StringUtils from "../../Utils/StringUtils";
|
||||||
import * as InMemoryContentProviderUtils from "../Notebook/NotebookComponent/ContentProviders/InMemoryContentProviderUtils";
|
|
||||||
import { SnapshotFragment } from "./NotebookComponent/types";
|
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||||
|
|
||||||
// Must match rx-jupyter' FileType
|
// Must match rx-jupyter' FileType
|
||||||
export type FileType = "directory" | "file" | "notebook";
|
export type FileType = "directory" | "file" | "notebook";
|
||||||
export enum NotebookContentProviderType {
|
|
||||||
GitHubContentProviderType,
|
|
||||||
InMemoryContentProviderType,
|
|
||||||
JupyterContentProviderType,
|
|
||||||
}
|
|
||||||
// Utilities for notebooks
|
// Utilities for notebooks
|
||||||
export class NotebookUtil {
|
export class NotebookUtil {
|
||||||
public static UntrustedNotebookRunHint = "Please trust notebook first before running any code cells";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's a notebook file if the filename ends with .ipynb.
|
* It's a notebook file if the filename ends with .ipynb.
|
||||||
*/
|
*/
|
||||||
@@ -63,25 +50,6 @@ export class NotebookUtil {
|
|||||||
return new Date().getTime();
|
return new Date().getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Override from kernel-lifecycle.ts to improve kernel selection:
|
|
||||||
* Only return the kernel name persisted in the notebook
|
|
||||||
*
|
|
||||||
* @param filepath
|
|
||||||
* @param notebook
|
|
||||||
*/
|
|
||||||
public static extractNewKernel(filepath: string | null, notebook: ImmutableNotebook) {
|
|
||||||
const cwd = (filepath && path.dirname(filepath)) || "/";
|
|
||||||
|
|
||||||
const kernelSpecName =
|
|
||||||
notebook.getIn(["metadata", "kernelspec", "name"]) || notebook.getIn(["metadata", "language_info", "name"]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
cwd,
|
|
||||||
kernelSpecName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getFilePath(path: string, fileName: string): string {
|
public static getFilePath(path: string, fileName: string): string {
|
||||||
const contentInfo = GitHubUtils.fromContentUri(path);
|
const contentInfo = GitHubUtils.fromContentUri(path);
|
||||||
if (contentInfo) {
|
if (contentInfo) {
|
||||||
@@ -132,18 +100,6 @@ export class NotebookUtil {
|
|||||||
return relativePath.split("/").pop();
|
return relativePath.split("/").pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getContentProviderType(path: string): NotebookContentProviderType {
|
|
||||||
if (InMemoryContentProviderUtils.fromContentUri(path)) {
|
|
||||||
return NotebookContentProviderType.InMemoryContentProviderType;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (GitHubUtils.fromContentUri(path)) {
|
|
||||||
return NotebookContentProviderType.GitHubContentProviderType;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NotebookContentProviderType.JupyterContentProviderType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static replaceName(path: string, newName: string): string {
|
public static replaceName(path: string, newName: string): string {
|
||||||
const contentInfo = GitHubUtils.fromContentUri(path);
|
const contentInfo = GitHubUtils.fromContentUri(path);
|
||||||
if (contentInfo) {
|
if (contentInfo) {
|
||||||
@@ -164,186 +120,4 @@ export class NotebookUtil {
|
|||||||
const basePath = path.split(contentName).shift();
|
const basePath = path.split(contentName).shift();
|
||||||
return `${basePath}${newName}`;
|
return `${basePath}${newName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static hasCodeCellOutput(cell: ImmutableCodeCell): boolean {
|
|
||||||
return !!cell?.outputs?.find(
|
|
||||||
(output) =>
|
|
||||||
output.output_type === "display_data" ||
|
|
||||||
output.output_type === "execute_result" ||
|
|
||||||
output.output_type === "stream",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static isNotebookUntrusted(state: AppState, contentRef: string): boolean {
|
|
||||||
const content = selectors.content(state, { contentRef });
|
|
||||||
if (content?.type === "notebook") {
|
|
||||||
const metadata = selectors.notebook.metadata(content.model);
|
|
||||||
return metadata.getIn(["untrusted"]) as boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find code cells with display
|
|
||||||
* @param notebookObject
|
|
||||||
* @returns array of cell ids
|
|
||||||
*/
|
|
||||||
public static findCodeCellWithDisplay(notebookObject: ImmutableNotebook): string[] {
|
|
||||||
return notebookObject.cellOrder.reduce((accumulator: string[], cellId) => {
|
|
||||||
const cell = notebookObject.cellMap.get(cellId);
|
|
||||||
if (cell?.cell_type === "code") {
|
|
||||||
if (NotebookUtil.hasCodeCellOutput(cell as ImmutableCodeCell)) {
|
|
||||||
accumulator.push(cellId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return accumulator;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static takeScreenshotHtml2Canvas = (
|
|
||||||
target: HTMLElement,
|
|
||||||
aspectRatio: number,
|
|
||||||
subSnapshots: SnapshotFragment[],
|
|
||||||
downloadFilename?: string,
|
|
||||||
): Promise<{ imageSrc: string | undefined }> => {
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
try {
|
|
||||||
// target.scrollIntoView();
|
|
||||||
const canvas = await Html2Canvas(target, {
|
|
||||||
useCORS: true,
|
|
||||||
allowTaint: true,
|
|
||||||
scale: 1,
|
|
||||||
logging: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
//redraw canvas to fit aspect ratio
|
|
||||||
const originalImageData = canvas.toDataURL();
|
|
||||||
const width = parseInt(canvas.style.width.split("px")[0]);
|
|
||||||
if (aspectRatio) {
|
|
||||||
canvas.height = width * aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (originalImageData === "data:,") {
|
|
||||||
// Empty output
|
|
||||||
resolve({ imageSrc: undefined });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = canvas.getContext("2d");
|
|
||||||
const image = new Image();
|
|
||||||
image.src = originalImageData;
|
|
||||||
image.onload = () => {
|
|
||||||
if (!context) {
|
|
||||||
reject(new Error("No context to draw on"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
context.drawImage(image, 0, 0);
|
|
||||||
|
|
||||||
// draw sub images
|
|
||||||
if (subSnapshots) {
|
|
||||||
const parentRect = target.getBoundingClientRect();
|
|
||||||
subSnapshots.forEach((snapshot) => {
|
|
||||||
if (snapshot.image) {
|
|
||||||
context.drawImage(
|
|
||||||
snapshot.image,
|
|
||||||
snapshot.boundingClientRect.x - parentRect.x,
|
|
||||||
snapshot.boundingClientRect.y - parentRect.y,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve({ imageSrc: canvas.toDataURL() });
|
|
||||||
|
|
||||||
if (downloadFilename) {
|
|
||||||
NotebookUtil.downloadFile(
|
|
||||||
downloadFilename,
|
|
||||||
canvas.toDataURL("image/png").replace("image/png", "image/octet-stream"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public static takeScreenshotDomToImage = (
|
|
||||||
target: HTMLElement,
|
|
||||||
aspectRatio: number,
|
|
||||||
subSnapshots: SnapshotFragment[],
|
|
||||||
downloadFilename?: string,
|
|
||||||
): Promise<{ imageSrc?: string }> => {
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
// target.scrollIntoView();
|
|
||||||
try {
|
|
||||||
const filter = (node: Node): boolean => {
|
|
||||||
const excludedList = ["IMG", "CANVAS"];
|
|
||||||
return !excludedList.includes((node as HTMLElement).tagName);
|
|
||||||
};
|
|
||||||
|
|
||||||
const originalImageData = await domtoimage.toPng(target, { filter });
|
|
||||||
if (originalImageData === "data:,") {
|
|
||||||
// Empty output
|
|
||||||
resolve({});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseImage = new Image();
|
|
||||||
baseImage.src = originalImageData;
|
|
||||||
baseImage.onload = () => {
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = baseImage.width;
|
|
||||||
canvas.height = aspectRatio !== undefined ? baseImage.width * aspectRatio : baseImage.width;
|
|
||||||
|
|
||||||
const context = canvas.getContext("2d");
|
|
||||||
if (!context) {
|
|
||||||
reject(new Error("No Canvas to draw on"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// White background otherwise image is transparent
|
|
||||||
context.fillStyle = "white";
|
|
||||||
context.fillRect(0, 0, baseImage.width, baseImage.height);
|
|
||||||
|
|
||||||
context.drawImage(baseImage, 0, 0);
|
|
||||||
|
|
||||||
// draw sub images
|
|
||||||
if (subSnapshots) {
|
|
||||||
const parentRect = target.getBoundingClientRect();
|
|
||||||
subSnapshots.forEach((snapshot) => {
|
|
||||||
if (snapshot.image) {
|
|
||||||
context.drawImage(
|
|
||||||
snapshot.image,
|
|
||||||
snapshot.boundingClientRect.x - parentRect.x,
|
|
||||||
snapshot.boundingClientRect.y - parentRect.y,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve({ imageSrc: canvas.toDataURL() });
|
|
||||||
|
|
||||||
if (downloadFilename) {
|
|
||||||
NotebookUtil.downloadFile(
|
|
||||||
downloadFilename,
|
|
||||||
canvas.toDataURL("image/png").replace("image/png", "image/octet-stream"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private static downloadFile(filename: string, content: string): void {
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = content;
|
|
||||||
link.download = filename;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
.schemaAnalyzer {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
import { Spinner, SpinnerSize, Stack } from "@fluentui/react";
|
|
||||||
import { ImmutableExecuteResult, ImmutableOutput } from "@nteract/commutable";
|
|
||||||
import { actions, AppState, ContentRef, KernelRef, selectors } from "@nteract/core";
|
|
||||||
import Immutable from "immutable";
|
|
||||||
import * as React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
import * as Logger from "../../../Common/Logger";
|
|
||||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import loadTransform from "../NotebookComponent/loadTransform";
|
|
||||||
import SandboxOutputs from "../NotebookRenderer/outputs/SandboxOutputs";
|
|
||||||
import "./SchemaAnalyzer.less";
|
|
||||||
import { DefaultFilter, DefaultSampleSize, SchemaAnalyzerHeader } from "./SchemaAnalyzerHeader";
|
|
||||||
import { SchemaAnalyzerSplashScreen } from "./SchemaAnalyzerSplashScreen";
|
|
||||||
|
|
||||||
interface SchemaAnalyzerPureProps {
|
|
||||||
contentRef: ContentRef;
|
|
||||||
kernelRef: KernelRef;
|
|
||||||
databaseId: string;
|
|
||||||
collectionId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SchemaAnalyzerDispatchProps {
|
|
||||||
runCell: (contentRef: ContentRef, cellId: string) => void;
|
|
||||||
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void;
|
|
||||||
updateCell: (text: string, id: string, contentRef: ContentRef) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutputType = "rich" | "json";
|
|
||||||
|
|
||||||
interface SchemaAnalyzerState {
|
|
||||||
outputType: OutputType;
|
|
||||||
isFiltering: boolean;
|
|
||||||
sampleSize: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type SchemaAnalyzerProps = SchemaAnalyzerPureProps & StateProps & SchemaAnalyzerDispatchProps;
|
|
||||||
|
|
||||||
export class SchemaAnalyzer extends React.Component<SchemaAnalyzerProps, SchemaAnalyzerState> {
|
|
||||||
private clickAnalyzeTelemetryStartKey: number;
|
|
||||||
|
|
||||||
constructor(props: SchemaAnalyzerProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
outputType: "rich",
|
|
||||||
isFiltering: false,
|
|
||||||
sampleSize: DefaultSampleSize,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
loadTransform(this.props);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onAnalyzeButtonClick = (filter: string = DefaultFilter, sampleSize: string = this.state.sampleSize) => {
|
|
||||||
const query = {
|
|
||||||
command: "listSchema",
|
|
||||||
database: this.props.databaseId,
|
|
||||||
collection: this.props.collectionId,
|
|
||||||
outputType: this.state.outputType,
|
|
||||||
filter,
|
|
||||||
sampleSize,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isFiltering: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.props.updateCell(JSON.stringify(query), this.props.firstCellId, this.props.contentRef);
|
|
||||||
|
|
||||||
this.clickAnalyzeTelemetryStartKey = traceStart(Action.SchemaAnalyzerClickAnalyze, {
|
|
||||||
database: this.props.databaseId,
|
|
||||||
collection: this.props.collectionId,
|
|
||||||
sampleSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.props.runCell(this.props.contentRef, this.props.firstCellId);
|
|
||||||
};
|
|
||||||
|
|
||||||
private traceClickAnalyzeComplete = (kernelStatus: string, outputs: Immutable.List<ImmutableOutput>) => {
|
|
||||||
/**
|
|
||||||
* CosmosMongoKernel always returns 1st output as "text/html"
|
|
||||||
* This output can be an error stack or information about how many documents were sampled
|
|
||||||
*/
|
|
||||||
let firstTextHtmlOutput: string;
|
|
||||||
if (outputs.size > 0 && outputs.get(0).output_type === "execute_result") {
|
|
||||||
const executeResult = outputs.get(0) as ImmutableExecuteResult;
|
|
||||||
firstTextHtmlOutput = executeResult.data["text/html"];
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
database: this.props.databaseId,
|
|
||||||
collection: this.props.collectionId,
|
|
||||||
firstTextHtmlOutput,
|
|
||||||
sampleSize: this.state.sampleSize,
|
|
||||||
numOfOutputs: outputs.size,
|
|
||||||
kernelStatus,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only in cases where CosmosMongoKernel runs into an error we get a single output
|
|
||||||
if (outputs.size === 1) {
|
|
||||||
traceFailure(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey);
|
|
||||||
Logger.logError(`Failed to analyze schema: ${JSON.stringify(data)}`, "SchemaAnalyzer/traceClickAnalyzeComplete");
|
|
||||||
} else {
|
|
||||||
traceSuccess(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render(): JSX.Element {
|
|
||||||
const { firstCellId: id, contentRef, kernelStatus, outputs } = this.props;
|
|
||||||
if (!id) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isKernelBusy = kernelStatus === "busy";
|
|
||||||
const isKernelIdle = kernelStatus === "idle";
|
|
||||||
const showSchemaOutput = isKernelIdle && outputs?.size > 0;
|
|
||||||
|
|
||||||
if (showSchemaOutput && this.clickAnalyzeTelemetryStartKey) {
|
|
||||||
this.traceClickAnalyzeComplete(kernelStatus, outputs);
|
|
||||||
this.clickAnalyzeTelemetryStartKey = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="schemaAnalyzer">
|
|
||||||
<Stack tokens={{ childrenGap: 20, padding: 20 }}>
|
|
||||||
<SchemaAnalyzerHeader
|
|
||||||
isKernelIdle={isKernelIdle}
|
|
||||||
isKernelBusy={isKernelBusy}
|
|
||||||
onSampleSizeUpdated={(sampleSize) => this.setState({ sampleSize })}
|
|
||||||
onAnalyzeButtonClick={this.onAnalyzeButtonClick}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{showSchemaOutput ? (
|
|
||||||
<SandboxOutputs
|
|
||||||
id={id}
|
|
||||||
contentRef={contentRef}
|
|
||||||
outputsContainerClassName="schema-analyzer-cell-outputs"
|
|
||||||
outputClassName="schema-analyzer-cell-output"
|
|
||||||
/>
|
|
||||||
) : this.state.isFiltering ? (
|
|
||||||
<Spinner styles={{ root: { marginTop: 40 } }} size={SpinnerSize.large} />
|
|
||||||
) : (
|
|
||||||
<SchemaAnalyzerSplashScreen
|
|
||||||
isKernelIdle={isKernelIdle}
|
|
||||||
isKernelBusy={isKernelBusy}
|
|
||||||
onAnalyzeButtonClick={this.onAnalyzeButtonClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
firstCellId: string;
|
|
||||||
kernelStatus: string;
|
|
||||||
outputs: Immutable.List<ImmutableOutput>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InitialProps {
|
|
||||||
kernelRef: string;
|
|
||||||
contentRef: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redux
|
|
||||||
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
|
|
||||||
const { kernelRef, contentRef } = initialProps;
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
|
||||||
let kernelStatus;
|
|
||||||
let firstCellId;
|
|
||||||
let outputs;
|
|
||||||
|
|
||||||
const kernel = selectors.kernel(state, { kernelRef });
|
|
||||||
if (kernel) {
|
|
||||||
kernelStatus = kernel.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = selectors.content(state, { contentRef });
|
|
||||||
if (content?.type === "notebook") {
|
|
||||||
const cellOrder = selectors.notebook.cellOrder(content.model);
|
|
||||||
if (cellOrder.size > 0) {
|
|
||||||
firstCellId = cellOrder.first() as string;
|
|
||||||
|
|
||||||
const model = selectors.model(state, { contentRef });
|
|
||||||
if (model && model.type === "notebook") {
|
|
||||||
const cell = selectors.notebook.cellById(model, { id: firstCellId });
|
|
||||||
if (cell) {
|
|
||||||
outputs = cell.get("outputs", Immutable.List());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
firstCellId,
|
|
||||||
kernelStatus,
|
|
||||||
outputs,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeMapDispatchToProps = () => {
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) => {
|
|
||||||
return {
|
|
||||||
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
|
|
||||||
return dispatch(
|
|
||||||
actions.addTransform({
|
|
||||||
mediaType: transform.MIMETYPE,
|
|
||||||
component: transform,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
runCell: (contentRef: ContentRef, cellId: string) => {
|
|
||||||
return dispatch(
|
|
||||||
actions.executeCell({
|
|
||||||
contentRef,
|
|
||||||
id: cellId,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
updateCell: (text: string, id: string, contentRef: ContentRef) => {
|
|
||||||
dispatch(actions.updateCellSource({ id, contentRef, value: text }));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return mapDispatchToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, makeMapDispatchToProps)(SchemaAnalyzer);
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { actions, createContentRef, createKernelRef, KernelRef } from "@nteract/core";
|
|
||||||
import * as React from "react";
|
|
||||||
import { Provider } from "react-redux";
|
|
||||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
|
||||||
import {
|
|
||||||
NotebookComponentBootstrapper,
|
|
||||||
NotebookComponentBootstrapperOptions,
|
|
||||||
} from "../NotebookComponent/NotebookComponentBootstrapper";
|
|
||||||
import SchemaAnalyzer from "./SchemaAnalyzer";
|
|
||||||
import { SchemaAnalyzerNotebook } from "./SchemaAnalyzerUtils";
|
|
||||||
|
|
||||||
export class SchemaAnalyzerAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
|
|
||||||
public parameters: unknown;
|
|
||||||
private kernelRef: KernelRef;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
options: NotebookComponentBootstrapperOptions,
|
|
||||||
private databaseId: string,
|
|
||||||
private collectionId: string,
|
|
||||||
) {
|
|
||||||
super(options);
|
|
||||||
|
|
||||||
if (!this.contentRef) {
|
|
||||||
this.contentRef = createContentRef();
|
|
||||||
this.kernelRef = createKernelRef();
|
|
||||||
|
|
||||||
this.getStore().dispatch(
|
|
||||||
actions.fetchContent({
|
|
||||||
filepath: SchemaAnalyzerNotebook.path,
|
|
||||||
params: {},
|
|
||||||
kernelRef: this.kernelRef,
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
|
||||||
const props = {
|
|
||||||
contentRef: this.contentRef,
|
|
||||||
kernelRef: this.kernelRef,
|
|
||||||
databaseId: this.databaseId,
|
|
||||||
collectionId: this.collectionId,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Provider store={this.getStore()}>
|
|
||||||
<SchemaAnalyzer {...props} />;
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import {
|
|
||||||
DefaultButton,
|
|
||||||
Icon,
|
|
||||||
IRenderFunction,
|
|
||||||
ITextFieldProps,
|
|
||||||
PrimaryButton,
|
|
||||||
Stack,
|
|
||||||
TextField,
|
|
||||||
TooltipHost,
|
|
||||||
} from "@fluentui/react";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
type SchemaAnalyzerHeaderProps = {
|
|
||||||
isKernelIdle: boolean;
|
|
||||||
isKernelBusy: boolean;
|
|
||||||
onSampleSizeUpdated: (sampleSize?: string) => void;
|
|
||||||
onAnalyzeButtonClick: (filter: string, sampleSize: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DefaultFilter = "";
|
|
||||||
export const DefaultSampleSize = "1000";
|
|
||||||
const FilterPlaceholder = "{ field: 'value' }";
|
|
||||||
const SampleSizePlaceholder = "1000";
|
|
||||||
const MinSampleSize = 1;
|
|
||||||
const MaxSampleSize = 5000;
|
|
||||||
|
|
||||||
export const SchemaAnalyzerHeader = ({
|
|
||||||
isKernelIdle,
|
|
||||||
isKernelBusy,
|
|
||||||
onSampleSizeUpdated,
|
|
||||||
onAnalyzeButtonClick,
|
|
||||||
}: SchemaAnalyzerHeaderProps): JSX.Element => {
|
|
||||||
const [filter, setFilter] = React.useState<string | undefined>(DefaultFilter);
|
|
||||||
const [sampleSize, setSampleSize] = React.useState<string | undefined>(DefaultSampleSize);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
|
||||||
<Stack.Item grow>
|
|
||||||
<TextField
|
|
||||||
value={filter}
|
|
||||||
onChange={(_event, newValue?: string) => setFilter(newValue)}
|
|
||||||
label="Filter"
|
|
||||||
placeholder={FilterPlaceholder}
|
|
||||||
disabled={!isKernelIdle}
|
|
||||||
/>
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item>
|
|
||||||
<TextField
|
|
||||||
value={sampleSize}
|
|
||||||
onChange={(_event, newValue?: string) => {
|
|
||||||
const num = Number(newValue);
|
|
||||||
if (!newValue || (num >= MinSampleSize && num <= MaxSampleSize)) {
|
|
||||||
setSampleSize(newValue);
|
|
||||||
onSampleSizeUpdated(newValue);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
label="Sample size"
|
|
||||||
onRenderLabel={onSampleSizeWrapDefaultLabelRenderer}
|
|
||||||
placeholder={SampleSizePlaceholder}
|
|
||||||
disabled={!isKernelIdle}
|
|
||||||
/>
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item align="end">
|
|
||||||
<PrimaryButton
|
|
||||||
text={isKernelBusy ? "Analyzing..." : "Analyze"}
|
|
||||||
onClick={() => {
|
|
||||||
const sampleSizeToUse = sampleSize || DefaultSampleSize;
|
|
||||||
setSampleSize(sampleSizeToUse);
|
|
||||||
onAnalyzeButtonClick(filter, sampleSizeToUse);
|
|
||||||
}}
|
|
||||||
disabled={!isKernelIdle}
|
|
||||||
styles={{ root: { width: 120 } }}
|
|
||||||
/>
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item align="end">
|
|
||||||
<DefaultButton
|
|
||||||
text="Reset"
|
|
||||||
disabled={!isKernelIdle}
|
|
||||||
onClick={() => {
|
|
||||||
setFilter(DefaultFilter);
|
|
||||||
setSampleSize(DefaultSampleSize);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack.Item>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSampleSizeWrapDefaultLabelRenderer = (
|
|
||||||
props: ITextFieldProps,
|
|
||||||
defaultRender: IRenderFunction<ITextFieldProps>,
|
|
||||||
): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
|
|
||||||
<span>{defaultRender(props)}</span>
|
|
||||||
<TooltipHost content={`Number of documents to sample between ${MinSampleSize} and ${MaxSampleSize}`}>
|
|
||||||
<Icon iconName="Info" ariaLabel="Info" />
|
|
||||||
</TooltipHost>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { FontIcon, PrimaryButton, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
type SchemaAnalyzerSplashScreenProps = {
|
|
||||||
isKernelIdle: boolean;
|
|
||||||
isKernelBusy: boolean;
|
|
||||||
onAnalyzeButtonClick: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SchemaAnalyzerSplashScreen = ({
|
|
||||||
isKernelIdle,
|
|
||||||
isKernelBusy,
|
|
||||||
onAnalyzeButtonClick,
|
|
||||||
}: SchemaAnalyzerSplashScreenProps): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<Stack horizontalAlign="center" tokens={{ childrenGap: 20, padding: 20 }}>
|
|
||||||
<Stack.Item>
|
|
||||||
<FontIcon iconName="Chart" style={{ fontSize: 100, color: "#43B1E5", marginTop: 40 }} />
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item>
|
|
||||||
<Text variant="xxLarge">Explore your schema</Text>
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item>
|
|
||||||
<Text variant="large">
|
|
||||||
Quickly visualize your schema to infer the frequency, types and ranges of fields in your data set.
|
|
||||||
</Text>
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item>
|
|
||||||
<PrimaryButton
|
|
||||||
styles={{ root: { fontSize: 18, padding: 30 } }}
|
|
||||||
text={isKernelBusy ? "Analyzing..." : "Analyze Schema"}
|
|
||||||
onClick={() => onAnalyzeButtonClick()}
|
|
||||||
disabled={!isKernelIdle}
|
|
||||||
/>
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item>{isKernelBusy && <Spinner size={SpinnerSize.large} />}</Stack.Item>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { Notebook } from "@nteract/commutable";
|
|
||||||
import { IContent } from "@nteract/types";
|
|
||||||
import * as InMemoryContentProviderUtils from "../NotebookComponent/ContentProviders/InMemoryContentProviderUtils";
|
|
||||||
|
|
||||||
const notebookName = "schema-analyzer-component-notebook.ipynb";
|
|
||||||
const notebookPath = InMemoryContentProviderUtils.toContentUri(notebookName);
|
|
||||||
const notebook: Notebook = {
|
|
||||||
cells: [
|
|
||||||
{
|
|
||||||
cell_type: "code",
|
|
||||||
metadata: {},
|
|
||||||
execution_count: 0,
|
|
||||||
outputs: [],
|
|
||||||
source: "",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
metadata: {
|
|
||||||
kernelspec: {
|
|
||||||
displayName: "Mongo",
|
|
||||||
language: "mongocli",
|
|
||||||
name: "mongo",
|
|
||||||
},
|
|
||||||
language_info: {
|
|
||||||
file_extension: "ipynb",
|
|
||||||
mimetype: "application/json",
|
|
||||||
name: "mongo",
|
|
||||||
version: "1.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
nbformat: 4,
|
|
||||||
nbformat_minor: 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SchemaAnalyzerNotebook: IContent<"notebook"> = {
|
|
||||||
name: notebookName,
|
|
||||||
path: notebookPath,
|
|
||||||
type: "notebook",
|
|
||||||
writable: true,
|
|
||||||
created: "",
|
|
||||||
last_modified: "",
|
|
||||||
mimetype: "application/x-ipynb+json",
|
|
||||||
content: notebook,
|
|
||||||
format: "json",
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { shallow } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
import { SecurityWarningBar } from "./SecurityWarningBar";
|
|
||||||
|
|
||||||
describe("SecurityWarningBar", () => {
|
|
||||||
it("renders if notebook is untrusted", () => {
|
|
||||||
const wrapper = shallow(
|
|
||||||
<SecurityWarningBar
|
|
||||||
contentRef={"contentRef"}
|
|
||||||
isNotebookUntrusted={true}
|
|
||||||
markNotebookAsTrusted={undefined}
|
|
||||||
saveNotebook={undefined}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders if notebook is trusted", () => {
|
|
||||||
const wrapper = shallow(
|
|
||||||
<SecurityWarningBar
|
|
||||||
contentRef={"contentRef"}
|
|
||||||
isNotebookUntrusted={false}
|
|
||||||
markNotebookAsTrusted={undefined}
|
|
||||||
saveNotebook={undefined}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import { MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
|
||||||
import { actions, AppState } from "@nteract/core";
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
import { NotebookUtil } from "../NotebookUtil";
|
|
||||||
|
|
||||||
export interface SecurityWarningBarPureProps {
|
|
||||||
contentRef: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SecurityWarningBarDispatchProps {
|
|
||||||
markNotebookAsTrusted: (contentRef: string) => void;
|
|
||||||
saveNotebook: (contentRef: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type SecurityWarningBarProps = SecurityWarningBarPureProps & StateProps & SecurityWarningBarDispatchProps;
|
|
||||||
|
|
||||||
interface SecurityWarningBarState {
|
|
||||||
isBarDismissed: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SecurityWarningBar extends React.Component<SecurityWarningBarProps, SecurityWarningBarState> {
|
|
||||||
constructor(props: SecurityWarningBarProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isBarDismissed: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render(): JSX.Element {
|
|
||||||
return this.props.isNotebookUntrusted && !this.state.isBarDismissed ? (
|
|
||||||
<MessageBar
|
|
||||||
messageBarType={MessageBarType.warning}
|
|
||||||
isMultiline={false}
|
|
||||||
onDismiss={() => this.setState({ isBarDismissed: true })}
|
|
||||||
dismissButtonAriaLabel="Close"
|
|
||||||
actions={
|
|
||||||
<MessageBarButton
|
|
||||||
onClick={() => {
|
|
||||||
this.props.markNotebookAsTrusted(this.props.contentRef);
|
|
||||||
this.props.saveNotebook(this.props.contentRef);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Trust Notebook
|
|
||||||
</MessageBarButton>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{" "}
|
|
||||||
This notebook was downloaded from the public gallery. Running code cells from a notebook authored by someone
|
|
||||||
else may involve security risks.
|
|
||||||
</MessageBar>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
isNotebookUntrusted: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InitialProps {
|
|
||||||
contentRef: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redux
|
|
||||||
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
|
|
||||||
const mapStateToProps = (state: AppState): StateProps => ({
|
|
||||||
isNotebookUntrusted: NotebookUtil.isNotebookUntrusted(state, initialProps.contentRef),
|
|
||||||
});
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeMapDispatchToProps = () => {
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch): SecurityWarningBarDispatchProps => {
|
|
||||||
return {
|
|
||||||
markNotebookAsTrusted: (contentRef: string) => {
|
|
||||||
return dispatch(
|
|
||||||
actions.deleteMetadataField({
|
|
||||||
contentRef,
|
|
||||||
field: "untrusted",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
saveNotebook: (contentRef: string) => dispatch(actions.save({ contentRef })),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return mapDispatchToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, makeMapDispatchToProps)(SecurityWarningBar);
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`SecurityWarningBar renders if notebook is trusted 1`] = `<Fragment />`;
|
|
||||||
|
|
||||||
exports[`SecurityWarningBar renders if notebook is untrusted 1`] = `
|
|
||||||
<StyledMessageBar
|
|
||||||
actions={
|
|
||||||
<CustomizedMessageBarButton
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
Trust Notebook
|
|
||||||
</CustomizedMessageBarButton>
|
|
||||||
}
|
|
||||||
dismissButtonAriaLabel="Close"
|
|
||||||
isMultiline={false}
|
|
||||||
messageBarType={5}
|
|
||||||
onDismiss={[Function]}
|
|
||||||
>
|
|
||||||
|
|
||||||
This notebook was downloaded from the public gallery. Running code cells from a notebook authored by someone else may involve security risks.
|
|
||||||
</StyledMessageBar>
|
|
||||||
`;
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
jest.mock("./NotebookComponent/store");
|
|
||||||
jest.mock("@nteract/core");
|
|
||||||
import { defineConfigOption } from "@nteract/mythic-configuration";
|
|
||||||
import { NotebookClientV2 } from "./NotebookClientV2";
|
|
||||||
import configureStore from "./NotebookComponent/store";
|
|
||||||
|
|
||||||
describe("auto start kernel", () => {
|
|
||||||
it("configure autoStartKernelOnNotebookOpen properly depending whether notebook is/is not read-only", async () => {
|
|
||||||
(configureStore as jest.Mock).mockReturnValue({
|
|
||||||
dispatch: () => {
|
|
||||||
/* noop */
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
defineConfigOption({
|
|
||||||
label: "editorType",
|
|
||||||
key: "editorType",
|
|
||||||
defaultValue: "foo",
|
|
||||||
});
|
|
||||||
|
|
||||||
defineConfigOption({
|
|
||||||
label: "autoSaveInterval",
|
|
||||||
key: "autoSaveInterval",
|
|
||||||
defaultValue: 1234,
|
|
||||||
});
|
|
||||||
|
|
||||||
defineConfigOption({
|
|
||||||
label: "Line numbers",
|
|
||||||
key: "codeMirror.lineNumbers",
|
|
||||||
defaultValue: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
[true, false].forEach((isReadOnly) => {
|
|
||||||
new NotebookClientV2({
|
|
||||||
connectionInfo: {
|
|
||||||
authToken: "autToken",
|
|
||||||
notebookServerEndpoint: "notebookServerEndpoint",
|
|
||||||
forwardingId: "Id",
|
|
||||||
},
|
|
||||||
databaseAccountName: undefined,
|
|
||||||
defaultExperience: undefined,
|
|
||||||
isReadOnly,
|
|
||||||
contentProvider: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(configureStore).toHaveBeenCalledWith(
|
|
||||||
expect.anything(), // initial state
|
|
||||||
undefined, // content provider
|
|
||||||
expect.anything(), // onTraceFailure
|
|
||||||
expect.anything(), // customMiddlewares
|
|
||||||
!isReadOnly,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -9,7 +9,7 @@ import { configContext } from "../../ConfigContext";
|
|||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels";
|
import { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels";
|
||||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||||
@@ -234,32 +234,6 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
|||||||
galleryContentRoot,
|
galleryContentRoot,
|
||||||
gitHubNotebooksContentRoot,
|
gitHubNotebooksContentRoot,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (get().notebookServerInfo?.notebookServerEndpoint) {
|
|
||||||
const updatedRoot = await notebookManager?.notebookContentClient?.updateItemChildren(myNotebooksContentRoot);
|
|
||||||
set({ myNotebooksContentRoot: updatedRoot });
|
|
||||||
|
|
||||||
if (updatedRoot?.children) {
|
|
||||||
// Count 1st generation children (tree is lazy-loaded)
|
|
||||||
const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
|
|
||||||
updatedRoot.children.forEach((notebookItem) => {
|
|
||||||
switch (notebookItem.type) {
|
|
||||||
case NotebookContentItemType.File:
|
|
||||||
nodeCounts.files++;
|
|
||||||
break;
|
|
||||||
case NotebookContentItemType.Directory:
|
|
||||||
nodeCounts.directories++;
|
|
||||||
break;
|
|
||||||
case NotebookContentItemType.Notebook:
|
|
||||||
nodeCounts.notebooks++;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]): void => {
|
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]): void => {
|
||||||
const gitHubNotebooksContentRoot = cloneDeep(get().gitHubNotebooksContentRoot);
|
const gitHubNotebooksContentRoot = cloneDeep(get().gitHubNotebooksContentRoot);
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ describe("OpenActions", () => {
|
|||||||
collection.expandCollection = jest.fn();
|
collection.expandCollection = jest.fn();
|
||||||
collection.onDocumentDBDocumentsClick = jest.fn();
|
collection.onDocumentDBDocumentsClick = jest.fn();
|
||||||
collection.onMongoDBDocumentsClick = jest.fn();
|
collection.onMongoDBDocumentsClick = jest.fn();
|
||||||
collection.onSchemaAnalyzerClick = jest.fn();
|
|
||||||
collection.onTableEntitiesClick = jest.fn();
|
collection.onTableEntitiesClick = jest.fn();
|
||||||
collection.onGraphDocumentsClick = jest.fn();
|
collection.onGraphDocumentsClick = jest.fn();
|
||||||
collection.onNewQueryClick = jest.fn();
|
collection.onNewQueryClick = jest.fn();
|
||||||
|
|||||||
@@ -107,14 +107,6 @@ function openCollectionTab(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
action.tabKind === ActionContracts.TabKind.SchemaAnalyzer ||
|
|
||||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
|
|
||||||
) {
|
|
||||||
collection.onSchemaAnalyzerClick();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
action.tabKind === ActionContracts.TabKind.TableEntities ||
|
action.tabKind === ActionContracts.TabKind.TableEntities ||
|
||||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
|
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
|
||||||
@@ -232,17 +224,5 @@ export function handleOpenAction(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
action.actionType === ActionContracts.ActionType.OpenSampleNotebook ||
|
|
||||||
action.actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenSampleNotebook]
|
|
||||||
) {
|
|
||||||
openFile(action as ActionContracts.OpenSampleNotebook, explorer);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openFile(action: ActionContracts.OpenSampleNotebook, explorer: Explorer) {
|
|
||||||
explorer.handleOpenFileAction(decodeURIComponent(action.path));
|
|
||||||
}
|
|
||||||
|
|||||||
-1
@@ -166,7 +166,6 @@ exports[`AddGlobalSecondaryIndexPanel render default panel 1`] = `
|
|||||||
"refreshNotebookList": [Function],
|
"refreshNotebookList": [Function],
|
||||||
"resourceTree": ResourceTreeAdapter {
|
"resourceTree": ResourceTreeAdapter {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
import { IDropdownOption } from "@fluentui/react";
|
|
||||||
import { Keys, t } from "Localization";
|
|
||||||
import React, { FormEvent, FunctionComponent, useEffect, useState } from "react";
|
|
||||||
import { HttpStatusCodes } from "../../../Common/Constants";
|
|
||||||
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
|
|
||||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
|
||||||
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
|
|
||||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
|
||||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
|
||||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
|
||||||
import Explorer from "../../Explorer";
|
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
|
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
|
||||||
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
|
|
||||||
|
|
||||||
interface Location {
|
|
||||||
type: "MyNotebooks" | "GitHub";
|
|
||||||
|
|
||||||
// GitHub
|
|
||||||
owner?: string;
|
|
||||||
repo?: string;
|
|
||||||
branch?: string;
|
|
||||||
}
|
|
||||||
export interface CopyNotebookPanelProps {
|
|
||||||
name: string;
|
|
||||||
content: string;
|
|
||||||
container: Explorer;
|
|
||||||
junoClient: JunoClient;
|
|
||||||
gitHubOAuthService: GitHubOAuthService;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
|
||||||
name,
|
|
||||||
content,
|
|
||||||
container,
|
|
||||||
junoClient,
|
|
||||||
gitHubOAuthService,
|
|
||||||
}: CopyNotebookPanelProps) => {
|
|
||||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>();
|
|
||||||
const [formError, setFormError] = useState<string>("");
|
|
||||||
const [pinnedRepos, setPinnedRepos] = useState<IPinnedRepo[]>();
|
|
||||||
const [selectedLocation, setSelectedLocation] = useState<Location>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
open();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const open = async (): Promise<void> => {
|
|
||||||
if (gitHubOAuthService.isLoggedIn()) {
|
|
||||||
const response = await junoClient.getPinnedRepos(gitHubOAuthService.getTokenObservable()()?.scope);
|
|
||||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
|
||||||
handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.data?.length > 0) {
|
|
||||||
setPinnedRepos(response.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const submit = async (): Promise<void> => {
|
|
||||||
let destination: string = selectedLocation?.type;
|
|
||||||
let clearMessage: () => void;
|
|
||||||
setIsExecuting(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!selectedLocation) {
|
|
||||||
throw new Error(`No location selected`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedLocation.type === "GitHub") {
|
|
||||||
destination = `${destination} - ${GitHubUtils.toRepoFullName(
|
|
||||||
selectedLocation.owner,
|
|
||||||
selectedLocation.repo,
|
|
||||||
)} - ${selectedLocation.branch}`;
|
|
||||||
} else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenixNotebooks) {
|
|
||||||
destination = useNotebook.getState().notebookFolderName;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);
|
|
||||||
|
|
||||||
const notebookContentItem = await copyNotebook(selectedLocation);
|
|
||||||
if (!notebookContentItem) {
|
|
||||||
throw new Error(t(Keys.panes.copyNotebook.uploadFailedError, { name }));
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${name} to ${destination}`);
|
|
||||||
closeSidePanel();
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = getErrorMessage(error);
|
|
||||||
setFormError(t(Keys.panes.copyNotebook.copyFailedError, { name, destination }));
|
|
||||||
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError);
|
|
||||||
} finally {
|
|
||||||
clearMessage && clearMessage();
|
|
||||||
setIsExecuting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
|
|
||||||
let parent: NotebookContentItem;
|
|
||||||
let isGithubTree: boolean;
|
|
||||||
switch (location.type) {
|
|
||||||
case "MyNotebooks":
|
|
||||||
parent = {
|
|
||||||
name: useNotebook.getState().notebookFolderName,
|
|
||||||
path: useNotebook.getState().notebookBasePath,
|
|
||||||
type: NotebookContentItemType.Directory,
|
|
||||||
};
|
|
||||||
isGithubTree = false;
|
|
||||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
|
||||||
await container.allocateContainer();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "GitHub":
|
|
||||||
parent = {
|
|
||||||
name: selectedLocation.branch,
|
|
||||||
path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""),
|
|
||||||
type: NotebookContentItemType.Directory,
|
|
||||||
};
|
|
||||||
isGithubTree = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported location type ${location.type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return container.uploadFile(name, content, parent, isGithubTree);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDropDownChange = (_: FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
|
|
||||||
setSelectedLocation(option?.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const props: RightPaneFormProps = {
|
|
||||||
formError,
|
|
||||||
isExecuting: isExecuting,
|
|
||||||
submitButtonText: t(Keys.common.ok),
|
|
||||||
onSubmit: () => submit(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyNotebookPaneProps: CopyNotebookPaneProps = {
|
|
||||||
name,
|
|
||||||
pinnedRepos,
|
|
||||||
onDropDownChange: onDropDownChange,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RightPaneForm {...props}>
|
|
||||||
<CopyNotebookPaneComponent {...copyNotebookPaneProps} />
|
|
||||||
</RightPaneForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import {
|
|
||||||
Dropdown,
|
|
||||||
IDropdownOption,
|
|
||||||
IDropdownProps,
|
|
||||||
IRenderFunction,
|
|
||||||
ISelectableOption,
|
|
||||||
Label,
|
|
||||||
SelectableOptionMenuItemType,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
} from "@fluentui/react";
|
|
||||||
import { GitHubReposTitle } from "Explorer/Tree/ResourceTree";
|
|
||||||
import React, { FormEvent, FunctionComponent } from "react";
|
|
||||||
import { IPinnedRepo } from "../../../Juno/JunoClient";
|
|
||||||
import { Keys, t } from "Localization";
|
|
||||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
|
||||||
import { useNotebook } from "../../Notebook/useNotebook";
|
|
||||||
|
|
||||||
interface Location {
|
|
||||||
type: "MyNotebooks" | "GitHub";
|
|
||||||
|
|
||||||
// GitHub
|
|
||||||
owner?: string;
|
|
||||||
repo?: string;
|
|
||||||
branch?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CopyNotebookPaneProps {
|
|
||||||
name: string;
|
|
||||||
pinnedRepos: IPinnedRepo[];
|
|
||||||
onDropDownChange: (_: FormEvent<HTMLDivElement>, option?: IDropdownOption) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps> = ({
|
|
||||||
name,
|
|
||||||
pinnedRepos,
|
|
||||||
onDropDownChange,
|
|
||||||
}: CopyNotebookPaneProps) => {
|
|
||||||
const BranchNameWhiteSpace = " ";
|
|
||||||
|
|
||||||
const onRenderDropDownTitle: IRenderFunction<IDropdownOption[]> = (options: IDropdownOption[]): JSX.Element => {
|
|
||||||
return <span>{options.length && options[0].title}</span>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRenderDropDownOption: IRenderFunction<ISelectableOption> = (option: ISelectableOption): JSX.Element => {
|
|
||||||
return <span style={{ whiteSpace: "pre-wrap" }}>{option.text}</span>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDropDownOptions = (): IDropdownOption[] => {
|
|
||||||
const options: IDropdownOption[] = [];
|
|
||||||
options.push({
|
|
||||||
key: "MyNotebooks-Item",
|
|
||||||
text: useNotebook.getState().notebookFolderName,
|
|
||||||
title: useNotebook.getState().notebookFolderName,
|
|
||||||
data: {
|
|
||||||
type: "MyNotebooks",
|
|
||||||
} as Location,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (pinnedRepos && pinnedRepos.length > 0) {
|
|
||||||
options.push({
|
|
||||||
key: "GitHub-Header-Divider",
|
|
||||||
text: undefined,
|
|
||||||
itemType: SelectableOptionMenuItemType.Divider,
|
|
||||||
});
|
|
||||||
|
|
||||||
options.push({
|
|
||||||
key: "GitHub-Header",
|
|
||||||
text: GitHubReposTitle,
|
|
||||||
itemType: SelectableOptionMenuItemType.Header,
|
|
||||||
});
|
|
||||||
|
|
||||||
pinnedRepos.forEach((pinnedRepo) => {
|
|
||||||
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
|
|
||||||
options.push({
|
|
||||||
key: `GitHub-Repo-${repoFullName}`,
|
|
||||||
text: repoFullName,
|
|
||||||
disabled: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
pinnedRepo.branches.forEach((branch) =>
|
|
||||||
options.push({
|
|
||||||
key: `GitHub-Repo-${repoFullName}-${branch.name}`,
|
|
||||||
text: `${BranchNameWhiteSpace}${branch.name}`,
|
|
||||||
title: `${repoFullName} - ${branch.name}`,
|
|
||||||
data: {
|
|
||||||
type: "GitHub",
|
|
||||||
owner: pinnedRepo.owner,
|
|
||||||
repo: pinnedRepo.name,
|
|
||||||
branch: branch.name,
|
|
||||||
} as Location,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
};
|
|
||||||
const dropDownProps: IDropdownProps = {
|
|
||||||
label: t(Keys.panes.copyNotebook.location),
|
|
||||||
ariaLabel: t(Keys.panes.copyNotebook.locationAriaLabel),
|
|
||||||
placeholder: "Select an option",
|
|
||||||
onRenderTitle: onRenderDropDownTitle,
|
|
||||||
onRenderOption: onRenderDropDownOption,
|
|
||||||
options: getDropDownOptions(),
|
|
||||||
onChange: onDropDownChange,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="paneMainContent">
|
|
||||||
<Stack tokens={{ childrenGap: 10 }}>
|
|
||||||
<Stack.Item>
|
|
||||||
<Label htmlFor="notebookName">{t(Keys.panes.copyNotebook.name)}</Label>
|
|
||||||
<Text id="notebookName">{name}</Text>
|
|
||||||
</Stack.Item>
|
|
||||||
|
|
||||||
<Dropdown {...dropDownProps} />
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -38,7 +38,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
|||||||
"refreshNotebookList": [Function],
|
"refreshNotebookList": [Function],
|
||||||
"resourceTree": ResourceTreeAdapter {
|
"resourceTree": ResourceTreeAdapter {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"copyNotebook": [Function],
|
|
||||||
"parameters": [Function],
|
"parameters": [Function],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { mount } from "enzyme";
|
|
||||||
import React from "react";
|
|
||||||
import Explorer from "../../Explorer";
|
|
||||||
import { StringInputPane } from "./StringInputPane";
|
|
||||||
const props = {
|
|
||||||
explorer: new Explorer(),
|
|
||||||
closePanel: (): void => undefined,
|
|
||||||
errorMessage: "Could not create directory ",
|
|
||||||
inProgressMessage: "Creating directory ",
|
|
||||||
successMessage: "Created directory ",
|
|
||||||
inputLabel: "Enter new directory name",
|
|
||||||
paneTitle: "Create new directory",
|
|
||||||
submitButtonLabel: "Create",
|
|
||||||
defaultInput: "",
|
|
||||||
onSubmit: jest.fn(),
|
|
||||||
notebookFile: {
|
|
||||||
name: "Untitled1123.ipynb",
|
|
||||||
path: "notebooks/Untitled1123.ipynb",
|
|
||||||
type: 0,
|
|
||||||
timestamp: 1618452275805,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
describe("StringInput Pane", () => {
|
|
||||||
it("should render Create new directory properly", () => {
|
|
||||||
const wrapper = mount(<StringInputPane {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import { TextField } from "@fluentui/react";
|
|
||||||
import * as ViewModels from "Contracts/ViewModels";
|
|
||||||
import { useTabs } from "hooks/useTabs";
|
|
||||||
import React, { FormEvent, FunctionComponent, useState } from "react";
|
|
||||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
|
||||||
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
|
|
||||||
import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
|
|
||||||
import NotebookV2Tab from "../../Tabs/NotebookV2Tab";
|
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
|
||||||
|
|
||||||
export interface StringInputPanelProps {
|
|
||||||
closePanel: () => void;
|
|
||||||
errorMessage: string;
|
|
||||||
inProgressMessage: string;
|
|
||||||
successMessage: string;
|
|
||||||
inputLabel: string;
|
|
||||||
paneTitle: string;
|
|
||||||
submitButtonLabel: string;
|
|
||||||
defaultInput: string;
|
|
||||||
onSubmit: (notebookFile: NotebookContentItem, input: string) => Promise<NotebookContentItem>;
|
|
||||||
notebookFile: NotebookContentItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
|
|
||||||
closePanel,
|
|
||||||
errorMessage,
|
|
||||||
inProgressMessage,
|
|
||||||
successMessage,
|
|
||||||
inputLabel,
|
|
||||||
paneTitle,
|
|
||||||
submitButtonLabel,
|
|
||||||
defaultInput,
|
|
||||||
onSubmit,
|
|
||||||
notebookFile,
|
|
||||||
}: StringInputPanelProps): JSX.Element => {
|
|
||||||
const [stringInput, setStringInput] = useState<string>(defaultInput);
|
|
||||||
const [formErrors, setFormErrors] = useState<string>("");
|
|
||||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const submit = async (): Promise<void> => {
|
|
||||||
if (stringInput === "") {
|
|
||||||
const errorMessage = "Please " + inputLabel;
|
|
||||||
setFormErrors(errorMessage);
|
|
||||||
logConsoleError("Error while " + paneTitle + " : " + errorMessage);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
setFormErrors("");
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearMessage = logConsoleProgress(`${inProgressMessage} ${stringInput}`);
|
|
||||||
try {
|
|
||||||
const newNotebookFile: NotebookContentItem = await onSubmit(notebookFile, stringInput);
|
|
||||||
logConsoleInfo(`${successMessage}: ${stringInput}`);
|
|
||||||
const originalPath = notebookFile.path;
|
|
||||||
|
|
||||||
const notebookTabs = useTabs
|
|
||||||
.getState()
|
|
||||||
.getTabs(
|
|
||||||
ViewModels.CollectionTabKind.NotebookV2,
|
|
||||||
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath),
|
|
||||||
);
|
|
||||||
notebookTabs.forEach((tab) => {
|
|
||||||
tab.tabTitle(newNotebookFile.name);
|
|
||||||
tab.tabPath(newNotebookFile.path);
|
|
||||||
(tab as NotebookV2Tab).notebookPath(newNotebookFile.path);
|
|
||||||
});
|
|
||||||
closePanel();
|
|
||||||
} catch (reason) {
|
|
||||||
let error = reason;
|
|
||||||
if (reason instanceof Error) {
|
|
||||||
error = reason.message;
|
|
||||||
} else if (typeof reason === "object") {
|
|
||||||
error = JSON.stringify(reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reason?.response?.message) {
|
|
||||||
error += `. ${reason.response.message}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFormErrors(errorMessage);
|
|
||||||
logConsoleError(`${errorMessage} ${stringInput}: ${error}`);
|
|
||||||
} finally {
|
|
||||||
setIsExecuting(false);
|
|
||||||
clearMessage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const props: RightPaneFormProps = {
|
|
||||||
formError: formErrors,
|
|
||||||
isExecuting: isExecuting,
|
|
||||||
submitButtonText: submitButtonLabel,
|
|
||||||
onSubmit: submit,
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<RightPaneForm {...props}>
|
|
||||||
<div className="paneMainContent">
|
|
||||||
<TextField
|
|
||||||
label={inputLabel}
|
|
||||||
name="collectionIdConfirmation"
|
|
||||||
value={stringInput}
|
|
||||||
autoFocus
|
|
||||||
required
|
|
||||||
onChange={(event: FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) =>
|
|
||||||
setStringInput(newValue)
|
|
||||||
}
|
|
||||||
aria-label={inputLabel}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</RightPaneForm>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user