mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-02-13 11:45:15 +00:00
288 lines
10 KiB
TypeScript
288 lines
10 KiB
TypeScript
// 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<AppState, AnyAction>;
|
|
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<AppState, AnyAction> {
|
|
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<string, HostRecord>().set(this.contentHostRef, jupyterHostRecord),
|
|
}),
|
|
comms: makeCommsRecord(),
|
|
contents: makeContentsRecord({
|
|
byRef: Immutable.Map<string, ContentRecord>(),
|
|
}),
|
|
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 = <D extends Dispatch<AnyAction>, S extends AppState>({
|
|
dispatch,
|
|
getState,
|
|
}: MiddlewareAPI<D, S>) => (next: Dispatch<AnyAction>) => <A extends AnyAction>(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}`);
|
|
}
|
|
};
|
|
}
|