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:
@@ -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;
|
||||
onGraphDocumentsClick(): void;
|
||||
onMongoDBDocumentsClick(): void;
|
||||
onSchemaAnalyzerClick(): void;
|
||||
openTab(): 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],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"parameters": [Function],
|
||||
},
|
||||
},
|
||||
@@ -252,7 +251,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"parameters": [Function],
|
||||
},
|
||||
},
|
||||
@@ -479,7 +477,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"parameters": [Function],
|
||||
},
|
||||
},
|
||||
@@ -551,7 +548,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"parameters": [Function],
|
||||
},
|
||||
}
|
||||
@@ -720,7 +716,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"parameters": [Function],
|
||||
},
|
||||
},
|
||||
@@ -792,7 +787,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"refreshNotebookList": [Function],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"parameters": [Function],
|
||||
},
|
||||
}
|
||||
|
||||
+4
-344
@@ -24,7 +24,7 @@ import { AuthType } from "../AuthType";
|
||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
|
||||
import { getErrorMessage, getErrorStack } from "../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { QueriesClient } from "../Common/QueriesClient";
|
||||
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 { updateUserContext, userContext } from "../UserContext";
|
||||
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
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 { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { ReactTabKind, useTabs } from "../hooks/useTabs";
|
||||
import "./ComponentRegisterer";
|
||||
import { DialogProps, useDialog } from "./Controls/Dialog";
|
||||
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 { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||
import { useNotebook } from "./Notebook/useNotebook";
|
||||
import { AddCollectionPanel } from "./Panes/AddCollectionPanel/AddCollectionPanel";
|
||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
||||
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
||||
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
|
||||
import TabsBase from "./Tabs/TabsBase";
|
||||
import TerminalTab from "./Tabs/TerminalTab";
|
||||
import Database from "./Tree/Database";
|
||||
@@ -93,10 +85,6 @@ export default class Explorer {
|
||||
public notebookManager?: NotebookManager;
|
||||
|
||||
private _isInitializingNotebooks: boolean;
|
||||
private notebookToImport: {
|
||||
name: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
||||
public phoenixClient: PhoenixClient;
|
||||
@@ -652,313 +640,11 @@ export default class Explorer {
|
||||
}
|
||||
}
|
||||
|
||||
public uploadFile(
|
||||
name: string,
|
||||
content: string,
|
||||
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);
|
||||
}
|
||||
private refreshNotebookList = (): Promise<void> => {
|
||||
// Notebook authoring and listing have been removed.
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
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 {
|
||||
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 {
|
||||
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane onUpload={onUpload} />);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,6 @@ import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||
|
||||
export enum Type {
|
||||
OpenCollection = "OpenCollection",
|
||||
OpenNotebook = "OpenNotebook",
|
||||
}
|
||||
|
||||
export interface OpenNotebookItem {
|
||||
type: Type.OpenNotebook;
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface OpenCollectionItem {
|
||||
@@ -19,7 +12,7 @@ export interface OpenCollectionItem {
|
||||
collectionId: string;
|
||||
}
|
||||
|
||||
type Item = OpenNotebookItem | OpenCollectionItem;
|
||||
type Item = OpenCollectionItem;
|
||||
|
||||
const itemsMaxNumber: number = 5;
|
||||
|
||||
@@ -42,14 +35,14 @@ const migrateOldData = () => {
|
||||
componentName: AppStateComponentNames.MostRecentActivity,
|
||||
globalAccountName: accountName,
|
||||
},
|
||||
itemsMap[accountId].map((item) => {
|
||||
if ((item.type as unknown as number) === 0) {
|
||||
item.type = Type.OpenCollection;
|
||||
} else if ((item.type as unknown as number) === 1) {
|
||||
item.type = Type.OpenNotebook;
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
itemsMap[accountId]
|
||||
.filter((item) => (item.type as unknown as number) !== 1 && (item.type as string) !== "OpenNotebook")
|
||||
.map((item) => {
|
||||
if ((item.type as unknown as number) === 0) {
|
||||
item.type = Type.OpenCollection;
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -97,7 +90,7 @@ export const getItems = (accountName: string): Item[] => {
|
||||
componentName: AppStateComponentNames.MostRecentActivity,
|
||||
globalAccountName: accountName,
|
||||
}) as Item[]) || []
|
||||
);
|
||||
).filter((item) => item.type in Type);
|
||||
};
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
import { ImmutableNotebook } from "@nteract/commutable";
|
||||
import type { IContentProvider } from "@nteract/core";
|
||||
import React from "react";
|
||||
import { contents } from "rx-jupyter";
|
||||
import { Areas, HttpStatusCodes } from "../../Common/Constants";
|
||||
import { HttpStatusCodes } from "../../Common/Constants";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { GitHubClient } from "../../GitHub/GitHubClient";
|
||||
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
|
||||
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
|
||||
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||
import { JunoClient } from "../../Juno/JunoClient";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { useDialog } from "../Controls/Dialog";
|
||||
import Explorer from "../Explorer";
|
||||
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
|
||||
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||
import { InMemoryContentProvider } from "./NotebookComponent/ContentProviders/InMemoryContentProvider";
|
||||
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
|
||||
import { NotebookContainerClient } from "./NotebookContainerClient";
|
||||
import { NotebookContentClient } from "./NotebookContentClient";
|
||||
import { SchemaAnalyzerNotebook } from "./SchemaAnalyzer/SchemaAnalyzerUtils";
|
||||
import { useNotebook } from "./useNotebook";
|
||||
|
||||
type NotebookPaneContent = string | ImmutableNotebook;
|
||||
|
||||
export type { NotebookPaneContent };
|
||||
|
||||
export interface NotebookManagerOptions {
|
||||
container: Explorer;
|
||||
resourceTree: ResourceTreeAdapter;
|
||||
@@ -44,12 +29,8 @@ export default class NotebookManager {
|
||||
private params: NotebookManagerOptions;
|
||||
public junoClient: JunoClient;
|
||||
|
||||
public notebookContentProvider: IContentProvider;
|
||||
public notebookClient: NotebookContainerClient;
|
||||
public notebookContentClient: NotebookContentClient;
|
||||
|
||||
private inMemoryContentProvider: InMemoryContentProvider;
|
||||
private gitHubContentProvider: GitHubContentProvider;
|
||||
public gitHubOAuthService: GitHubOAuthService;
|
||||
public gitHubClient: GitHubClient;
|
||||
|
||||
@@ -60,30 +41,10 @@ export default class NotebookManager {
|
||||
this.gitHubOAuthService = new GitHubOAuthService(this.junoClient);
|
||||
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.params.container.initNotebooks(userContext?.databaseAccount),
|
||||
);
|
||||
|
||||
this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider);
|
||||
|
||||
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
|
||||
this.gitHubClient.setToken(token?.access_token);
|
||||
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
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
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 { NotebookUtil } from "./NotebookUtil";
|
||||
|
||||
@@ -19,55 +9,6 @@ const notebookPath = `${folderPath}/${notebookName}`;
|
||||
const gitHubFolderUri = GitHubUtils.toContentUri("owner", "repo", "branch", folderPath);
|
||||
const gitHubFileUri = GitHubUtils.toContentUri("owner", "repo", "branch", filePath);
|
||||
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("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 StringUtils from "../../Utils/StringUtils";
|
||||
import * as InMemoryContentProviderUtils from "../Notebook/NotebookComponent/ContentProviders/InMemoryContentProviderUtils";
|
||||
import { SnapshotFragment } from "./NotebookComponent/types";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||
|
||||
// Must match rx-jupyter' FileType
|
||||
export type FileType = "directory" | "file" | "notebook";
|
||||
export enum NotebookContentProviderType {
|
||||
GitHubContentProviderType,
|
||||
InMemoryContentProviderType,
|
||||
JupyterContentProviderType,
|
||||
}
|
||||
|
||||
// Utilities for notebooks
|
||||
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.
|
||||
*/
|
||||
@@ -63,25 +50,6 @@ export class NotebookUtil {
|
||||
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 {
|
||||
const contentInfo = GitHubUtils.fromContentUri(path);
|
||||
if (contentInfo) {
|
||||
@@ -132,18 +100,6 @@ export class NotebookUtil {
|
||||
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 {
|
||||
const contentInfo = GitHubUtils.fromContentUri(path);
|
||||
if (contentInfo) {
|
||||
@@ -164,186 +120,4 @@ export class NotebookUtil {
|
||||
const basePath = path.split(contentName).shift();
|
||||
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 { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels";
|
||||
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 { userContext } from "../../UserContext";
|
||||
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||
@@ -234,32 +234,6 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
galleryContentRoot,
|
||||
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 => {
|
||||
const gitHubNotebooksContentRoot = cloneDeep(get().gitHubNotebooksContentRoot);
|
||||
|
||||
@@ -25,7 +25,6 @@ describe("OpenActions", () => {
|
||||
collection.expandCollection = jest.fn();
|
||||
collection.onDocumentDBDocumentsClick = jest.fn();
|
||||
collection.onMongoDBDocumentsClick = jest.fn();
|
||||
collection.onSchemaAnalyzerClick = jest.fn();
|
||||
collection.onTableEntitiesClick = jest.fn();
|
||||
collection.onGraphDocumentsClick = jest.fn();
|
||||
collection.onNewQueryClick = jest.fn();
|
||||
|
||||
@@ -107,14 +107,6 @@ function openCollectionTab(
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.SchemaAnalyzer ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
|
||||
) {
|
||||
collection.onSchemaAnalyzerClick();
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
action.tabKind === ActionContracts.TabKind.TableEntities ||
|
||||
action.tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]
|
||||
@@ -232,17 +224,5 @@ export function handleOpenAction(
|
||||
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;
|
||||
}
|
||||
|
||||
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],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [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],
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [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
@@ -31,7 +31,6 @@ import LinkIcon from "../../../images/Link_blue.svg";
|
||||
import PowerShellIcon from "../../../images/PowerShell.svg";
|
||||
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
|
||||
import VisualStudioIcon from "../../../images/VisualStudio.svg";
|
||||
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
|
||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { userContext } from "../../UserContext";
|
||||
@@ -471,33 +470,10 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
|
||||
};
|
||||
};
|
||||
|
||||
const decorateOpenNotebookActivity = (activity: MostRecentActivity.OpenNotebookItem): SplashScreenItem => {
|
||||
return {
|
||||
info: activity.path,
|
||||
iconSrc: NotebookIcon,
|
||||
title: activity.name,
|
||||
description: t(Keys.splashScreen.sections.notebook),
|
||||
onClick: () => {
|
||||
const notebookItem = container.createNotebookContentItemFile(activity.name, activity.path);
|
||||
notebookItem && container.openNotebook(notebookItem);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const createRecentItems = (): SplashScreenItem[] => {
|
||||
return MostRecentActivity.getItems(userContext.databaseAccount?.name).map((activity) => {
|
||||
switch (activity.type) {
|
||||
default: {
|
||||
const unknownActivity: never = activity;
|
||||
throw new Error(`Unknown activity: ${unknownActivity}`);
|
||||
}
|
||||
case MostRecentActivity.Type.OpenNotebook:
|
||||
return decorateOpenNotebookActivity(activity);
|
||||
|
||||
case MostRecentActivity.Type.OpenCollection:
|
||||
return decorateOpenCollectionActivity(activity);
|
||||
}
|
||||
});
|
||||
return MostRecentActivity.getItems(userContext.databaseAccount?.name).map((activity) =>
|
||||
decorateOpenCollectionActivity(activity),
|
||||
);
|
||||
};
|
||||
|
||||
const onSplashScreenItemKeyPress = (event: React.KeyboardEvent, callback: () => void) => {
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Areas } from "../../Common/Constants";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import Explorer from "../Explorer";
|
||||
import { NotebookClientV2 } from "../Notebook/NotebookClientV2";
|
||||
import { useNotebook } from "../Notebook/useNotebook";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
export interface NotebookTabBaseOptions extends ViewModels.TabOptions {
|
||||
account: DataModels.DatabaseAccount;
|
||||
masterKey: string;
|
||||
container: Explorer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every notebook-based tab inherits from this class. It holds the static reference to a notebook client (singleton)
|
||||
* Re-initiating the constructor when ever a new container got allocated.
|
||||
*/
|
||||
export default class NotebookTabBase extends TabsBase {
|
||||
protected static clientManager: NotebookClientV2;
|
||||
protected container: Explorer;
|
||||
|
||||
constructor(options: NotebookTabBaseOptions) {
|
||||
super(options);
|
||||
|
||||
this.container = options.container;
|
||||
|
||||
useNotebook.subscribe(
|
||||
() => {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint) {
|
||||
NotebookTabBase.clientManager = undefined;
|
||||
}
|
||||
},
|
||||
(state) => state.notebookServerInfo,
|
||||
);
|
||||
if (!NotebookTabBase.clientManager) {
|
||||
NotebookTabBase.clientManager = new NotebookClientV2({
|
||||
connectionInfo: useNotebook.getState().notebookServerInfo,
|
||||
databaseAccountName: userContext?.databaseAccount?.name,
|
||||
defaultExperience: userContext.apiType,
|
||||
contentProvider: this.container.notebookManager?.notebookContentProvider,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override base behavior
|
||||
*/
|
||||
public getContainer(): Explorer {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
protected traceTelemetry(actionType: number): void {
|
||||
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
||||
dataExplorerArea: Areas.Notebook,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,381 +0,0 @@
|
||||
import { stringifyNotebook, toJS } from "@nteract/commutable";
|
||||
import * as ko from "knockout";
|
||||
import * as Q from "q";
|
||||
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
|
||||
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
|
||||
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
|
||||
import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg";
|
||||
import PasteIcon from "../../../images/notebook/Notebook-paste.svg";
|
||||
import RestartIcon from "../../../images/notebook/Notebook-restart.svg";
|
||||
import RunAllIcon from "../../../images/notebook/Notebook-run-all.svg";
|
||||
import RunIcon from "../../../images/notebook/Notebook-run.svg";
|
||||
import { default as InterruptKernelIcon, default as KillKernelIcon } from "../../../images/notebook/Notebook-stop.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils";
|
||||
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import { useDialog } from "../Controls/Dialog";
|
||||
import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory";
|
||||
import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2";
|
||||
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
|
||||
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||
import { useNotebook } from "../Notebook/useNotebook";
|
||||
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
|
||||
|
||||
export interface NotebookTabOptions extends NotebookTabBaseOptions {
|
||||
notebookContentItem: NotebookContentItem;
|
||||
}
|
||||
|
||||
export default class NotebookTabV2 extends NotebookTabBase {
|
||||
public readonly html = '<div data-bind="react:notebookComponentAdapter" style="height: 100%"></div>';
|
||||
public notebookPath: ko.Observable<string>;
|
||||
private notebookComponentAdapter: NotebookComponentAdapter;
|
||||
|
||||
constructor(options: NotebookTabOptions) {
|
||||
super(options);
|
||||
|
||||
this.container = options.container;
|
||||
this.notebookPath = ko.observable(options.notebookContentItem.path);
|
||||
useNotebook.subscribe(
|
||||
() => logConsoleInfo("New notebook server info received."),
|
||||
(state) => state.notebookServerInfo,
|
||||
);
|
||||
this.notebookComponentAdapter = new NotebookComponentAdapter({
|
||||
contentItem: options.notebookContentItem,
|
||||
notebooksBasePath: useNotebook.getState().notebookBasePath,
|
||||
notebookClient: NotebookTabBase.clientManager,
|
||||
onUpdateKernelInfo: this.onKernelUpdate,
|
||||
});
|
||||
}
|
||||
/*
|
||||
* Hard cleaning the workspace(Closing tabs connected with old container connection) when new container got allocated.
|
||||
*/
|
||||
public onCloseTabButtonClick(hardClose = false): Q.Promise<any> {
|
||||
const cleanup = () => {
|
||||
this.notebookComponentAdapter.notebookShutdown();
|
||||
super.onCloseTabButtonClick();
|
||||
};
|
||||
|
||||
if (this.notebookComponentAdapter.isContentDirty() && hardClose === false) {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkCancelModalDialog(
|
||||
"Close without saving?",
|
||||
`File has unsaved changes, close without saving?`,
|
||||
"Close",
|
||||
cleanup,
|
||||
"Cancel",
|
||||
undefined,
|
||||
);
|
||||
return Q.resolve(null);
|
||||
} else {
|
||||
cleanup();
|
||||
return Q.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
public async reconfigureServiceEndpoints() {
|
||||
if (!this.notebookComponentAdapter) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName());
|
||||
}
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
||||
const isNotebookUntrusted = this.notebookComponentAdapter.isNotebookUntrusted();
|
||||
|
||||
const runBtnTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined;
|
||||
|
||||
const saveLabel = "Save";
|
||||
const copyToLabel = "Copy to ...";
|
||||
const kernelLabel = "No Kernel";
|
||||
const runLabel = "Run";
|
||||
const runActiveCellLabel = "Run Active Cell";
|
||||
const runAllLabel = "Run All";
|
||||
const interruptKernelLabel = "Interrupt Kernel";
|
||||
const killKernelLabel = "Halt Kernel";
|
||||
const restartKernelLabel = "Restart Kernel";
|
||||
const clearLabel = "Clear outputs";
|
||||
const newCellLabel = "New Cell";
|
||||
const cellTypeLabel = "Cell Type";
|
||||
const codeLabel = "Code";
|
||||
const markdownLabel = "Markdown";
|
||||
const rawLabel = "Raw";
|
||||
const copyLabel = "Copy";
|
||||
const cutLabel = "Cut";
|
||||
const pasteLabel = "Paste";
|
||||
const cellCodeType = "code";
|
||||
const cellMarkdownType = "markdown";
|
||||
const cellRawType = "raw";
|
||||
|
||||
const saveButtonChildren = [];
|
||||
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
||||
saveButtonChildren.push({
|
||||
iconName: copyToLabel,
|
||||
onCommandClick: () => this.copyNotebook(),
|
||||
commandButtonLabel: copyToLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: copyToLabel,
|
||||
});
|
||||
}
|
||||
|
||||
let buttons: CommandButtonComponentProps[] = [
|
||||
{
|
||||
iconSrc: SaveIcon,
|
||||
iconAlt: saveLabel,
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
|
||||
commandButtonLabel: saveLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: saveLabel,
|
||||
children: saveButtonChildren.length && [
|
||||
{
|
||||
iconName: "Save",
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
|
||||
commandButtonLabel: saveLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: saveLabel,
|
||||
},
|
||||
...saveButtonChildren,
|
||||
],
|
||||
},
|
||||
{
|
||||
iconSrc: null,
|
||||
iconAlt: kernelLabel,
|
||||
onCommandClick: () => {},
|
||||
commandButtonLabel: null,
|
||||
hasPopup: false,
|
||||
disabled: availableKernels.length < 1,
|
||||
isDropdown: true,
|
||||
dropdownPlaceholder: kernelLabel,
|
||||
dropdownSelectedKey: this.notebookComponentAdapter.getSelectedKernelName(), //this.currentKernelName,
|
||||
dropdownWidth: 100,
|
||||
children: availableKernels.map(
|
||||
(kernel: KernelSpecsDisplay) =>
|
||||
({
|
||||
iconSrc: null,
|
||||
iconAlt: kernel.displayName,
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebookChangeKernel(kernel.name),
|
||||
commandButtonLabel: kernel.displayName,
|
||||
dropdownItemKey: kernel.name,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: kernel.displayName,
|
||||
}) as CommandButtonComponentProps,
|
||||
),
|
||||
ariaLabel: kernelLabel,
|
||||
},
|
||||
{
|
||||
iconSrc: RunIcon,
|
||||
iconAlt: runLabel,
|
||||
onCommandClick: () => {
|
||||
this.notebookComponentAdapter.notebookRunAndAdvance();
|
||||
this.traceTelemetry(Action.ExecuteCell);
|
||||
},
|
||||
commandButtonLabel: runLabel,
|
||||
tooltipText: runBtnTooltip,
|
||||
ariaLabel: runLabel,
|
||||
hasPopup: false,
|
||||
disabled: isNotebookUntrusted,
|
||||
children: [
|
||||
{
|
||||
iconSrc: RunIcon,
|
||||
iconAlt: runActiveCellLabel,
|
||||
onCommandClick: () => {
|
||||
this.notebookComponentAdapter.notebookRunAndAdvance();
|
||||
this.traceTelemetry(Action.ExecuteCell);
|
||||
},
|
||||
commandButtonLabel: runActiveCellLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: runActiveCellLabel,
|
||||
},
|
||||
{
|
||||
iconSrc: RunAllIcon,
|
||||
iconAlt: runAllLabel,
|
||||
onCommandClick: () => {
|
||||
this.notebookComponentAdapter.notebookRunAll();
|
||||
this.traceTelemetry(Action.ExecuteAllCells);
|
||||
},
|
||||
commandButtonLabel: runAllLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: runAllLabel,
|
||||
},
|
||||
{
|
||||
iconSrc: InterruptKernelIcon,
|
||||
iconAlt: interruptKernelLabel,
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebookInterruptKernel(),
|
||||
commandButtonLabel: interruptKernelLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: interruptKernelLabel,
|
||||
},
|
||||
{
|
||||
iconSrc: KillKernelIcon,
|
||||
iconAlt: killKernelLabel,
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebookKillKernel(),
|
||||
commandButtonLabel: killKernelLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: killKernelLabel,
|
||||
},
|
||||
{
|
||||
iconSrc: RestartIcon,
|
||||
iconAlt: restartKernelLabel,
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebookRestartKernel(),
|
||||
commandButtonLabel: restartKernelLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: restartKernelLabel,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
iconSrc: ClearAllOutputsIcon,
|
||||
iconAlt: clearLabel,
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebookClearAllOutputs(),
|
||||
commandButtonLabel: clearLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: clearLabel,
|
||||
},
|
||||
{
|
||||
iconSrc: NewCellIcon,
|
||||
iconAlt: newCellLabel,
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebookInsertBelow(),
|
||||
commandButtonLabel: newCellLabel,
|
||||
ariaLabel: newCellLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
},
|
||||
CommandBarComponentButtonFactory.createDivider(),
|
||||
{
|
||||
iconSrc: null,
|
||||
iconAlt: null,
|
||||
onCommandClick: () => {},
|
||||
commandButtonLabel: null,
|
||||
ariaLabel: cellTypeLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
isDropdown: true,
|
||||
dropdownPlaceholder: cellTypeLabel,
|
||||
dropdownSelectedKey: this.notebookComponentAdapter.getActiveCellTypeStr(),
|
||||
dropdownWidth: 110,
|
||||
children: [
|
||||
{
|
||||
iconSrc: null,
|
||||
iconAlt: null,
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellCodeType),
|
||||
commandButtonLabel: codeLabel,
|
||||
ariaLabel: codeLabel,
|
||||
dropdownItemKey: cellCodeType,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: null,
|
||||
iconAlt: null,
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellMarkdownType),
|
||||
commandButtonLabel: markdownLabel,
|
||||
ariaLabel: markdownLabel,
|
||||
dropdownItemKey: cellMarkdownType,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: null,
|
||||
iconAlt: null,
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellRawType),
|
||||
commandButtonLabel: rawLabel,
|
||||
ariaLabel: rawLabel,
|
||||
dropdownItemKey: cellRawType,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
iconSrc: CopyIcon,
|
||||
iconAlt: copyLabel,
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebokCopy(),
|
||||
commandButtonLabel: copyLabel,
|
||||
ariaLabel: copyLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
children: [
|
||||
{
|
||||
iconSrc: CopyIcon,
|
||||
iconAlt: copyLabel,
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebokCopy(),
|
||||
commandButtonLabel: copyLabel,
|
||||
ariaLabel: copyLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: CutIcon,
|
||||
iconAlt: cutLabel,
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebookCut(),
|
||||
commandButtonLabel: cutLabel,
|
||||
ariaLabel: cutLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
iconSrc: PasteIcon,
|
||||
iconAlt: pasteLabel,
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebookPaste(),
|
||||
commandButtonLabel: pasteLabel,
|
||||
ariaLabel: pasteLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
// TODO: Uncomment when undo/redo is reimplemented in nteract
|
||||
];
|
||||
return buttons;
|
||||
}
|
||||
|
||||
protected buildCommandBarOptions(): void {
|
||||
this.updateNavbarWithTabsButtons();
|
||||
}
|
||||
|
||||
private onKernelUpdate = async () => {
|
||||
await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName()).catch((reason) => {
|
||||
/* Erroring is ok here */
|
||||
});
|
||||
this.updateNavbarWithTabsButtons();
|
||||
};
|
||||
|
||||
private async configureServiceEndpoints(kernelName: string) {
|
||||
const notebookConnectionInfo = useNotebook.getState().notebookServerInfo;
|
||||
const sparkClusterConnectionInfo = useNotebook.getState().sparkClusterConnectionInfo;
|
||||
await NotebookConfigurationUtils.configureServiceEndpoints(
|
||||
this.notebookPath(),
|
||||
notebookConnectionInfo,
|
||||
kernelName,
|
||||
sparkClusterConnectionInfo,
|
||||
);
|
||||
}
|
||||
|
||||
private copyNotebook = () => {
|
||||
const notebookContent = this.notebookComponentAdapter.getContent();
|
||||
let content: string;
|
||||
if (typeof notebookContent.content === "string") {
|
||||
content = notebookContent.content;
|
||||
} else {
|
||||
content = stringifyNotebook(toJS(notebookContent.content));
|
||||
}
|
||||
|
||||
this.container.copyNotebook(notebookContent.name, content);
|
||||
};
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { SchemaAnalyzerAdapter } from "../Notebook/SchemaAnalyzer/SchemaAnalyzerAdapter";
|
||||
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
|
||||
|
||||
export default class SchemaAnalyzerTab extends NotebookTabBase {
|
||||
public readonly html = '<div data-bind="react:schemaAnalyzerAdapter" style="height: 100%"></div>';
|
||||
private schemaAnalyzerAdapter: SchemaAnalyzerAdapter;
|
||||
|
||||
constructor(options: NotebookTabBaseOptions) {
|
||||
super(options);
|
||||
this.schemaAnalyzerAdapter = new SchemaAnalyzerAdapter(
|
||||
{
|
||||
contentRef: undefined,
|
||||
notebookClient: NotebookTabBase.clientManager,
|
||||
},
|
||||
options.collection?.databaseId,
|
||||
options.collection?.id(),
|
||||
);
|
||||
}
|
||||
|
||||
public onActivate(): void {
|
||||
traceSuccess(
|
||||
Action.Tab,
|
||||
{
|
||||
databaseName: this.collection?.databaseId,
|
||||
collectionName: this.collection?.id,
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: "Schema",
|
||||
},
|
||||
this.onLoadStartKey,
|
||||
);
|
||||
|
||||
super.onActivate();
|
||||
}
|
||||
|
||||
protected buildCommandBarOptions(): void {
|
||||
this.updateNavbarWithTabsButtons();
|
||||
}
|
||||
}
|
||||
@@ -548,52 +548,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
}
|
||||
};
|
||||
|
||||
public onSchemaAnalyzerClick = async () => {
|
||||
if (useNotebook.getState().isPhoenixFeatures) {
|
||||
await this.container.allocateContainer();
|
||||
}
|
||||
useSelectedNode.getState().setSelectedNode(this);
|
||||
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
|
||||
const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default;
|
||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||
description: "Schema node",
|
||||
databaseName: this.databaseId,
|
||||
collectionName: this.id(),
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
});
|
||||
|
||||
for (const tab of useTabs.getState().openedTabs) {
|
||||
if (
|
||||
tab instanceof SchemaAnalyzerTab &&
|
||||
tab.collection?.databaseId === this.databaseId &&
|
||||
tab.collection?.id() === this.id()
|
||||
) {
|
||||
return useTabs.getState().activateTab(tab);
|
||||
}
|
||||
}
|
||||
|
||||
const startKey = TelemetryProcessor.traceStart(Action.Tab, {
|
||||
databaseName: this.databaseId,
|
||||
collectionName: this.id(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: "Schema",
|
||||
});
|
||||
this.documentIds([]);
|
||||
useTabs.getState().activateNewTab(
|
||||
new SchemaAnalyzerTab({
|
||||
account: userContext.databaseAccount,
|
||||
masterKey: userContext.masterKey || "",
|
||||
container: this.container,
|
||||
tabKind: ViewModels.CollectionTabKind.SchemaAnalyzer,
|
||||
title: "Schema",
|
||||
tabPath: "",
|
||||
collection: this,
|
||||
node: this,
|
||||
onLoadStartKey: startKey,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
public onSettingsClick = async (): Promise<void> => {
|
||||
useSelectedNode.getState().setSelectedNode(this);
|
||||
const throughputCap = userContext.databaseAccount?.properties.capacity?.totalThroughputLimit;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user