From 4f22d308b3514ad034af7fac3d4cd91608af056f Mon Sep 17 00:00:00 2001 From: Jordi Bunster Date: Wed, 7 Apr 2021 09:15:00 -0700 Subject: [PATCH 01/38] Move tabs state out into React (#621) This PR is just about moving the tabs array. I'm hoping to let it bake for a bit before merging the rest of the tabs in react work. Preview here: https://ms.portal.azure.com/?dataExplorerSource=https%3A%2F%2Fcosmos-explorer-preview.azurewebsites.net%2Fcommit%2Fda809beb82bb54dc82da18eda41caaf7b9b6597f%2Fexplorer.html#@microsoft.onmicrosoft.com/resource/subscriptions/b9c77f10-b438-4c32-9819-eef8a654e478/resourceGroups/stfaul/providers/Microsoft.DocumentDb/databaseAccounts/stfaul-sql/dataExplorer --- src/Explorer/Explorer.tsx | 3 ++- src/Explorer/SplashScreen/SplashScreen.tsx | 13 +++++++------ src/Main.tsx | 10 +++++----- src/hooks/useObservableState.ts | 16 ++++++++++++++++ src/hooks/useTabs.ts | 16 ++++++++++++++++ 5 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 src/hooks/useObservableState.ts create mode 100644 src/hooks/useTabs.ts diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 367693c99..971ac43a5 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -93,6 +93,7 @@ export interface ExplorerParams { closeSidePanel: () => void; closeDialog: () => void; openDialog: (props: DialogProps) => void; + tabsManager: TabsManager; } export default class Explorer { @@ -600,7 +601,7 @@ export default class Explorer { container: this, }); - this.tabsManager = new TabsManager(); + this.tabsManager = params?.tabsManager ?? new TabsManager(); this._panes = [ this.addDatabasePane, diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index 634ba3e44..e5debb883 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -50,10 +50,6 @@ export class SplashScreen extends React.Component { this.subscriptions = []; } - public shouldComponentUpdate() { - return this.container.tabsManager.openedTabs.length === 0; - } - public componentWillUnmount() { while (this.subscriptions.length) { this.subscriptions.pop().dispose(); @@ -62,7 +58,6 @@ export class SplashScreen extends React.Component { public componentDidMount() { this.subscriptions.push( - this.container.tabsManager.openedTabs.subscribe(() => this.setState({})), this.container.selectedNode.subscribe(() => this.setState({})), this.container.isNotebookEnabled.subscribe(() => this.setState({})) ); @@ -80,7 +75,13 @@ export class SplashScreen extends React.Component { const tipsItems = this.createTipsItems(); const onClearRecent = this.clearMostRecent; - return ( + const formContainer = (jsx: JSX.Element) => ( +
+
{jsx}
+
+ ); + + return formContainer(
diff --git a/src/Main.tsx b/src/Main.tsx index d829e897b..4137aa59f 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -53,6 +53,7 @@ import "./Explorer/Tabs/QueryTab.less"; import { useConfig } from "./hooks/useConfig"; import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer"; import { useSidePanel } from "./hooks/useSidePanel"; +import { useTabs } from "./hooks/useTabs"; import { KOCommentEnd, KOCommentIfStart } from "./koComment"; import "./Libs/jquery"; import "./Shared/appInsights"; @@ -78,6 +79,7 @@ const App: React.FunctionComponent = () => { }; const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel(); + const { tabs, tabsManager } = useTabs(); const explorerParams: ExplorerParams = { setIsNotificationConsoleExpanded, @@ -87,7 +89,9 @@ const App: React.FunctionComponent = () => { closeSidePanel, openDialog, closeDialog, + tabsManager, }; + const config = useConfig(); const explorer = useKnockoutExplorer(config?.platform, explorerParams); @@ -200,11 +204,7 @@ const App: React.FunctionComponent = () => { {/* Splitter - End */}
{/* Collections Tree - End */} -
-
- - -
+ {tabs.length === 0 && }
{/* Collections Tree and Tabs - End */} diff --git a/src/hooks/useObservableState.ts b/src/hooks/useObservableState.ts new file mode 100644 index 000000000..8894499b5 --- /dev/null +++ b/src/hooks/useObservableState.ts @@ -0,0 +1,16 @@ +import { isObservableArray, Observable, ObservableArray } from "knockout"; +import { useEffect, useState } from "react"; + +export function useObservableState(observable: Observable): [T, (s: T) => void]; +export function useObservableState(observable: ObservableArray): [T[], (s: T[]) => void]; +export function useObservableState(observable: ObservableArray | Observable): [T | T[], (s: T | T[]) => void] { + const [value, setValue] = useState(observable()); + + useEffect(() => { + isObservableArray(observable) + ? observable.subscribe((values) => setValue([...values])) + : observable.subscribe(setValue); + }, [observable]); + + return [value, observable]; +} diff --git a/src/hooks/useTabs.ts b/src/hooks/useTabs.ts new file mode 100644 index 000000000..b487accbd --- /dev/null +++ b/src/hooks/useTabs.ts @@ -0,0 +1,16 @@ +import { useState } from "react"; +import TabsBase from "../Explorer/Tabs/TabsBase"; +import { TabsManager } from "../Explorer/Tabs/TabsManager"; +import { useObservableState } from "./useObservableState"; + +export type UseTabs = { + tabs: readonly TabsBase[]; + tabsManager: TabsManager; +}; + +export function useTabs(): UseTabs { + const [tabsManager] = useState(() => new TabsManager()); + const [tabs] = useObservableState(tabsManager.openedTabs); + + return { tabs, tabsManager }; +} From d2423f28dcb59a88424392327f68c23555629ba9 Mon Sep 17 00:00:00 2001 From: Srinath Narayanan Date: Wed, 7 Apr 2021 11:17:15 -0700 Subject: [PATCH 02/38] Added className to SelfServeBaseClass (#627) * Added className to SelfServeBaseClass * addressed PR comments * addressed PR comments * fixed lint errors --- src/SelfServe/SelfServe.tsx | 10 ++++++---- src/SelfServe/SelfServeTypes.ts | 2 ++ src/SelfServe/SelfServeUtils.test.tsx | 3 +-- src/SelfServe/SelfServeUtils.tsx | 9 +++------ src/SelfServe/SqlX/SqlX.tsx | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/SelfServe/SelfServe.tsx b/src/SelfServe/SelfServe.tsx index 2550f465c..946305540 100644 --- a/src/SelfServe/SelfServe.tsx +++ b/src/SelfServe/SelfServe.tsx @@ -41,13 +41,15 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise { ]); const expectedDescriptor = { root: { - id: "TestClass", children: [ { id: "dbThroughput", @@ -270,7 +269,7 @@ describe("SelfServeUtils", () => { "invalidRegions", ], }; - const descriptor = mapToSmartUiDescriptor("TestClass", context); + const descriptor = mapToSmartUiDescriptor(context); expect(descriptor).toEqual(expectedDescriptor); }); }); diff --git a/src/SelfServe/SelfServeUtils.tsx b/src/SelfServe/SelfServeUtils.tsx index 8106153a2..bec436597 100644 --- a/src/SelfServe/SelfServeUtils.tsx +++ b/src/SelfServe/SelfServeUtils.tsx @@ -112,21 +112,18 @@ export const updateContextWithDecorator = { const context = Reflect.getMetadata(className, target) as Map; - const smartUiDescriptor = mapToSmartUiDescriptor(className, context); + const smartUiDescriptor = mapToSmartUiDescriptor(context); Reflect.defineMetadata(className, smartUiDescriptor, target); }; -export const mapToSmartUiDescriptor = ( - className: string, - context: Map -): SelfServeDescriptor => { +export const mapToSmartUiDescriptor = (context: Map): SelfServeDescriptor => { const inputNames: string[] = []; const root = context.get("root"); context.delete("root"); const smartUiDescriptor: SelfServeDescriptor = { root: { - id: className, + id: undefined, info: undefined, children: [], }, diff --git a/src/SelfServe/SqlX/SqlX.tsx b/src/SelfServe/SqlX/SqlX.tsx index b27f69e69..fe08a5c62 100644 --- a/src/SelfServe/SqlX/SqlX.tsx +++ b/src/SelfServe/SqlX/SqlX.tsx @@ -177,7 +177,7 @@ export default class SqlX extends SelfServeBaseClass { currentValues: Map, baselineValues: Map ): Promise => { - selfServeTrace({ selfServeClassName: "SqlX" }); + selfServeTrace({ selfServeClassName: this.constructor.name }); const dedicatedGatewayCurrentlyEnabled = currentValues.get("enableDedicatedGateway")?.value as boolean; const dedicatedGatewayOriginallyEnabled = baselineValues.get("enableDedicatedGateway")?.value as boolean; From e20c9569e82b64b6db34f4580b92238ea160f5ae Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Wed, 7 Apr 2021 13:31:50 -0500 Subject: [PATCH 03/38] Remove dynamic loading status (#616) --- src/Explorer/Explorer.tsx | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 971ac43a5..de18e3617 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -925,10 +925,8 @@ export default class Explorer { // TODO: Refactor const deferred: Q.Deferred = Q.defer(); - this._setLoadingStatusText("Fetching databases..."); readDatabases().then( (databases: DataModels.Database[]) => { - this._setLoadingStatusText("Successfully fetched databases."); TelemetryProcessor.traceSuccess( Action.LoadDatabases, { @@ -941,20 +939,16 @@ export default class Explorer { this.addDatabasesToList(deltaDatabases.toAdd); this.deleteDatabasesFromList(deltaDatabases.toDelete); this.selectedNode(currentlySelectedNode); - this._setLoadingStatusText("Fetching containers..."); this.refreshAndExpandNewDatabases(deltaDatabases.toAdd).then( () => { - this._setLoadingStatusText("Successfully fetched containers."); deferred.resolve(); }, (reason) => { - this._setLoadingStatusText("Failed to fetch containers."); deferred.reject(reason); } ); }, (error) => { - this._setLoadingStatusText("Failed to fetch databases."); deferred.reject(error); const errorMessage = getErrorMessage(error); TelemetryProcessor.traceFailure( @@ -2226,32 +2220,6 @@ export default class Explorer { } } - private _setLoadingStatusText(text: string, title: string = "Welcome to Azure Cosmos DB") { - if (!text) { - return; - } - - const loadingText = document.getElementById("explorerLoadingStatusText"); - if (!loadingText) { - Logger.logError( - "getElementById('explorerLoadingStatusText') failed to find element", - "Explorer/_setLoadingStatusText" - ); - return; - } - loadingText.innerHTML = text; - - const loadingTitle = document.getElementById("explorerLoadingStatusTitle"); - if (!loadingTitle) { - Logger.logError( - "getElementById('explorerLoadingStatusTitle') failed to find element", - "Explorer/_setLoadingStatusText" - ); - } else { - loadingTitle.innerHTML = title; - } - } - private _openSetupNotebooksPaneForQuickstart(): void { const title = "Enable Notebooks (Preview)"; const description = From f060d4b1b80cf986a6915c2f6a9a889081e644a4 Mon Sep 17 00:00:00 2001 From: Srinath Narayanan Date: Wed, 7 Apr 2021 16:10:26 -0700 Subject: [PATCH 04/38] Made webpack changes (#629) --- src/SelfServe/SqlX/SqlX.tsx | 2 +- webpack.config.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/SelfServe/SqlX/SqlX.tsx b/src/SelfServe/SqlX/SqlX.tsx index fe08a5c62..a532c5244 100644 --- a/src/SelfServe/SqlX/SqlX.tsx +++ b/src/SelfServe/SqlX/SqlX.tsx @@ -177,7 +177,7 @@ export default class SqlX extends SelfServeBaseClass { currentValues: Map, baselineValues: Map ): Promise => { - selfServeTrace({ selfServeClassName: this.constructor.name }); + selfServeTrace({ selfServeClassName: SqlX.name }); const dedicatedGatewayCurrentlyEnabled = currentValues.get("enableDedicatedGateway")?.value as boolean; const dedicatedGatewayOriginallyEnabled = baselineValues.get("enableDedicatedGateway")?.value as boolean; diff --git a/webpack.config.js b/webpack.config.js index 1c5f8193c..f21f310c8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -220,7 +220,10 @@ module.exports = function (env = {}, argv = {}) { terserOptions: { // These options increase our initial bundle size by ~5% but the builds are significantly faster and won't run out of memory compress: false, - mangle: true, + mangle: { + keep_fnames: true, + keep_classnames: true, + }, }, }), ], From 3ab6b2a05d19e1c3d197183d5adcc8a90673da2c Mon Sep 17 00:00:00 2001 From: Jordi Bunster Date: Thu, 8 Apr 2021 12:31:36 -0700 Subject: [PATCH 05/38] Apease eslint (#631) --- src/Explorer/Graph/GraphExplorerComponent/GraphUtil.ts | 4 ++-- src/Utils/AuthorizationUtils.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Explorer/Graph/GraphExplorerComponent/GraphUtil.ts b/src/Explorer/Graph/GraphExplorerComponent/GraphUtil.ts index 2faf25c67..712ef6845 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GraphUtil.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GraphUtil.ts @@ -136,7 +136,7 @@ export function createFetchEdgePairQuery( export function trimGraph( currentRoot: GraphData.GremlinVertex, graphData: GraphData.GraphData -) { +): void { const importantNodes = [currentRoot.id].concat(currentRoot._ancestorsId); graphData.unloadAllVertices(importantNodes); @@ -150,7 +150,7 @@ export function addRootChildToGraph( root: GraphData.GremlinVertex, child: GraphData.GremlinVertex, graphData: GraphData.GraphData -) { +): void { child._ancestorsId = (root._ancestorsId || []).concat([root.id]); graphData.addVertex(child); createEdgesfromNode(child, graphData); diff --git a/src/Utils/AuthorizationUtils.ts b/src/Utils/AuthorizationUtils.ts index 308e7f0ad..16d2082de 100644 --- a/src/Utils/AuthorizationUtils.ts +++ b/src/Utils/AuthorizationUtils.ts @@ -18,6 +18,7 @@ export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMet } } +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function decryptJWTToken(token: string) { if (!token) { Logger.logError("Cannot decrypt token: No JWT token found", "AuthorizationUtils/decryptJWTToken"); From 37e0f50ef225bfddc3bd3e3de28fbc568385018f Mon Sep 17 00:00:00 2001 From: Tanuj Mittal Date: Fri, 9 Apr 2021 00:22:41 -0700 Subject: [PATCH 06/38] Fix telemetry from child windows of Data Explorer (#633) * Fix telemetry from child windows of Data Explorer * Address feedback --- src/Common/MessageHandler.ts | 46 ++++++++++++++++---------------- src/hooks/useKnockoutExplorer.ts | 9 ++++++- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/Common/MessageHandler.ts b/src/Common/MessageHandler.ts index 73636a5fd..97201b8b2 100644 --- a/src/Common/MessageHandler.ts +++ b/src/Common/MessageHandler.ts @@ -48,32 +48,18 @@ export function sendCachedDataMessage( } export function sendMessage(data: any): void { - if (canSendMessage()) { - // We try to find data explorer window first, then fallback to current window - const portalChildWindow = getDataExplorerWindow(window) || window; - portalChildWindow.parent.postMessage( - { - signature: "pcIframe", - data: data, - }, - portalChildWindow.document.referrer || "*" - ); - } + _sendMessage({ + signature: "pcIframe", + data: data, + }); } export function sendReadyMessage(): void { - if (canSendMessage()) { - // We try to find data explorer window first, then fallback to current window - const portalChildWindow = getDataExplorerWindow(window) || window; - portalChildWindow.parent.postMessage( - { - signature: "pcIframe", - kind: "ready", - data: "ready", - }, - portalChildWindow.document.referrer || "*" - ); - } + _sendMessage({ + signature: "pcIframe", + kind: "ready", + data: "ready", + }); } export function canSendMessage(): boolean { @@ -89,3 +75,17 @@ export function runGarbageCollector() { } }); } + +const _sendMessage = (message: any): void => { + if (canSendMessage()) { + // Portal window can receive messages from only child windows + const portalChildWindow = getDataExplorerWindow(window) || window; + if (portalChildWindow === window) { + // Current window is a child of portal, send message to portal window + portalChildWindow.parent.postMessage(message, portalChildWindow.document.referrer || "*"); + } else { + // Current window is not a child of portal, send message to the child window instead (which is data explorer) + portalChildWindow.postMessage(message, portalChildWindow.location.origin || "*"); + } + } +}; diff --git a/src/hooks/useKnockoutExplorer.ts b/src/hooks/useKnockoutExplorer.ts index f1fce5827..e7d5a6853 100644 --- a/src/hooks/useKnockoutExplorer.ts +++ b/src/hooks/useKnockoutExplorer.ts @@ -3,7 +3,7 @@ import { applyExplorerBindings } from "../applyExplorerBindings"; import { AuthType } from "../AuthType"; import { AccountKind, DefaultAccountExperience } from "../Common/Constants"; import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; -import { sendReadyMessage } from "../Common/MessageHandler"; +import { sendMessage, sendReadyMessage } from "../Common/MessageHandler"; import { configContext, Platform, updateConfigContext } from "../ConfigContext"; import { ActionType, DataExplorerAction } from "../Contracts/ActionContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts"; @@ -266,6 +266,8 @@ async function configurePortal(explorerParams: ExplorerParams): Promise Date: Mon, 12 Apr 2021 08:59:18 -0700 Subject: [PATCH 07/38] Sandbox all outputs in iFrame (#624) --- src/Explorer/Notebook/NotebookClientV2.ts | 31 ++++---- .../NotebookReadOnlyRenderer.tsx | 34 ++++++--- .../NotebookRenderer/NotebookRenderer.tsx | 59 +++++++++------- .../outputs/IFrameOutputs.tsx | 70 +++++++++++++++++++ .../NotebookRenderer/outputs/SandboxFrame.tsx | 64 +++++++++++++++++ .../NotebookRenderer/outputs/javascript.tsx | 26 +++++++ src/Platform/Hosted/extractFeatures.ts | 2 + src/Utils/StyleUtils.ts | 23 ++++++ 8 files changed, 255 insertions(+), 54 deletions(-) create mode 100644 src/Explorer/Notebook/NotebookRenderer/outputs/IFrameOutputs.tsx create mode 100644 src/Explorer/Notebook/NotebookRenderer/outputs/SandboxFrame.tsx create mode 100644 src/Explorer/Notebook/NotebookRenderer/outputs/javascript.tsx create mode 100644 src/Utils/StyleUtils.ts 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