cosmos-explorer/src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx
Laurent Nguyen 861042c27e
Fix bug publish screenshot (#762)
[Preview this branch](https://cosmos-explorer-preview.azurewebsites.net/pull/762?feature.someFeatureFlagYouMightNeed=true)

The main change in this PR fixes the snapshot functionality in the Publish pane-related components. Because the code cell outputs are now rendered in their own iframes for security reasons, a single snapshot of the notebook is no longer possible: each cell output takes its own snapshot and the snapshots are collated on the main notebook snapshot.
- Move the snapshot functionality to notebook components: this removes the reference of the notebook DOM node that we must pass to the Publish pane via explorer.
- Add slice in the state and actions in notebook redux for notebook snapshot requests and result
- Add post robot message to take snapshots and receive results
- Add logic in `NotebookRenderer` to wait for all output snapshots done before taking the main one collating.
- Use `zustand` to share snapshot between Redux world and React world. This solves the issue of keeping the `PanelContainer` component generic, while being able to update its children (`PublishPanel` component) with the new snapshot.

Additional changes:
- Add `local()` in `@font-face` to check if font is already installed before downloading the font (must be done for Safari, but not Edge/Chrome)
- Add "Export output to image" menu item in notebook cell, since each cell output can take its own snapshot (which can be downloaded)
![image](https://user-images.githubusercontent.com/21954022/117454706-b5f16600-af46-11eb-8535-6bf99f3d9170.png)
2021-05-11 18:24:05 +00:00

328 lines
8.6 KiB
TypeScript

import { CellId, CellType, ImmutableNotebook } from "@nteract/commutable";
// Vendor modules
import {
actions,
AppState,
ContentRef,
DocumentRecordProps,
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 * as Immutable from "immutable";
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 { 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;
}
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: any) {
return {
name,
path,
last_modified: new Date(),
created: "",
content,
format: "json",
mimetype: null as any,
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 setContent(name: string, content: any): 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 {
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) {
console.log("No error");
return false;
}
return selectors.notebook.isDirty(content.model as Immutable.RecordOf<DocumentRecordProps>);
}
/**
* 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();
}
}