diff --git a/src/Explorer/Notebook/NotebookClientV2.ts b/src/Explorer/Notebook/NotebookClientV2.ts index 138384b6f..5a440685d 100644 --- a/src/Explorer/Notebook/NotebookClientV2.ts +++ b/src/Explorer/Notebook/NotebookClientV2.ts @@ -1,15 +1,16 @@ // Manages all the redux logic for the notebook nteract code // TODO: Merge with NotebookClient? -import { NotebookWorkspaceConnectionInfo } from "../../Contracts/DataModels"; -import * as Constants from "../../Common/Constants"; -import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types"; - // Vendor modules import { actions, AppState, + ContentRecord, createHostRef, createKernelspecsRef, + HostRecord, + HostRef, + IContentProvider, + KernelspecsRef, makeAppRecord, makeCommsRecord, makeContentsRecord, @@ -19,23 +20,21 @@ import { makeJupyterHostRecord, makeStateRecord, makeTransformsRecord, - ContentRecord, - HostRecord, - HostRef, - KernelspecsRef, - IContentProvider, } from "@nteract/core"; +import { configOption, createConfigCollection, defineConfigOption } from "@nteract/mythic-configuration"; import { Media } from "@nteract/outputs"; import TransformVDOM from "@nteract/transform-vdom"; 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 * 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 { 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 }; @@ -168,7 +167,7 @@ export class NotebookClientV2 { "application/vnd.vega.v5+json": NullTransform, "application/vdom.v1+json": TransformVDOM, "application/json": Media.Json, - "application/javascript": Media.JavaScript, + "application/javascript": userContext.features.sandboxNotebookOutputs ? JavaScript : Media.JavaScript, "text/html": Media.HTML, "text/markdown": Media.Markdown, "text/latex": Media.LaTeX, diff --git a/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx b/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx index ae75450be..75133d7f3 100644 --- a/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx +++ b/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx @@ -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 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 { 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 IFrameOutputs from "./outputs/IFrameOutputs"; export interface NotebookRendererProps { contentRef: any; @@ -60,6 +62,16 @@ class NotebookReadOnlyRenderer extends React.Component { {{ prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef), + outputs: userContext.features.sandboxNotebookOutputs + ? (props: any) => ( + + + + + + + ) + : undefined, editor: { monaco: (props: PassedEditorProps) => this.props.hideInputs ? <> : , diff --git a/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx b/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx index bb16b103a..eea9ac076 100644 --- a/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx +++ b/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx @@ -1,37 +1,32 @@ -import * as React from "react"; -import "./base.css"; -import "./default.css"; - -import { RawCell, Cells, CodeCell, MarkdownCell } from "@nteract/stateful-components"; +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, 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 from "./Prompt"; -import { promptContent } from "./PromptContent"; - -import { AzureTheme } from "./AzureTheme"; +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"; - import { connect } from "react-redux"; import { Dispatch } from "redux"; -import { actions, ContentRef } from "@nteract/core"; -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 { userContext } from "../../../UserContext"; 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 { contentRef: any; @@ -112,6 +107,16 @@ class BaseNotebookRenderer extends React.Component { ), toolbar: () => , + outputs: userContext.features.sandboxNotebookOutputs + ? (props: any) => ( + + + + + + + ) + : undefined, }} ), diff --git a/src/Explorer/Notebook/NotebookRenderer/outputs/IFrameOutputs.tsx b/src/Explorer/Notebook/NotebookRenderer/outputs/IFrameOutputs.tsx new file mode 100644 index 000000000..dc7ac5ba2 --- /dev/null +++ b/src/Explorer/Notebook/NotebookRenderer/outputs/IFrameOutputs.tsx @@ -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 + ); + } + + componentWillUnmount() { + this.resizeObserver?.disconnect(); + } + + onFrameLoad(event: React.SyntheticEvent): 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); + } +} diff --git a/src/Explorer/Notebook/NotebookRenderer/outputs/javascript.tsx b/src/Explorer/Notebook/NotebookRenderer/outputs/javascript.tsx new file mode 100644 index 000000000..88793770b --- /dev/null +++ b/src/Explorer/Notebook/NotebookRenderer/outputs/javascript.tsx @@ -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 { + static defaultProps = { + data: "", + mediaType: "application/javascript", + }; + + render(): JSX.Element { + return ${this.props.data}`} />; + } +} + +export default JavaScript; diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 5a81ec699..c0d40df1e 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -17,6 +17,7 @@ export type Features = { readonly notebookBasePath?: string; readonly notebookServerToken?: string; readonly notebookServerUrl?: string; + readonly sandboxNotebookOutputs: boolean; readonly selfServeType?: string; readonly showMinRUSurvey: boolean; readonly ttl90Days: boolean; @@ -54,6 +55,7 @@ export function extractFeatures(given = new URLSearchParams()): Features { notebookBasePath: get("notebookbasepath"), notebookServerToken: get("notebookservertoken"), notebookServerUrl: get("notebookserverurl"), + sandboxNotebookOutputs: "true" === get("sandboxnotebookoutputs"), selfServeType: get("selfservetype"), showMinRUSurvey: "true" === get("showminrusurvey"), ttl90Days: "true" === get("ttl90days"), diff --git a/src/Utils/StyleUtils.ts b/src/Utils/StyleUtils.ts new file mode 100644 index 000000000..fa76c6910 --- /dev/null +++ b/src/Utils/StyleUtils.ts @@ -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 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