mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-19 08:51:24 +00:00
Add CellOutputViewer for SandboxOutputs (#686)
* Initial commit * Optimizations * Optimize notebookOutputViewer bundle size by lazy loading transforms * Update package-lock.json * More optimizations * Updates * Fix unit test and other updates * Address feedback * Update package-lock.json * Update test snapshots * Fix build * Reduce cellOutputViewer bundle size * Renaming
This commit is contained in:
68
src/CellOutputViewer/CellOutputViewer.tsx
Normal file
68
src/CellOutputViewer/CellOutputViewer.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { createImmutableOutput, JSONObject, OnDiskOutput } from "@nteract/commutable";
|
||||
// import outputs individually to avoid increasing the bundle size
|
||||
import { KernelOutputError } from "@nteract/outputs/lib/components/kernel-output-error";
|
||||
import { Output } from "@nteract/outputs/lib/components/output";
|
||||
import { StreamText } from "@nteract/outputs/lib/components/stream-text";
|
||||
import { ContentRef } from "@nteract/types";
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import postRobot from "post-robot";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import "../../externals/iframeResizer.contentWindow.min.js"; // Required for iFrameResizer to work
|
||||
import "../Explorer/Notebook/NotebookRenderer/base.css";
|
||||
import "../Explorer/Notebook/NotebookRenderer/default.css";
|
||||
import { TransformMedia } from "./TransformMedia";
|
||||
|
||||
export interface CellOutputViewerProps {
|
||||
id: string;
|
||||
contentRef: ContentRef;
|
||||
hidden: boolean;
|
||||
expanded: boolean;
|
||||
outputs: OnDiskOutput[];
|
||||
onMetadataChange: (metadata: JSONObject, mediaType: string, index?: number) => void;
|
||||
}
|
||||
|
||||
const onInit = async () => {
|
||||
postRobot.on(
|
||||
"props",
|
||||
{
|
||||
window: window.parent,
|
||||
domain: window.location.origin,
|
||||
},
|
||||
(event) => {
|
||||
// Typescript definition for event is wrong. So read props by casting to <any>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const props = (event as any).data as CellOutputViewerProps;
|
||||
const outputs = (
|
||||
<div
|
||||
data-iframe-height
|
||||
className={`nteract-cell-outputs ${props.hidden ? "hidden" : ""} ${props.expanded ? "expanded" : ""}`}
|
||||
>
|
||||
{props.outputs?.map((output, index) => (
|
||||
<Output output={createImmutableOutput(output)} key={index}>
|
||||
<TransformMedia
|
||||
output_type={"display_data"}
|
||||
id={props.id}
|
||||
contentRef={props.contentRef}
|
||||
onMetadataChange={(metadata, mediaType) => props.onMetadataChange(metadata, mediaType, index)}
|
||||
/>
|
||||
<TransformMedia
|
||||
output_type={"execute_result"}
|
||||
id={props.id}
|
||||
contentRef={props.contentRef}
|
||||
onMetadataChange={(metadata, mediaType) => props.onMetadataChange(metadata, mediaType, index)}
|
||||
/>
|
||||
<KernelOutputError />
|
||||
<StreamText />
|
||||
</Output>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
ReactDOM.render(outputs, document.getElementById("cellOutput"));
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Entry point
|
||||
window.addEventListener("load", onInit);
|
||||
138
src/CellOutputViewer/TransformMedia.tsx
Normal file
138
src/CellOutputViewer/TransformMedia.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { ImmutableDisplayData, ImmutableExecuteResult, JSONObject } from "@nteract/commutable";
|
||||
// import outputs individually to avoid increasing the bundle size
|
||||
import { HTML } from "@nteract/outputs/lib/components/media/html";
|
||||
import { Image } from "@nteract/outputs/lib/components/media/image";
|
||||
import { JavaScript } from "@nteract/outputs/lib/components/media/javascript";
|
||||
import { Json } from "@nteract/outputs/lib/components/media/json";
|
||||
import { LaTeX } from "@nteract/outputs/lib/components/media/latex";
|
||||
import { Plain } from "@nteract/outputs/lib/components/media/plain";
|
||||
import { SVG } from "@nteract/outputs/lib/components/media/svg";
|
||||
import { ContentRef } from "@nteract/types";
|
||||
import React, { Suspense } from "react";
|
||||
|
||||
const EmptyTransform = (): JSX.Element => <></>;
|
||||
|
||||
const displayOrder = [
|
||||
"application/vnd.jupyter.widget-view+json",
|
||||
"application/vnd.vega.v5+json",
|
||||
"application/vnd.vega.v4+json",
|
||||
"application/vnd.vega.v3+json",
|
||||
"application/vnd.vega.v2+json",
|
||||
"application/vnd.vegalite.v4+json",
|
||||
"application/vnd.vegalite.v3+json",
|
||||
"application/vnd.vegalite.v2+json",
|
||||
"application/vnd.vegalite.v1+json",
|
||||
"application/geo+json",
|
||||
"application/vnd.plotly.v1+json",
|
||||
"text/vnd.plotly.v1+html",
|
||||
"application/x-nteract-model-debug+json",
|
||||
"application/vnd.dataresource+json",
|
||||
"application/vdom.v1+json",
|
||||
"application/json",
|
||||
"application/javascript",
|
||||
"text/html",
|
||||
"text/markdown",
|
||||
"text/latex",
|
||||
"image/svg+xml",
|
||||
"image/gif",
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"text/plain",
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const transformsById = new Map<string, React.ComponentType<any>>([
|
||||
["text/vnd.plotly.v1+html", React.lazy(() => import("@nteract/transform-plotly"))],
|
||||
["application/vnd.plotly.v1+json", React.lazy(() => import("@nteract/transform-plotly"))],
|
||||
["application/geo+json", EmptyTransform], // TODO: The geojson transform will likely need some work because of the basemap URL(s)
|
||||
["application/x-nteract-model-debug+json", React.lazy(() => import("@nteract/transform-model-debug"))],
|
||||
["application/vnd.dataresource+json", React.lazy(() => import("@nteract/data-explorer"))],
|
||||
["application/vnd.jupyter.widget-view+json", React.lazy(() => import("./transforms/WidgetDisplay"))],
|
||||
["application/vnd.vegalite.v1+json", React.lazy(() => import("./transforms/VegaLite1"))],
|
||||
["application/vnd.vegalite.v2+json", React.lazy(() => import("./transforms/VegaLite2"))],
|
||||
["application/vnd.vegalite.v3+json", React.lazy(() => import("./transforms/VegaLite3"))],
|
||||
["application/vnd.vegalite.v4+json", React.lazy(() => import("./transforms/VegaLite4"))],
|
||||
["application/vnd.vega.v2+json", React.lazy(() => import("./transforms/Vega2"))],
|
||||
["application/vnd.vega.v3+json", React.lazy(() => import("./transforms/Vega3"))],
|
||||
["application/vnd.vega.v4+json", React.lazy(() => import("./transforms/Vega4"))],
|
||||
["application/vnd.vega.v5+json", React.lazy(() => import("./transforms/Vega5"))],
|
||||
["application/vdom.v1+json", React.lazy(() => import("@nteract/transform-vdom"))],
|
||||
["application/json", Json],
|
||||
["application/javascript", JavaScript],
|
||||
["text/html", HTML],
|
||||
["text/markdown", React.lazy(() => import("@nteract/outputs/lib/components/media/markdown"))], // Markdown increases the bundle size so lazy load it
|
||||
["text/latex", LaTeX],
|
||||
["image/svg+xml", SVG],
|
||||
["image/gif", Image],
|
||||
["image/png", Image],
|
||||
["image/jpeg", Image],
|
||||
["text/plain", Plain],
|
||||
]);
|
||||
|
||||
interface TransformMediaProps {
|
||||
output_type: string;
|
||||
id: string;
|
||||
contentRef: ContentRef;
|
||||
output?: ImmutableDisplayData | ImmutableExecuteResult;
|
||||
onMetadataChange: (metadata: JSONObject, mediaType: string) => void;
|
||||
}
|
||||
|
||||
export const TransformMedia = (props: TransformMediaProps): JSX.Element => {
|
||||
const { Media, mediaType, data, metadata } = getMediaInfo(props);
|
||||
|
||||
// If we had no valid result, return an empty output
|
||||
if (!mediaType || !data) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Media
|
||||
onMetadataChange={props.onMetadataChange}
|
||||
data={data}
|
||||
metadata={metadata}
|
||||
contentRef={props.contentRef}
|
||||
id={props.id}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
const getMediaInfo = (props: TransformMediaProps) => {
|
||||
const { output, output_type } = props;
|
||||
// This component should only be used with display data and execute result
|
||||
if (!output || !(output_type === "display_data" || output_type === "execute_result")) {
|
||||
console.warn("connected transform media managed to get a non media bundle output");
|
||||
return {
|
||||
Media: EmptyTransform,
|
||||
};
|
||||
}
|
||||
|
||||
// Find the first mediaType in the output data that we support with a handler
|
||||
const mediaType = displayOrder.find(
|
||||
(key) =>
|
||||
Object.prototype.hasOwnProperty.call(output.data, key) &&
|
||||
(Object.prototype.hasOwnProperty.call(transformsById, key) || transformsById.get(key))
|
||||
);
|
||||
|
||||
if (mediaType) {
|
||||
const metadata = output.metadata.get(mediaType);
|
||||
const data = output.data[mediaType];
|
||||
|
||||
const Media = transformsById.get(mediaType);
|
||||
return {
|
||||
Media,
|
||||
mediaType,
|
||||
data,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
Media: EmptyTransform,
|
||||
mediaType,
|
||||
output,
|
||||
};
|
||||
};
|
||||
|
||||
export default TransformMedia;
|
||||
12
src/CellOutputViewer/cellOutputViewer.html
Normal file
12
src/CellOutputViewer/cellOutputViewer.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0" />
|
||||
<title>Cell Output Viewer</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="cellOutput" id="cellOutput"></div>
|
||||
</body>
|
||||
</html>
|
||||
1
src/CellOutputViewer/transforms/Vega2.ts
Normal file
1
src/CellOutputViewer/transforms/Vega2.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Vega2 as default } from "@nteract/transform-vega";
|
||||
1
src/CellOutputViewer/transforms/Vega3.ts
Normal file
1
src/CellOutputViewer/transforms/Vega3.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Vega3 as default } from "@nteract/transform-vega";
|
||||
1
src/CellOutputViewer/transforms/Vega4.ts
Normal file
1
src/CellOutputViewer/transforms/Vega4.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Vega4 as default } from "@nteract/transform-vega";
|
||||
1
src/CellOutputViewer/transforms/Vega5.ts
Normal file
1
src/CellOutputViewer/transforms/Vega5.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Vega5 as default } from "@nteract/transform-vega";
|
||||
1
src/CellOutputViewer/transforms/VegaLite1.ts
Normal file
1
src/CellOutputViewer/transforms/VegaLite1.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { VegaLite1 as default } from "@nteract/transform-vega";
|
||||
1
src/CellOutputViewer/transforms/VegaLite2.ts
Normal file
1
src/CellOutputViewer/transforms/VegaLite2.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { VegaLite2 as default } from "@nteract/transform-vega";
|
||||
1
src/CellOutputViewer/transforms/VegaLite3.ts
Normal file
1
src/CellOutputViewer/transforms/VegaLite3.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { VegaLite3 as default } from "@nteract/transform-vega";
|
||||
1
src/CellOutputViewer/transforms/VegaLite4.ts
Normal file
1
src/CellOutputViewer/transforms/VegaLite4.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { VegaLite4 as default } from "@nteract/transform-vega";
|
||||
1
src/CellOutputViewer/transforms/WidgetDisplay.ts
Normal file
1
src/CellOutputViewer/transforms/WidgetDisplay.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { WidgetDisplay as default } from "@nteract/jupyter-widgets";
|
||||
@@ -34,8 +34,6 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import configureStore from "./NotebookComponent/store";
|
||||
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
|
||||
import SandboxJavaScript from "./NotebookRenderer/outputs/SandboxJavaScript";
|
||||
import SanitizedHTML from "./NotebookRenderer/outputs/SanitizedHTML";
|
||||
|
||||
export type KernelSpecsDisplay = { name: string; displayName: string };
|
||||
|
||||
@@ -125,62 +123,62 @@ export class NotebookClientV2 {
|
||||
contents: makeContentsRecord({
|
||||
byRef: Immutable.Map<string, ContentRecord>(),
|
||||
}),
|
||||
transforms: 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": userContext.features.sandboxNotebookOutputs
|
||||
? SandboxJavaScript
|
||||
: Media.JavaScript,
|
||||
"text/html": userContext.features.sandboxNotebookOutputs ? SanitizedHTML : 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,
|
||||
}),
|
||||
}),
|
||||
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({
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { actions, ContentRef } from "@nteract/core";
|
||||
import { KernelOutputError, StreamText } from "@nteract/outputs";
|
||||
import { Cells, CodeCell, MarkdownCell, 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";
|
||||
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
|
||||
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
@@ -14,7 +12,7 @@ import { AzureTheme } from "./AzureTheme";
|
||||
import "./base.css";
|
||||
import "./default.css";
|
||||
import "./NotebookReadOnlyRenderer.less";
|
||||
import IFrameOutputs from "./outputs/IFrameOutputs";
|
||||
import SandboxOutputs from "./outputs/SandboxOutputs";
|
||||
|
||||
export interface NotebookRendererProps {
|
||||
contentRef: any;
|
||||
@@ -27,7 +25,9 @@ export interface NotebookRendererProps {
|
||||
*/
|
||||
class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
||||
componentDidMount() {
|
||||
loadTransform(this.props as any);
|
||||
if (!userContext.features.sandboxNotebookOutputs) {
|
||||
loadTransform(this.props as any);
|
||||
}
|
||||
}
|
||||
|
||||
private renderPrompt(id: string, contentRef: string): JSX.Element {
|
||||
@@ -63,14 +63,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
||||
{{
|
||||
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
|
||||
outputs: userContext.features.sandboxNotebookOutputs
|
||||
? (props: any) => (
|
||||
<IFrameOutputs id={id} contentRef={contentRef}>
|
||||
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
|
||||
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
|
||||
<KernelOutputError />
|
||||
<StreamText />
|
||||
</IFrameOutputs>
|
||||
)
|
||||
? () => <SandboxOutputs id={id} contentRef={contentRef} />
|
||||
: undefined,
|
||||
editor: {
|
||||
monaco: (props: PassedEditorProps) =>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { CellId } from "@nteract/commutable";
|
||||
import { CellType } from "@nteract/commutable/src";
|
||||
import { actions, ContentRef } from "@nteract/core";
|
||||
import { KernelOutputError, StreamText } from "@nteract/outputs";
|
||||
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";
|
||||
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
|
||||
import * as React from "react";
|
||||
import { DndProvider } from "react-dnd";
|
||||
import HTML5Backend from "react-dnd-html5-backend";
|
||||
@@ -23,7 +21,7 @@ import KeyboardShortcuts from "./decorators/kbd-shortcuts";
|
||||
import "./default.css";
|
||||
import MarkdownCell from "./markdown-cell";
|
||||
import "./NotebookRenderer.less";
|
||||
import IFrameOutputs from "./outputs/IFrameOutputs";
|
||||
import SandboxOutputs from "./outputs/SandboxOutputs";
|
||||
import Prompt from "./Prompt";
|
||||
import { promptContent } from "./PromptContent";
|
||||
import StatusBar from "./StatusBar";
|
||||
@@ -71,7 +69,9 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
loadTransform(this.props as any);
|
||||
if (!userContext.features.sandboxNotebookOutputs) {
|
||||
loadTransform(this.props as any);
|
||||
}
|
||||
this.props.updateNotebookParentDomElt(this.props.contentRef, this.notebookRendererRef.current);
|
||||
}
|
||||
|
||||
@@ -109,14 +109,7 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
||||
),
|
||||
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
|
||||
outputs: userContext.features.sandboxNotebookOutputs
|
||||
? (props: any) => (
|
||||
<IFrameOutputs id={id} contentRef={contentRef}>
|
||||
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
|
||||
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
|
||||
<KernelOutputError />
|
||||
<StreamText />
|
||||
</IFrameOutputs>
|
||||
)
|
||||
? () => <SandboxOutputs id={id} contentRef={contentRef} />
|
||||
: undefined,
|
||||
}}
|
||||
</CodeCell>
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { AppState, ContentRef, selectors } from "@nteract/core";
|
||||
import { Output } from "@nteract/outputs";
|
||||
import Immutable from "immutable";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { SandboxFrame } from "./SandboxFrame";
|
||||
|
||||
// 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;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
hidden: boolean;
|
||||
expanded: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
outputs: Immutable.List<any>;
|
||||
}
|
||||
|
||||
export class IFrameOutputs extends React.PureComponent<ComponentProps & StateProps> {
|
||||
render(): JSX.Element {
|
||||
const { outputs, children, hidden, expanded } = this.props;
|
||||
return (
|
||||
<SandboxFrame
|
||||
style={{ border: "none", width: "100%" }}
|
||||
sandbox="allow-downloads allow-forms allow-pointer-lock allow-same-origin allow-scripts"
|
||||
>
|
||||
<div className={`nteract-cell-outputs ${hidden ? "hidden" : ""} ${expanded ? "expanded" : ""}`}>
|
||||
{outputs.map((output, index) => (
|
||||
<Output output={output} key={index}>
|
||||
{children}
|
||||
</Output>
|
||||
))}
|
||||
</div>
|
||||
</SandboxFrame>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const makeMapStateToProps = (
|
||||
initialState: AppState,
|
||||
ownProps: ComponentProps
|
||||
): ((state: AppState) => StateProps) => {
|
||||
const mapStateToProps = (state: AppState): 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;
|
||||
}
|
||||
}
|
||||
|
||||
return { outputs, hidden, expanded };
|
||||
};
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export default connect<StateProps, void, ComponentProps, AppState>(makeMapStateToProps)(IFrameOutputs);
|
||||
@@ -1,69 +0,0 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { copyStyles } from "../../../../Utils/StyleUtils";
|
||||
|
||||
interface SandboxFrameProps {
|
||||
style: React.CSSProperties;
|
||||
sandbox: string;
|
||||
}
|
||||
|
||||
interface SandboxFrameState {
|
||||
frame: HTMLIFrameElement;
|
||||
frameBody: HTMLElement;
|
||||
frameHeight: number;
|
||||
}
|
||||
|
||||
export class SandboxFrame extends React.PureComponent<SandboxFrameProps, SandboxFrameState> {
|
||||
private resizeObserver: ResizeObserver;
|
||||
private mutationObserver: MutationObserver;
|
||||
|
||||
constructor(props: SandboxFrameProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
frame: undefined,
|
||||
frameBody: undefined,
|
||||
frameHeight: 0,
|
||||
};
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<iframe
|
||||
ref={(ele) => this.setState({ frame: ele })}
|
||||
srcDoc={`<!DOCTYPE html>`}
|
||||
onLoad={(event) => this.onFrameLoad(event)}
|
||||
style={this.props.style}
|
||||
sandbox={this.props.sandbox}
|
||||
height={this.state.frameHeight}
|
||||
>
|
||||
{this.state.frameBody && ReactDOM.createPortal(this.props.children, this.state.frameBody)}
|
||||
</iframe>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.resizeObserver?.disconnect();
|
||||
this.mutationObserver?.disconnect();
|
||||
}
|
||||
|
||||
onFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
|
||||
const doc = (event.target as HTMLIFrameElement).contentDocument;
|
||||
copyStyles(document, doc);
|
||||
|
||||
this.setState({ frameBody: doc.body });
|
||||
|
||||
this.mutationObserver = new MutationObserver(() => {
|
||||
const bodyFirstElementChild = this.state.frameBody?.firstElementChild;
|
||||
if (!this.resizeObserver && bodyFirstElementChild) {
|
||||
this.resizeObserver = new ResizeObserver(() =>
|
||||
this.setState({
|
||||
frameHeight: this.state.frameBody?.firstElementChild.scrollHeight,
|
||||
})
|
||||
);
|
||||
this.resizeObserver.observe(bodyFirstElementChild);
|
||||
}
|
||||
});
|
||||
this.mutationObserver.observe(doc.body, { childList: true });
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Media } from "@nteract/outputs";
|
||||
import React from "react";
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* The JavaScript code that we would like to execute.
|
||||
*/
|
||||
data: string;
|
||||
/**
|
||||
* The media type associated with our component.
|
||||
*/
|
||||
mediaType: "text/javascript";
|
||||
}
|
||||
|
||||
export class SandboxJavaScript extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
data: "",
|
||||
mediaType: "application/javascript",
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
return <Media.HTML data={`<script>${this.props.data}</script>`} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default SandboxJavaScript;
|
||||
@@ -0,0 +1,132 @@
|
||||
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 } from "../../../../CellOutputViewer/CellOutputViewer";
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
hidden: boolean;
|
||||
expanded: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
outputs: Immutable.List<any>;
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
onMetadataChange?: (metadata: JSONObject, mediaType: string, index?: number) => void;
|
||||
}
|
||||
|
||||
export class SandboxOutputs extends React.PureComponent<ComponentProps & StateProps & DispatchProps> {
|
||||
private childWindow: Window;
|
||||
|
||||
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 (
|
||||
<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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
hidden: this.props.hidden,
|
||||
expanded: this.props.expanded,
|
||||
outputs: this.props.outputs.toArray().map((output) => outputToJS(output)),
|
||||
onMetadataChange: this.props.onMetadataChange,
|
||||
};
|
||||
|
||||
postRobot.send(this.childWindow, "props", props);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.sendPropsToFrame();
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
this.sendPropsToFrame();
|
||||
}
|
||||
}
|
||||
|
||||
export const makeMapStateToProps = (
|
||||
initialState: AppState,
|
||||
ownProps: ComponentProps
|
||||
): ((state: AppState) => StateProps) => {
|
||||
const mapStateToProps = (state: AppState): 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;
|
||||
}
|
||||
}
|
||||
|
||||
return { outputs, hidden, expanded };
|
||||
};
|
||||
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,
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
return mapDispatchToProps;
|
||||
};
|
||||
|
||||
export default connect<StateProps, DispatchProps, ComponentProps, AppState>(
|
||||
makeMapStateToProps,
|
||||
makeMapDispatchToProps
|
||||
)(SandboxOutputs);
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Media } from "@nteract/outputs";
|
||||
import React from "react";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* The HTML string that will be rendered.
|
||||
*/
|
||||
data: string;
|
||||
/**
|
||||
* The media type associated with the HTML
|
||||
* string. This defaults to text/html.
|
||||
*/
|
||||
mediaType: "text/html";
|
||||
}
|
||||
|
||||
export class SanitizedHTML extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
data: "",
|
||||
mediaType: "text/html",
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
return <Media.HTML data={sanitize(this.props.data)} />;
|
||||
}
|
||||
}
|
||||
|
||||
function sanitize(html: string): string {
|
||||
return sanitizeHtml(html, {
|
||||
allowedTags: false, // allow all tags
|
||||
allowedAttributes: false, // allow all attrs
|
||||
transformTags: {
|
||||
iframe: "iframe-disabled", // disable iframes
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default SanitizedHTML;
|
||||
Reference in New Issue
Block a user