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:
jawelton74
2026-06-22 09:36:47 -07:00
committed by GitHub
parent 0e175c8a9c
commit 8e90672ff5
109 changed files with 142 additions and 14512 deletions
@@ -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,
};
}
}
@@ -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
View File
@@ -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 = (
-33
View File
@@ -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(".");
}
-20
View File
@@ -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;
}
-287
View File
@@ -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",
};
}
}
@@ -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,
};
}
}
+1 -88
View File
@@ -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 -227
View File
@@ -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);
@@ -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,
);
});
});
});
+1 -27
View File
@@ -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();
-20
View File
@@ -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));
}
@@ -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
+3 -27
View File
@@ -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) => {
-62
View File
@@ -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,
});
}
}
-381
View File
@@ -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);
};
}
-41
View File
@@ -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();
}
}
-46
View File
@@ -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;
+7 -120
View File
@@ -106,15 +106,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
type: NotebookContentItemType.Directory,
};
// Only if notebook server is available we can refresh
if (useNotebook.getState().notebookServerInfo?.notebookServerEndpoint) {
refreshTasks.push(
this.container.refreshContentItem(this.myNotebooksContentRoot).then(() => {
this.triggerRender();
this.traceMyNotebookTreeInfo();
}),
);
}
this.gitHubNotebooksContentRoot = {
name: ResourceTreeAdapter.GitHubReposTitle,
path: ResourceTreeAdapter.PseudoDirPath,
@@ -490,117 +481,17 @@ export class ResourceTreeAdapter implements ReactAdapter {
(activeTab as any).notebookPath() === item.path
);
},
contextMenu: createFileContextMenu && this.createFileContextMenu(item),
contextMenu: createFileContextMenu && this.createFileContextMenu(),
data: item,
};
}
private createFileContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] {
let items: TreeNodeMenuItem[] = [
{
label: "Rename",
iconSrc: NotebookIcon,
onClick: () => this.container.renameNotebook(item),
},
{
label: "Delete",
iconSrc: DeleteIcon,
onClick: () => {
useDialog
.getState()
.showOkCancelModalDialog(
"Confirm delete",
`Are you sure you want to delete "${item.name}"`,
"Delete",
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
"Cancel",
undefined,
);
},
},
{
label: "Copy to ...",
iconSrc: CopyIcon,
onClick: () => this.copyNotebook(item),
},
{
label: "Download",
iconSrc: NotebookIcon,
onClick: () => this.container.downloadFile(item),
},
];
if (item.type === NotebookContentItemType.Notebook) {
// Additional notebook-specific context menu items can be added here
}
// "Copy to ..." isn't needed if github locations are not available
if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
items = items.filter((item) => item.label !== "Copy to ...");
}
return items;
private createFileContextMenu(): TreeNodeMenuItem[] {
return [];
}
private copyNotebook = async (item: NotebookContentItem) => {
const content = await this.container.readFile(item);
if (content) {
this.container.copyNotebook(item.name, content);
}
};
private createDirectoryContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] {
let items: TreeNodeMenuItem[] = [
{
label: "Refresh",
iconSrc: RefreshIcon,
onClick: () => this.container.refreshContentItem(item).then(() => this.triggerRender()),
},
{
label: "Delete",
iconSrc: DeleteIcon,
onClick: () => {
useDialog
.getState()
.showOkCancelModalDialog(
"Confirm delete",
`Are you sure you want to delete "${item.name}?"`,
"Delete",
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
"Cancel",
undefined,
);
},
},
{
label: "Rename",
iconSrc: NotebookIcon,
onClick: () => this.container.renameNotebook(item),
},
{
label: "New Directory",
iconSrc: NewNotebookIcon,
onClick: () => this.container.onCreateDirectory(item),
},
];
//disallow renaming of temporary notebook workspace
if (item?.path === useNotebook.getState().notebookBasePath) {
items = items.filter((item) => item.label !== "Rename");
}
// For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File"
if (GitHubUtils.fromContentUri(item.path)) {
items = items.filter(
(item) =>
item.label !== "Delete" &&
item.label !== "Rename" &&
item.label !== "New Directory" &&
item.label !== "Upload File",
);
}
return items;
private createDirectoryContextMenu(): TreeNodeMenuItem[] {
return [];
}
private buildNotebookDirectoryNode(
@@ -615,11 +506,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
className: "notebookHeader",
isAlphaSorted: true,
isLeavesParentsSeparate: true,
onClick: () => {
if (!item.children) {
this.container.refreshContentItem(item).then(() => this.triggerRender());
}
},
onClick: undefined,
isSelected: () => {
const activeTab = useTabs.getState().activeTab;
return (
@@ -633,7 +520,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
},
contextMenu:
createDirectoryContextMenu && item.path !== ResourceTreeAdapter.PseudoDirPath
? this.createDirectoryContextMenu(item)
? this.createDirectoryContextMenu()
: undefined,
data: item,
children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu),