mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-25 15:06:55 +00:00
Sandbox all outputs in iFrame (#624)
This commit is contained in:
parent
37e0f50ef2
commit
14fd9054dd
@ -1,15 +1,16 @@
|
|||||||
// Manages all the redux logic for the notebook nteract code
|
// Manages all the redux logic for the notebook nteract code
|
||||||
// TODO: Merge with NotebookClient?
|
// TODO: Merge with NotebookClient?
|
||||||
import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels";
|
|
||||||
import * as Constants from "../../Common/Constants";
|
|
||||||
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
|
|
||||||
|
|
||||||
// Vendor modules
|
// Vendor modules
|
||||||
import {
|
import {
|
||||||
actions,
|
actions,
|
||||||
AppState,
|
AppState,
|
||||||
|
ContentRecord,
|
||||||
createHostRef,
|
createHostRef,
|
||||||
createKernelspecsRef,
|
createKernelspecsRef,
|
||||||
|
HostRecord,
|
||||||
|
HostRef,
|
||||||
|
IContentProvider,
|
||||||
|
KernelspecsRef,
|
||||||
makeAppRecord,
|
makeAppRecord,
|
||||||
makeCommsRecord,
|
makeCommsRecord,
|
||||||
makeContentsRecord,
|
makeContentsRecord,
|
||||||
@ -19,23 +20,21 @@ import {
|
|||||||
makeJupyterHostRecord,
|
makeJupyterHostRecord,
|
||||||
makeStateRecord,
|
makeStateRecord,
|
||||||
makeTransformsRecord,
|
makeTransformsRecord,
|
||||||
ContentRecord,
|
|
||||||
HostRecord,
|
|
||||||
HostRef,
|
|
||||||
KernelspecsRef,
|
|
||||||
IContentProvider,
|
|
||||||
} from "@nteract/core";
|
} from "@nteract/core";
|
||||||
|
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
|
||||||
import { Media } from "@nteract/outputs";
|
import { Media } from "@nteract/outputs";
|
||||||
import TransformVDOM from "@nteract/transform-vdom";
|
import TransformVDOM from "@nteract/transform-vdom";
|
||||||
import * as Immutable from "immutable";
|
import * as Immutable from "immutable";
|
||||||
import { Store, AnyAction, MiddlewareAPI, Middleware, Dispatch } from "redux";
|
|
||||||
|
|
||||||
import configureStore from "./NotebookComponent/store";
|
|
||||||
|
|
||||||
import { Notification } from "react-notification-system";
|
import { Notification } from "react-notification-system";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
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 { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
import configureStore from "./NotebookComponent/store";
|
||||||
|
import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types";
|
||||||
|
import JavaScript from "./NotebookRenderer/outputs/javascript";
|
||||||
|
|
||||||
export type KernelSpecsDisplay = { name: string; displayName: string };
|
export type KernelSpecsDisplay = { name: string; displayName: string };
|
||||||
|
|
||||||
@ -168,7 +167,7 @@ export class NotebookClientV2 {
|
|||||||
"application/vnd.vega.v5+json": NullTransform,
|
"application/vnd.vega.v5+json": NullTransform,
|
||||||
"application/vdom.v1+json": TransformVDOM,
|
"application/vdom.v1+json": TransformVDOM,
|
||||||
"application/json": Media.Json,
|
"application/json": Media.Json,
|
||||||
"application/javascript": Media.JavaScript,
|
"application/javascript": userContext.features.sandboxNotebookOutputs ? JavaScript : Media.JavaScript,
|
||||||
"text/html": Media.HTML,
|
"text/html": Media.HTML,
|
||||||
"text/markdown": Media.Markdown,
|
"text/markdown": Media.Markdown,
|
||||||
"text/latex": Media.LaTeX,
|
"text/latex": Media.LaTeX,
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
import * as React from "react";
|
|
||||||
import "./base.css";
|
|
||||||
import "./default.css";
|
|
||||||
|
|
||||||
import { CodeCell, RawCell, Cells, MarkdownCell } from "@nteract/stateful-components";
|
|
||||||
import Prompt, { PassedPromptProps } from "@nteract/stateful-components/lib/inputs/prompt";
|
|
||||||
import { AzureTheme } from "./AzureTheme";
|
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Dispatch } from "redux";
|
|
||||||
import { actions, ContentRef } from "@nteract/core";
|
import { actions, ContentRef } from "@nteract/core";
|
||||||
import loadTransform from "../NotebookComponent/loadTransform";
|
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 MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
|
||||||
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
|
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";
|
||||||
|
import { userContext } from "../../../UserContext";
|
||||||
|
import loadTransform from "../NotebookComponent/loadTransform";
|
||||||
|
import { AzureTheme } from "./AzureTheme";
|
||||||
|
import "./base.css";
|
||||||
|
import "./default.css";
|
||||||
import "./NotebookReadOnlyRenderer.less";
|
import "./NotebookReadOnlyRenderer.less";
|
||||||
|
import IFrameOutputs from "./outputs/IFrameOutputs";
|
||||||
|
|
||||||
export interface NotebookRendererProps {
|
export interface NotebookRendererProps {
|
||||||
contentRef: any;
|
contentRef: any;
|
||||||
@ -60,6 +62,16 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
|||||||
<CodeCell id={id} contentRef={contentRef}>
|
<CodeCell id={id} contentRef={contentRef}>
|
||||||
{{
|
{{
|
||||||
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
|
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>
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
editor: {
|
editor: {
|
||||||
monaco: (props: PassedEditorProps) =>
|
monaco: (props: PassedEditorProps) =>
|
||||||
this.props.hideInputs ? <></> : <MonacoEditor readOnly={true} {...props} editorType={"monaco"} />,
|
this.props.hideInputs ? <></> : <MonacoEditor readOnly={true} {...props} editorType={"monaco"} />,
|
||||||
|
@ -1,37 +1,32 @@
|
|||||||
import * as React from "react";
|
import { CellId } from "@nteract/commutable";
|
||||||
import "./base.css";
|
import { CellType } from "@nteract/commutable/src";
|
||||||
import "./default.css";
|
import { actions, ContentRef } from "@nteract/core";
|
||||||
|
import { KernelOutputError, StreamText } from "@nteract/outputs";
|
||||||
import { RawCell, Cells, CodeCell, MarkdownCell } from "@nteract/stateful-components";
|
import { Cells, CodeCell, MarkdownCell, RawCell } from "@nteract/stateful-components";
|
||||||
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
|
import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor";
|
||||||
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
|
import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor";
|
||||||
|
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
|
||||||
import Prompt from "./Prompt";
|
import * as React from "react";
|
||||||
import { promptContent } from "./PromptContent";
|
|
||||||
|
|
||||||
import { AzureTheme } from "./AzureTheme";
|
|
||||||
import { DndProvider } from "react-dnd";
|
import { DndProvider } from "react-dnd";
|
||||||
import HTML5Backend from "react-dnd-html5-backend";
|
import HTML5Backend from "react-dnd-html5-backend";
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Dispatch } from "redux";
|
import { Dispatch } from "redux";
|
||||||
import { actions, ContentRef } from "@nteract/core";
|
import { userContext } from "../../../UserContext";
|
||||||
import { CellId } from "@nteract/commutable";
|
|
||||||
import loadTransform from "../NotebookComponent/loadTransform";
|
|
||||||
import DraggableCell from "./decorators/draggable";
|
|
||||||
import CellCreator from "./decorators/CellCreator";
|
|
||||||
import KeyboardShortcuts from "./decorators/kbd-shortcuts";
|
|
||||||
|
|
||||||
import CellToolbar from "./Toolbar";
|
|
||||||
import StatusBar from "./StatusBar";
|
|
||||||
|
|
||||||
import HijackScroll from "./decorators/hijack-scroll";
|
|
||||||
import { CellType } from "@nteract/commutable/src";
|
|
||||||
|
|
||||||
import "./NotebookRenderer.less";
|
|
||||||
import HoverableCell from "./decorators/HoverableCell";
|
|
||||||
import CellLabeler from "./decorators/CellLabeler";
|
|
||||||
import * as cdbActions from "../NotebookComponent/actions";
|
import * as cdbActions from "../NotebookComponent/actions";
|
||||||
|
import loadTransform from "../NotebookComponent/loadTransform";
|
||||||
|
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 "./NotebookRenderer.less";
|
||||||
|
import IFrameOutputs from "./outputs/IFrameOutputs";
|
||||||
|
import Prompt from "./Prompt";
|
||||||
|
import { promptContent } from "./PromptContent";
|
||||||
|
import StatusBar from "./StatusBar";
|
||||||
|
import CellToolbar from "./Toolbar";
|
||||||
|
|
||||||
export interface NotebookRendererBaseProps {
|
export interface NotebookRendererBaseProps {
|
||||||
contentRef: any;
|
contentRef: any;
|
||||||
@ -112,6 +107,16 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
|||||||
</Prompt>
|
</Prompt>
|
||||||
),
|
),
|
||||||
toolbar: () => <CellToolbar id={id} contentRef={contentRef} />,
|
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>
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
}}
|
}}
|
||||||
</CodeCell>
|
</CodeCell>
|
||||||
),
|
),
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
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-popups allow-same-origin allow-scripts allow-popups-to-escape-sandbox"
|
||||||
|
>
|
||||||
|
<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);
|
@ -0,0 +1,64 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
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() {
|
||||||
|
this.resizeObserver?.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
onFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
|
||||||
|
const doc = (event.target as HTMLIFrameElement).contentDocument;
|
||||||
|
copyStyles(document, doc);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
frameBody: doc.body,
|
||||||
|
frameHeight: doc.body.scrollHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.resizeObserver = new ResizeObserver(() =>
|
||||||
|
this.setState({
|
||||||
|
frameHeight: this.state.frameBody.scrollHeight,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.resizeObserver.observe(doc.body);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
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 JavaScript extends React.PureComponent<Props> {
|
||||||
|
static defaultProps = {
|
||||||
|
data: "",
|
||||||
|
mediaType: "application/javascript",
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
return <Media.HTML data={`<script>${this.props.data}</script>`} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JavaScript;
|
@ -17,6 +17,7 @@ export type Features = {
|
|||||||
readonly notebookBasePath?: string;
|
readonly notebookBasePath?: string;
|
||||||
readonly notebookServerToken?: string;
|
readonly notebookServerToken?: string;
|
||||||
readonly notebookServerUrl?: string;
|
readonly notebookServerUrl?: string;
|
||||||
|
readonly sandboxNotebookOutputs: boolean;
|
||||||
readonly selfServeType?: string;
|
readonly selfServeType?: string;
|
||||||
readonly showMinRUSurvey: boolean;
|
readonly showMinRUSurvey: boolean;
|
||||||
readonly ttl90Days: boolean;
|
readonly ttl90Days: boolean;
|
||||||
@ -54,6 +55,7 @@ export function extractFeatures(given = new URLSearchParams()): Features {
|
|||||||
notebookBasePath: get("notebookbasepath"),
|
notebookBasePath: get("notebookbasepath"),
|
||||||
notebookServerToken: get("notebookservertoken"),
|
notebookServerToken: get("notebookservertoken"),
|
||||||
notebookServerUrl: get("notebookserverurl"),
|
notebookServerUrl: get("notebookserverurl"),
|
||||||
|
sandboxNotebookOutputs: "true" === get("sandboxnotebookoutputs"),
|
||||||
selfServeType: get("selfservetype"),
|
selfServeType: get("selfservetype"),
|
||||||
showMinRUSurvey: "true" === get("showminrusurvey"),
|
showMinRUSurvey: "true" === get("showminrusurvey"),
|
||||||
ttl90Days: "true" === get("ttl90days"),
|
ttl90Days: "true" === get("ttl90days"),
|
||||||
|
23
src/Utils/StyleUtils.ts
Normal file
23
src/Utils/StyleUtils.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// Adapted from https://gist.github.com/davidgilbertson/ed3c8bb8569bc64b094b87aa88bed5fa
|
||||||
|
export function copyStyles(sourceDoc: Document, targetDoc: Document): void {
|
||||||
|
Array.from(sourceDoc.styleSheets).forEach((styleSheet) => {
|
||||||
|
if (styleSheet.href) {
|
||||||
|
// for <link> elements loading CSS from a URL
|
||||||
|
const newLinkEl = sourceDoc.createElement("link");
|
||||||
|
|
||||||
|
newLinkEl.rel = "stylesheet";
|
||||||
|
newLinkEl.href = styleSheet.href;
|
||||||
|
targetDoc.head.appendChild(newLinkEl);
|
||||||
|
} else if (styleSheet.cssRules && styleSheet.cssRules.length > 0) {
|
||||||
|
// for <style> elements
|
||||||
|
const newStyleEl = sourceDoc.createElement("style");
|
||||||
|
|
||||||
|
Array.from(styleSheet.cssRules).forEach((cssRule) => {
|
||||||
|
// write the text of each rule into the body of the style element
|
||||||
|
newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText));
|
||||||
|
});
|
||||||
|
|
||||||
|
targetDoc.head.appendChild(newStyleEl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user