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)
This commit is contained in:
Laurent Nguyen
2021-05-11 20:24:05 +02:00
committed by GitHub
parent 4ed8fe9e7d
commit 861042c27e
24 changed files with 683 additions and 222 deletions

View File

@@ -1,6 +1,6 @@
import { CellId } from "@nteract/commutable";
import { CellType } from "@nteract/commutable/src";
import { actions, ContentRef } from "@nteract/core";
import { actions, ContentRef, selectors } from "@nteract/core";
import { Cells, CodeCell, RawCell } from "@nteract/stateful-components";
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
@@ -12,6 +12,8 @@ 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 { AzureTheme } from "./AzureTheme";
import "./base.css";
import CellCreator from "./decorators/CellCreator";
@@ -32,10 +34,18 @@ export interface NotebookRendererBaseProps {
}
interface NotebookRendererDispatchProps {
updateNotebookParentDomElt: (contentRef: ContentRef, parentElt: HTMLElement) => void;
storeNotebookSnapshot: (imageSrc: string, requestId: string) => void;
notebookSnapshotError: (error: string) => void;
}
type NotebookRendererProps = NotebookRendererBaseProps & NotebookRendererDispatchProps;
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 = () => (
@@ -60,27 +70,37 @@ const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, child
class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
private notebookRendererRef = React.createRef<HTMLDivElement>();
constructor(props: NotebookRendererProps) {
super(props);
this.state = {
hoveredCellId: undefined,
};
}
componentDidMount() {
if (!userContext.features.sandboxNotebookOutputs) {
loadTransform(this.props as any);
}
this.props.updateNotebookParentDomElt(this.props.contentRef, this.notebookRendererRef.current);
}
componentDidUpdate() {
this.props.updateNotebookParentDomElt(this.props.contentRef, this.notebookRendererRef.current);
}
componentWillUnmount() {
this.props.updateNotebookParentDomElt(this.props.contentRef, undefined);
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 {
@@ -156,28 +176,40 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
}
}
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 }) => {
return dispatch(
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) =>
dispatch(
actions.addTransform({
mediaType: transform.MIMETYPE,
component: transform,
})
);
},
updateNotebookParentDomElt: (contentRef: ContentRef, parentElt: HTMLElement) => {
return dispatch(
cdbActions.UpdateNotebookParentDomElt({
contentRef,
parentElt,
})
);
},
),
storeNotebookSnapshot: (imageSrc: string, requestId: string) =>
dispatch(cdbActions.storeNotebookSnapshot({ imageSrc, requestId })),
notebookSnapshotError: (error: string) => dispatch(cdbActions.notebookSnapshotError({ error })),
};
};
return mapDispatchToProps;
};
export default connect(null, makeMapDispatchToProps)(BaseNotebookRenderer);
export default connect(makeMapStateToProps, makeMapDispatchToProps)(BaseNotebookRenderer);