// Manages all the redux logic for the notebook nteract code // TODO: Merge with NotebookClient? // Vendor modules import { actions, AppState, ContentRecord, createHostRef, createKernelspecsRef, HostRecord, HostRef, IContentProvider, KernelspecsRef, makeAppRecord, makeCommsRecord, makeContentsRecord, makeEditorsRecord, makeEntitiesRecord, makeHostsRecord, makeJupyterHostRecord, makeStateRecord, makeTransformsRecord, } from "@nteract/core"; import { configOption, defineConfigOption } from "@nteract/mythic-configuration"; import { Media } from "@nteract/outputs"; import TransformVDOM from "@nteract/transform-vdom"; import * as Immutable from "immutable"; import { Notification } from "react-notification-system"; 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 * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import configureStore from "./NotebookComponent/store"; import { CdbAppState, makeCdbRecord } from "./NotebookComponent/types"; export type KernelSpecsDisplay = { name: string; displayName: string }; export interface NotebookClientV2Parameters { connectionInfo: NotebookWorkspaceConnectionInfo; databaseAccountName: string; defaultExperience: string; isReadOnly?: boolean; // if true: do not fetch kernelspecs automatically (this is for notebook viewer) cellEditorType?: string; // override "codemirror" default, autoSaveInterval?: number; // in ms contentProvider: IContentProvider; } export type ActionListener = (newValue: any) => void; export class NotebookClientV2 { private store: Store; private contentHostRef: HostRef; private kernelSpecsForDisplay: KernelSpecsDisplay[] = []; private kernelSpecsRef: KernelspecsRef; private databaseAccountName: string; private defaultExperience: string; constructor(params: NotebookClientV2Parameters) { this.databaseAccountName = params.databaseAccountName; this.defaultExperience = params.defaultExperience; this.configureStore(params); this.kernelSpecsRef = createKernelspecsRef(); // Fetch kernel specs when opening new tab if (!params.isReadOnly) { this.getStore().dispatch( actions.fetchKernelspecs({ hostRef: this.contentHostRef, kernelspecsRef: this.kernelSpecsRef, }) ); } } public getAvailableKernelSpecs(): KernelSpecsDisplay[] { return this.kernelSpecsForDisplay; } public getStore(): Store { return this.store; } /** * Lazy init redux store as singleton. * Don't move store in Explorer yet as it is typed to AppState which is nteract-specific */ private configureStore(params: NotebookClientV2Parameters): void { const jupyterHostRecord = makeJupyterHostRecord({ id: null, type: "jupyter", defaultKernelName: "python", token: params.connectionInfo.authToken, origin: params.connectionInfo.notebookServerEndpoint, basePath: "/", // Jupyter server base URL bookstoreEnabled: false, //!!config.bookstore.version, showHeaderEditor: true, crossDomain: true, }); this.contentHostRef = createHostRef(); const NullTransform = (): any => null; const kernelspecsRef = createKernelspecsRef(); const initialState: CdbAppState = { app: makeAppRecord({ version: "dataExplorer 1.0", host: jupyterHostRecord, // TODO: tamitta: notificationSystem.addNotification was removed, do we need a substitute? }), core: makeStateRecord({ currentKernelspecsRef: kernelspecsRef, entities: makeEntitiesRecord({ editors: makeEditorsRecord({}), hosts: makeHostsRecord({ byRef: Immutable.Map().set(this.contentHostRef, jupyterHostRecord), }), comms: makeCommsRecord(), contents: makeContentsRecord({ byRef: Immutable.Map(), }), 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({ databaseAccountName: params.databaseAccountName, defaultExperience: params.defaultExperience, }), }; /** * Intercept kernelspecs updates actions rather than subscribing to the store state changes (which * is triggered for *any* state change). * TODO: Use react-redux connect() to subscribe to state changes? */ const cacheKernelSpecsMiddleware: Middleware = , S extends AppState>({ dispatch, getState, }: MiddlewareAPI) => (next: Dispatch) => (action: A): A => { switch (action.type) { case actions.FETCH_KERNELSPECS_FULFILLED: { const payload = ((action as unknown) as actions.FetchKernelspecsFulfilled).payload; const defaultKernelName = payload.defaultKernelName; this.kernelSpecsForDisplay = Object.values(payload.kernelspecs) .filter((spec) => !spec.metadata?.hasOwnProperty("hidden")) .map((spec) => ({ name: spec.name, displayName: spec.displayName, })) .sort((a: KernelSpecsDisplay, b: KernelSpecsDisplay) => { // Put default at the top, otherwise lexicographically compare if (a.displayName === defaultKernelName) { return -1; } else if (b.name === defaultKernelName) { return 1; } else { return a.displayName.localeCompare(b.displayName); } }); break; } } return next(action); }; const traceErrorFct = (title: string, message: string) => { TelemetryProcessor.traceFailure(Action.NotebookErrorNotification, { dataExplorerArea: Constants.Areas.Notebook, title, message, level: "Error", }); console.error(`${title}: ${message}`); }; this.store = configureStore( initialState, params.contentProvider, traceErrorFct, [cacheKernelSpecsMiddleware], !params.isReadOnly ); // Additional configuration this.store.dispatch(configOption("editorType").action(params.cellEditorType ?? "codemirror")); this.store.dispatch( configOption("autoSaveInterval").action(params.autoSaveInterval ?? Constants.Notebook.autoSaveIntervalMs) ); this.store.dispatch(configOption("codeMirror.lineNumbers").action(true)); const readOnlyConfigOption = configOption("codeMirror.readOnly"); const readOnlyValue = params.isReadOnly ? "nocursor" : undefined; if (!readOnlyConfigOption) { defineConfigOption({ label: "Read-only", key: "codeMirror.readOnly", values: [ { label: "Read-Only", value: "nocursor" }, { label: "Not read-only", value: undefined }, ], defaultValue: readOnlyValue, }); } else { this.store.dispatch(readOnlyConfigOption.action(readOnlyValue)); } } /** * Handle notification coming from nteract * The messages coming from nteract are not good enough to expose to user. * We use the notificationsToUserEpic to control the messages from action. * We log possible errors coming from nteract in telemetry and display in console */ private handleNotification = (msg: Notification): void => { if (msg.level === "error") { TelemetryProcessor.traceFailure(Action.NotebookErrorNotification, { dataExplorerArea: Constants.Areas.Notebook, title: msg.title, message: msg.message, level: msg.level, }); console.error(`${msg.title}: ${msg.message}`); } else { console.log(`${msg.title}: ${msg.message}`); } }; }