diff --git a/attic/prosemirror/package.json b/attic/prosemirror/package.json index f953bfbc..c6a3126e 100644 --- a/attic/prosemirror/package.json +++ b/attic/prosemirror/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@codemirror/lang-python": "6.0.1", - "@datalayer/jupyter-react": "^0.12.0", + "@datalayer/jupyter-react": "^0.15.0", "@prosemirror-adapter/react": "0.2.4", "@types/orderedmap": "1.0.0", "codemirror": "6.0.1", diff --git a/examples/cra/package.json b/examples/cra/package.json index e58e39e6..f12b8b4d 100644 --- a/examples/cra/package.json +++ b/examples/cra/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@datalayer/icons-react": "^0.3.0", - "@datalayer/jupyter-react": "^0.12.0", + "@datalayer/jupyter-react": "^0.15.0", "@datalayer/primer-addons": "0.3.0", "jupyterlab-plotly": "^5.17.0", "plotly.js": "^2.26.2", diff --git a/examples/cra/src/examples/cell/CellComponents.tsx b/examples/cra/src/examples/cell/CellComponents.tsx index 408d0b24..b2eda44d 100755 --- a/examples/cra/src/examples/cell/CellComponents.tsx +++ b/examples/cra/src/examples/cell/CellComponents.tsx @@ -4,7 +4,7 @@ * MIT License */ -import { useCellStore, Cell } from "@datalayer/jupyter-react"; +import { useCellsStore, Cell } from "@datalayer/jupyter-react"; import CellToolbar from './CellToolbar'; const SOURCE_EXAMPLE = `""" @@ -36,7 +36,7 @@ ax2.set_ylabel('Undamped') plt.show()`; const CellPreview = () => { - const cellStore = useCellStore(); + const cellStore = useCellsStore(); return ( <>
source: {cellStore.source}
diff --git a/examples/cra/src/examples/cell/CellToolbar.tsx b/examples/cra/src/examples/cell/CellToolbar.tsx index 7d5588c2..01cdebbf 100755 --- a/examples/cra/src/examples/cell/CellToolbar.tsx +++ b/examples/cra/src/examples/cell/CellToolbar.tsx @@ -7,10 +7,10 @@ import React from 'react'; import { Box, IconButton, Text, Tooltip } from '@primer/react'; import { PlayIcon, ReplyIcon, ThreeBarsIcon } from '@primer/octicons-react'; -import { useCellStore } from '@datalayer/jupyter-react'; +import { useCellsStore } from '@datalayer/jupyter-react'; const CellToolbar: React.FC = () => { - const cellStore = useCellStore(); + const cellStore = useCellsStore(); const outputsCount = cellStore.outputsCount; return ( <> diff --git a/examples/next-js/package.json b/examples/next-js/package.json index 5a3a3859..3ae2d7db 100644 --- a/examples/next-js/package.json +++ b/examples/next-js/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@datalayer/icons-react": "^0.3.0", - "@datalayer/jupyter-react": "^0.12.0", + "@datalayer/jupyter-react": "^0.15.0", "@datalayer/primer-addons": "0.3.0", "autoprefixer": "^10.4.14", "eslint": "^8.40.0", diff --git a/examples/slate/package.json b/examples/slate/package.json index f35ef4ba..cd982ba7 100644 --- a/examples/slate/package.json +++ b/examples/slate/package.json @@ -24,7 +24,7 @@ }, "dependencies": { "@datalayer/icons-react": "^0.3.0", - "@datalayer/jupyter-react": "^0.12.0", + "@datalayer/jupyter-react": "^0.15.0", "@datalayer/primer-addons": "0.3.0", "@emotion/css": "^11.1.3", "@emotion/react": "^11.10.6", diff --git a/packages/docusaurus-plugin/package.json b/packages/docusaurus-plugin/package.json index ec592de3..e5cc53d5 100644 --- a/packages/docusaurus-plugin/package.json +++ b/packages/docusaurus-plugin/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@datalayer/icons-react": "^0.3.0", - "@datalayer/jupyter-react": "^0.12.0", + "@datalayer/jupyter-react": "^0.15.0", "@datalayer/primer-addons": "0.3.0", "@docusaurus/core": "^2.4.0", "@docusaurus/types": "^2.1.0" diff --git a/packages/lexical/package.json b/packages/lexical/package.json index 0a7e498d..d0445be4 100644 --- a/packages/lexical/package.json +++ b/packages/lexical/package.json @@ -59,7 +59,7 @@ }, "dependencies": { "@datalayer/icons-react": "^0.3.0", - "@datalayer/jupyter-react": "^0.12.0", + "@datalayer/jupyter-react": "^0.15.0", "@datalayer/primer-addons": "0.3.0", "@jupyterlab/application": "^4.0.0", "@jupyterlab/coreutils": "^6.0.0", diff --git a/packages/lexical/src/components/JupyterOutputComponent.tsx b/packages/lexical/src/components/JupyterOutputComponent.tsx index 7eda41d5..a37ca3b9 100644 --- a/packages/lexical/src/components/JupyterOutputComponent.tsx +++ b/packages/lexical/src/components/JupyterOutputComponent.tsx @@ -27,7 +27,7 @@ export const JupyterOutputComponent = (props: Props) => { adapter={outputAdapter} showEditor={false} autoRun={autoRun} - sourceId={outputNodeUuid} + id={outputNodeUuid} executeTrigger={executeTrigger} lumino={false} /> diff --git a/packages/lexical/src/examples/App2.tsx b/packages/lexical/src/examples/App2.tsx index 950e2e40..818c17e1 100644 --- a/packages/lexical/src/examples/App2.tsx +++ b/packages/lexical/src/examples/App2.tsx @@ -90,7 +90,7 @@ const Tabs = () => { diff --git a/packages/lexical/src/nodes/JupyterOutputNode.tsx b/packages/lexical/src/nodes/JupyterOutputNode.tsx index 0e68f3b8..f10263aa 100644 --- a/packages/lexical/src/nodes/JupyterOutputNode.tsx +++ b/packages/lexical/src/nodes/JupyterOutputNode.tsx @@ -8,7 +8,7 @@ import { LexicalEditor, EditorConfig, DecoratorNode, LexicalNode, NodeKey, Sprea import { UUID } from '@lumino/coreutils'; import { IOutput } from '@jupyterlab/nbformat'; import { OUTPUT_UUID_TO_CODE_UUID, CODE_UUID_TO_OUTPUT_KEY, CODE_UUID_TO_OUTPUT_UUID, OUTPUT_UUID_TO_OUTPUT_KEY } from "../plugins/JupyterPlugin"; -import { OutputAdapter } from "@datalayer/jupyter-react"; +import { OutputAdapter, newUuid } from "@datalayer/jupyter-react"; import JupyterOutputComponent from "../components/JupyterOutputComponent"; export type SerializedJupyterOutputNode = Spread< @@ -52,7 +52,7 @@ export class JupyterOutputNode extends DecoratorNode { /** @override */ static importJSON(serializedNode: SerializedJupyterOutputNode): JupyterOutputNode { - return $createJupyterOutputNode(serializedNode.source, new OutputAdapter(undefined, []), serializedNode.outputs, false, serializedNode.codeNodeUuid, serializedNode.outputNodeUuid); + return $createJupyterOutputNode(serializedNode.source, new OutputAdapter(newUuid(), undefined, []), serializedNode.outputs, false, serializedNode.codeNodeUuid, serializedNode.outputNodeUuid); } /** @override */ diff --git a/packages/lexical/src/plugins/JupyterPlugin.tsx b/packages/lexical/src/plugins/JupyterPlugin.tsx index 4a6d8adc..69bd4b71 100644 --- a/packages/lexical/src/plugins/JupyterPlugin.tsx +++ b/packages/lexical/src/plugins/JupyterPlugin.tsx @@ -10,7 +10,7 @@ import { $getNodeByKey, $createNodeSelection, $setSelection } from "lexical"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { $setBlocksType } from "@lexical/selection"; import { $insertNodeToNearestRoot } from '@lexical/utils'; -import { OutputAdapter } from '@datalayer/jupyter-react'; +import { OutputAdapter, newUuid } from '@datalayer/jupyter-react'; import { UUID } from '@lumino/coreutils'; import { IOutput } from '@jupyterlab/nbformat'; import { $createJupyterCodeNode, JupyterCodeNode, $isJupyterCodeNode } from "../nodes/JupyterCodeNode"; @@ -77,7 +77,7 @@ export const JupyterPlugin = () => { return true; } } - const jupyterOutputNode = $createJupyterOutputNode(code, new OutputAdapter(undefined, []), [], true, codeNodeUuid, UUID.uuid4()); + const jupyterOutputNode = $createJupyterOutputNode(code, new OutputAdapter(newUuid(), undefined, []), [], true, codeNodeUuid, UUID.uuid4()); $insertNodeToNearestRoot(jupyterOutputNode); const nodeSelection = $createNodeSelection(); nodeSelection.add(parentNode.__key); @@ -163,7 +163,7 @@ export const JupyterPlugin = () => { } else { selection.insertNodes([jupyterCodeNode]); } - const outputAdapter = new OutputAdapter(undefined, outputs); + const outputAdapter = new OutputAdapter(newUuid(), undefined, outputs); const jupyterOutputNode = $createJupyterOutputNode(code, outputAdapter, outputs || [], false, jupyterCodeNode.getCodeNodeUuid(), UUID.uuid4()) ; outputAdapter.outputArea.model.changed.connect((outputModel, args) => { editor.update(() => { diff --git a/packages/react/jupyter_react/static/README.md b/packages/react/jupyter_react/static/README.md deleted file mode 100644 index 5cde08ff..00000000 --- a/packages/react/jupyter_react/static/README.md +++ /dev/null @@ -1 +0,0 @@ -[![Datalayer](https://assets.datalayer.tech/datalayer-25.svg)](https://datalayer.io) diff --git a/packages/react/package.json b/packages/react/package.json index 709bcadc..d846a5f3 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@datalayer/jupyter-react", - "version": "0.14.0", + "version": "0.17.0", "description": "Jupyter React - React.js components 100% compatible with Jupyter.", "license": "MIT", "main": "lib/index.js", @@ -73,7 +73,7 @@ "@jupyter-widgets/output": "^6.0.0", "@jupyter/collaboration-extension": "^1.0.0", "@jupyter/web-components": "^0.15.3", - "@jupyter/ydoc": "^1.0.0", + "@jupyter/ydoc": "^2.0.1", "@jupyterlab/application": "^4.0.0", "@jupyterlab/application-extension": "^4.0.0", "@jupyterlab/apputils": "^4.0.0", @@ -146,6 +146,7 @@ "react-sparklines": "^1.7.0", "rxjs": "^6.6.0", "styled-components": "^5.3.10", + "ulid": "^2.3.0", "usehooks-ts": "^2.9.1", "utf-8-validate": "^6.0.3", "wildcard-match": "^5.1.2", diff --git a/packages/react/src/app/tabs/components/NotebookComponent.tsx b/packages/react/src/app/tabs/components/NotebookComponent.tsx index 85f1e281..ae7daffc 100644 --- a/packages/react/src/app/tabs/components/NotebookComponent.tsx +++ b/packages/react/src/app/tabs/components/NotebookComponent.tsx @@ -15,7 +15,7 @@ const NotebookComponent = () => { {/* { - const { type='code', source = '', autoStart, showToolbar=true } = props; - const { serverSettings, defaultKernel } = useJupyter(); + const { + autoStart, + showToolbar=true, + source = '', + type='code', + } = props; + const { defaultKernel, serverSettings } = useJupyter(); + const [id] = useState(props.id || newUuid()); const [adapter, setAdapter] = useState(); - const cellStore = useCellStore(); + + const cellsStore = useCellsStore(); const handleCellInitEvents = (adapter: CellAdapter) => { adapter.cell.model.contentChanged.connect( (cellModel, changedArgs) => { - cellStore.setSource(id, cellModel.sharedModel.getSource()); + cellsStore.setSource(id, cellModel.sharedModel.getSource()); } ); if (adapter.cell instanceof CodeCell) { adapter.cell.outputArea.outputLengthChanged?.connect( (outputArea, outputsCount) => { - cellStore.setOutputsCount(id, outputsCount); + cellsStore.setOutputsCount(id, outputsCount); } ); } adapter.sessionContext.initialize().then(() => { - if (!autoStart || !adapter.cell.model) { - return; - } - - // Perform auto-start for code or markdown cells - if (adapter.cell instanceof CodeCell) { - CodeCell.execute( - adapter.cell, - adapter.sessionContext - ); - } - - if (adapter.cell instanceof MarkdownCell) { - adapter.cell.rendered = true; + if (autoStart && adapter.cell.model) { + // Perform auto-start for code or markdown cells. + adapter.execute(); } }); adapter.sessionContext.kernelChanged.connect(() => { void adapter.sessionContext.session?.kernel?.info.then(info => { // Set that session/kernel is ready for this cell when the kernel is guaranteed to be connected - cellStore.setIsKernelSessionAvailable(id, true); + cellsStore.setKernelSessionAvailable(id, true); }) }); } @@ -88,14 +84,15 @@ export const Cell = (props: ICellProps) => { if (id && defaultKernel && serverSettings) { defaultKernel.ready.then(() => { const adapter = new CellAdapter({ + id, type, source, serverSettings, kernel: defaultKernel, boxOptions: {showToolbar} }); - cellStore.setAdapter(id, adapter); - cellStore.setSource(id, source); + cellsStore.setAdapter(id, adapter); + cellsStore.setSource(id, source); handleCellInitEvents(adapter); setAdapter(adapter); diff --git a/packages/react/src/components/cell/CellAdapter.ts b/packages/react/src/components/cell/CellAdapter.ts index 4688d675..81c4a12b 100755 --- a/packages/react/src/components/cell/CellAdapter.ts +++ b/packages/react/src/components/cell/CellAdapter.ts @@ -7,6 +7,7 @@ import { BoxPanel, Widget } from '@lumino/widgets'; import { find } from '@lumino/algorithm'; import { CommandRegistry } from '@lumino/commands'; +import { JSONObject } from '@lumino/coreutils'; import { SessionContext, ISessionContext, @@ -14,6 +15,7 @@ import { ToolbarButton, } from '@jupyterlab/apputils'; import { CodeCellModel, CodeCell, Cell, MarkdownCell, RawCell, MarkdownCellModel } from '@jupyterlab/cells'; +import { Kernel as JupyterKernel, KernelMessage } from '@jupyterlab/services'; import { ybinding, CodeMirrorMimeTypeService, @@ -34,6 +36,7 @@ import { RenderMimeRegistry, standardRendererFactories as initialFactories, } from '@jupyterlab/rendermime'; +import { execute as executeOutput } from './../output/OutputExecutor'; import { Session, ServerConnection, @@ -56,15 +59,18 @@ import CellCommands from './CellCommands'; interface BoxOptions { showToolbar?: boolean; } + export class CellAdapter { + private _id: string; private _cell: CodeCell | MarkdownCell | RawCell; private _kernel: Kernel; private _panel: BoxPanel; private _sessionContext: SessionContext; - private _type: 'code' | 'markdown' | 'raw' + private _type: 'code' | 'markdown' | 'raw'; - constructor(options: CellAdapter.ICellAdapterOptions) { - const { type, source, serverSettings, kernel, boxOptions } = options; + public constructor(options: CellAdapter.ICellAdapterOptions) { + const { id, type, source, serverSettings, kernel, boxOptions } = options; + this._id = id; this._kernel = kernel; this._type = type; this.setupCell(type, source, serverSettings, kernel, boxOptions); @@ -303,7 +309,7 @@ export class CellAdapter { }); handler.editor = editor; - CellCommands(commands, this._cell!, this._sessionContext, handler); + CellCommands(commands, this._cell!, handler, this); completer.hide(); completer.addClass('jp-Completer-Cell'); Widget.attach(completer, document.body); @@ -313,7 +319,7 @@ export class CellAdapter { icon: runIcon, onClick: () => { if (this._type === 'code') { - CodeCell.execute(this._cell as CodeCell, this._sessionContext); + this.execute(); } else if (this._type === 'markdown') { (this._cell as MarkdownCell).rendered = true; } @@ -377,15 +383,126 @@ export class CellAdapter { execute = () => { if (this._type === 'code') { - CodeCell.execute((this._cell as CodeCell), this._sessionContext); + this._execute(this._cell as CodeCell); } else if (this._type === 'markdown') { (this._cell as MarkdownCell).rendered = true; } - }; + } + + private async _execute( + cell: CodeCell, + metadata?: JSONObject + ): Promise { + const model = cell.model; + const code = model.sharedModel.getSource(); + if (!code.trim() || !this.kernel) { + model.sharedModel.transact(() => { + model.clearExecution(); + }, false); + return new Promise(() => {}); + } + const cellId = { cellId: model.sharedModel.getId() }; + metadata = { + ...model.metadata, + ...metadata, + ...cellId + }; + const { recordTiming } = metadata; + model.sharedModel.transact(() => { + model.clearExecution(); + cell.outputHidden = false; + }, false); + cell.setPrompt('*'); + model.trusted = true; + let future: + | JupyterKernel.IFuture< + KernelMessage.IExecuteRequestMsg, + KernelMessage.IExecuteReplyMsg + > + | undefined; + try { + const kernelMessagePromise = executeOutput( + this._id, + code, + cell.outputArea, + this._kernel, + metadata + ); + // cell.outputArea.future assigned synchronously in `execute`. + if (recordTiming) { + const recordTimingHook = (msg: KernelMessage.IIOPubMessage) => { + let label: string; + switch (msg.header.msg_type) { + case 'status': + label = `status.${ + (msg as KernelMessage.IStatusMsg).content.execution_state + }`; + break; + case 'execute_input': + label = 'execute_input'; + break; + default: + return true; + } + // If the data is missing, estimate it to now + // Date was added in 5.1: https://jupyter-client.readthedocs.io/en/stable/messaging.html#message-header + const value = msg.header.date || new Date().toISOString(); + const timingInfo: any = Object.assign( + {}, + model.getMetadata('execution') + ); + timingInfo[`iopub.${label}`] = value; + model.setMetadata('execution', timingInfo); + return true; + }; + cell.outputArea.future.registerMessageHook(recordTimingHook); + } else { + model.deleteMetadata('execution'); + } + // Save this execution's future so we can compare in the catch below. + future = cell.outputArea.future; + const executeReplyMessage = (await kernelMessagePromise)!; + model.executionCount = executeReplyMessage.content.execution_count; + if (recordTiming) { + const timingInfo = Object.assign( + {}, + model.getMetadata('execution') as any + ); + const started = executeReplyMessage.metadata.started as string; + // Started is not in the API, but metadata IPyKernel sends + if (started) { + timingInfo['shell.execute_reply.started'] = started; + } + // Per above, the 5.0 spec does not assume date, so we estimate is required + const finished = executeReplyMessage.header.date as string; + timingInfo['shell.execute_reply'] = + finished || new Date().toISOString(); + model.setMetadata('execution', timingInfo); + } + return executeReplyMessage; + } catch (e) { + // If we started executing, and the cell is still indicating this execution, clear the prompt. + if (future && !cell.isDisposed && cell.outputArea.future === future) { + cell.setPrompt(''); + if (recordTiming && future.isDisposed) { + // Record the time when the cell execution was aborted + const timingInfo: any = Object.assign( + {}, + model.getMetadata('execution') + ); + timingInfo['execution_failed'] = new Date().toISOString(); + model.setMetadata('execution', timingInfo); + } + } + throw e; + } + } + } export namespace CellAdapter { export type ICellAdapterOptions = { + id: string; type: 'code' | 'markdown' | 'raw'; source: string; serverSettings: ServerConnection.ISettings; diff --git a/packages/react/src/components/cell/CellCommands.ts b/packages/react/src/components/cell/CellCommands.ts index b08f8b2e..e48f36e0 100644 --- a/packages/react/src/components/cell/CellCommands.ts +++ b/packages/react/src/components/cell/CellCommands.ts @@ -7,7 +7,7 @@ import { CommandRegistry } from '@lumino/commands'; import { CompletionHandler } from '@jupyterlab/completer'; import { CodeCell, MarkdownCell, RawCell } from '@jupyterlab/cells'; -import { SessionContext } from '@jupyterlab/apputils'; +import CellAdapter from './CellAdapter'; const cmdIds = { invoke: 'completer:invoke', @@ -17,8 +17,8 @@ const cmdIds = { export const CellCommands = ( commandRegistry: CommandRegistry, cell: CodeCell | MarkdownCell | RawCell, - sessionContext: SessionContext, - completerHandler: CompletionHandler + completerHandler: CompletionHandler, + cellAdapter: CellAdapter, ): void => { commandRegistry.addCommand(cmdIds.invoke, { label: 'Completer: Invoke', @@ -31,7 +31,7 @@ export const CellCommands = ( commandRegistry.addCommand('run:cell', { execute: () => { if (cell instanceof CodeCell) { - CodeCell.execute(cell, sessionContext) + cellAdapter.execute(); } else if (cell instanceof MarkdownCell) { (cell as MarkdownCell).rendered = true; } diff --git a/packages/react/src/components/cell/CellState.ts b/packages/react/src/components/cell/CellState.ts index 0a7d1cdb..459cebcd 100644 --- a/packages/react/src/components/cell/CellState.ts +++ b/packages/react/src/components/cell/CellState.ts @@ -6,7 +6,7 @@ import { createStore } from 'zustand/vanilla'; import { useStore } from 'zustand'; -import CellAdapter from './CellAdapter'; +import { CellAdapter } from './CellAdapter'; export interface ICellState { source?: string; @@ -20,16 +20,16 @@ export interface ICellsState { areAllKernelSessionsReady: boolean; // Control the state for all cells } -export type CellState = ICellsState & { +export type CellsState = ICellsState & { setCells: (cells: Map) => void; setSource: (id: string, source: string) => void; setOutputsCount: (id: string, outputsCount: number) => void; - setIsKernelSessionAvailable: (id: string, kernelAvailable: boolean) => void; + setKernelSessionAvailable: (id: string, kernelAvailable: boolean) => void; setAdapter: (id: string, adapter?: CellAdapter) => void; getAdapter: (id: string) => CellAdapter | undefined; getSource: (id: string) => string | undefined; getOutputsCount: (id: string) => number | undefined; - getIsKernelSessionAvailable: (id: string) => boolean | undefined; + isKernelSessionAvailable: (id: string) => boolean | undefined; execute: (id?: string) => void; }; @@ -45,24 +45,22 @@ const areAllKernelSessionsAvailable = (cells: Map): boolean return true; }; -export const cellStore = createStore((set, get) => ({ +export const cellsStore = createStore((set, get) => ({ cells: new Map(), source: '', outputsCount: 0, - isKernelSessionAvailable: false, areAllKernelSessionsReady: false, adapter: undefined, - setCells: (cells: Map) => set((cell: CellState) => ({ cells })), - + setCells: (cells: Map) => set((cell: CellsState) => ({ cells })), setSource: (id: string, source: string) => { const cells = get().cells; const cell = cells.get(id); if (cell) { cell.source = source; } else { - cells.set(id, {source}); + cells.set(id, { source }); } - set((cell: CellState) => ({ cells })) + set((cell: CellsState) => ({ cells })) }, setOutputsCount: (id: string, outputsCount: number) => { const cells = get().cells; @@ -70,11 +68,11 @@ export const cellStore = createStore((set, get) => ({ if (cell) { cell.outputsCount = outputsCount; } else { - cells.set(id, {outputsCount}); + cells.set(id, { outputsCount }); } - set((state: CellState) => ({ cells })) + set((state: CellsState) => ({ cells })); }, - setIsKernelSessionAvailable: (id: string, isKernelSessionAvailable: boolean) => { + setKernelSessionAvailable: (id: string, isKernelSessionAvailable: boolean) => { const cells = get().cells; const cell = cells.get(id); if (cell) { @@ -83,7 +81,7 @@ export const cellStore = createStore((set, get) => ({ cells.set(id, {isKernelSessionAvailable}); } const areAllKernelSessionsReady = areAllKernelSessionsAvailable(cells); - set((cell: CellState) => ({ cells, areAllKernelSessionsReady })); + set((cell: CellsState) => ({ cells, areAllKernelSessionsReady })); }, setAdapter: (id: string, adapter?: CellAdapter) => { const cells = get().cells; @@ -93,7 +91,7 @@ export const cellStore = createStore((set, get) => ({ } else { cells.set(id, { adapter }); } - set((cell: CellState) => ({ cells })) + set((cell: CellsState) => ({ cells })) }, getAdapter: (id: string) => { return get().cells.get(id)?.adapter; @@ -104,24 +102,24 @@ export const cellStore = createStore((set, get) => ({ getOutputsCount: (id: string): number | undefined => { return get().cells.get(id)?.outputsCount; }, - getIsKernelSessionAvailable: (id: string): boolean | undefined => { + isKernelSessionAvailable: (id: string): boolean | undefined => { return get().cells.get(id)?.isKernelSessionAvailable; }, execute: (id: string) => { const cells = get().cells; const cell = cells.get(id); if (cell) { - cell.adapter?.execute() + cell.adapter?.execute(); } else { - get().cells.forEach((cell) => cell.adapter?.execute()) + get().cells.forEach((cell) => cell.adapter?.execute()); } }, })); -export function useCellStore(): CellState; -export function useCellStore(selector: (state: CellState) => T): T; -export function useCellStore(selector?: (state: CellState) => T) { - return useStore(cellStore, selector!); +export function useCellsStore(): CellsState; +export function useCellsStore(selector: (state: CellsState) => T): T; +export function useCellsStore(selector?: (state: CellsState) => T) { + return useStore(cellsStore, selector!); } -export default useCellStore; +export default useCellsStore; diff --git a/packages/react/src/components/codemirror/CodeMirrorEditor.tsx b/packages/react/src/components/codemirror/CodeMirrorEditor.tsx index 0e6204cf..aeee1352 100644 --- a/packages/react/src/components/codemirror/CodeMirrorEditor.tsx +++ b/packages/react/src/components/codemirror/CodeMirrorEditor.tsx @@ -13,7 +13,7 @@ import Kernel from '../../jupyter/kernel/Kernel'; import codeMirrorTheme from './CodeMirrorTheme'; import OutputAdapter from '../output/OutputAdapter'; import CodeMirrorOutputToolbar from './CodeMirrorOutputToolbar'; -import useOutputStore from '../output/OutputState'; +import useOutputsStore from '../output/OutputState'; export const CodeMirrorEditor = (props: { code: string; @@ -37,10 +37,10 @@ export const CodeMirrorEditor = (props: { insertText, kernel, } = props; - const outputStore = useOutputStore(); + const outputStore = useOutputsStore(); const [view, setView] = useState(); const dataset = outputStore.getDataset(sourceId); - const source = outputStore.getSource(sourceId); + const source = outputStore.getInput(sourceId); const editorDiv = useRef(); const setEditorSource = (source: string | undefined) => { if (view && source) { @@ -78,10 +78,7 @@ export const CodeMirrorEditor = (props: { return true; }; useEffect(() => { - outputStore.setSource({ - sourceId, - source: code, - }) + outputStore.setInput(sourceId, code); const language = new Compartment(); const keyBinding = [ { @@ -101,10 +98,7 @@ export const CodeMirrorEditor = (props: { EditorView.updateListener.of((viewUpdate: ViewUpdate) => { if (viewUpdate.docChanged) { const source = viewUpdate.state.doc.toString(); - outputStore.setSource({ - sourceId, - source, - }) + outputStore.setInput(sourceId, source); } }), ], @@ -122,10 +116,10 @@ export const CodeMirrorEditor = (props: { }; }, [code]); useEffect(() => { - doInsertText(dataset?.dataset); + doInsertText(dataset); }, [dataset]); useEffect(() => { - setEditorSource(source?.source); + setEditorSource(source); }, [source]); return ( <> diff --git a/packages/react/src/components/kernel/Kernelndicator.tsx b/packages/react/src/components/kernel/Kernelndicator.tsx index 0a2059de..f1428e77 100644 --- a/packages/react/src/components/kernel/Kernelndicator.tsx +++ b/packages/react/src/components/kernel/Kernelndicator.tsx @@ -23,7 +23,7 @@ import { KernelMessage } from '@jupyterlab/services'; import { ConnectionStatus, IKernelConnection } from '@jupyterlab/services/lib/kernel/kernel'; import { Environment } from '../environment/Environment'; -type KernelState = +export type ExecutionState = 'connecting' | 'connected-unknown' | 'connected-starting' | @@ -34,7 +34,8 @@ type KernelState = 'connected-autorestarting' | 'connected-dead' | 'disconnecting' | - 'undefined'; + 'undefined' + ; /** * The valid kernel connection states. @@ -54,7 +55,7 @@ type KernelState = * * Status = 'unknown' | 'starting' | 'idle' | 'busy' | 'terminating' | 'restarting' | 'autorestarting' | 'dead'; */ -export const KERNEL_STATES: Map = new Map([ +export const KERNEL_STATES: Map = new Map([ ['connecting', ], ['connected-unknown', ], ['connected-starting', ], @@ -68,27 +69,28 @@ export const KERNEL_STATES: Map = new Map([ ['undefined', ], ]); -type Props = { +export const toKernelState = ( + connectionStatus: ConnectionStatus, + status: KernelMessage.Status +): ExecutionState => { + if ( + connectionStatus === 'connecting' || + connectionStatus === 'disconnected' + ) { + return connectionStatus as ExecutionState; + } + return connectionStatus + '-' + status as ExecutionState; +}; + +type KernelIndicatorProps = { kernel?: IKernelConnection | null; env?: Environment; }; -export const KernelIndicator = (props: Props) => { +export const KernelIndicator = (props: KernelIndicatorProps) => { const { kernel, env } = props; const [connectionStatus, setConnectionStatus] = useState(); const [status, setStatus] = useState(); - const toState = ( - connectionStatus: ConnectionStatus, - status: KernelMessage.Status - ): KernelState => { - if ( - connectionStatus === 'connecting' || - connectionStatus === 'disconnected' - ) { - return connectionStatus as KernelState; - } - return connectionStatus + '-' + status as KernelState; - }; useEffect(() => { if (kernel) { setConnectionStatus(kernel?.connectionStatus); @@ -105,7 +107,7 @@ export const KernelIndicator = (props: Props) => { }, [kernel]); return connectionStatus && status ? ( - {KERNEL_STATES.get(toState(connectionStatus, status))} + {KERNEL_STATES.get(toKernelState(connectionStatus, status))} ) : ( diff --git a/packages/react/src/components/kernel/inspector/widget.tsx b/packages/react/src/components/kernel/inspector/widget.tsx index b1ae5870..99ce4970 100644 --- a/packages/react/src/components/kernel/inspector/widget.tsx +++ b/packages/react/src/components/kernel/inspector/widget.tsx @@ -12,7 +12,6 @@ import { closeIcon, jsonIcon, } from '@jupyterlab/ui-components'; -import { UUID } from '@lumino/coreutils'; import { Message as luminoMessage } from '@lumino/messaging'; import { Widget, BoxLayout } from '@lumino/widgets'; import { @@ -20,6 +19,7 @@ import { ObjectLabel, InspectorNodeParams, } from 'react-inspector'; +import { newUuid } from '../../../utils'; import { KernelSpyModel, ThreadIterator } from './model'; import './kernelinspector.css'; @@ -127,7 +127,7 @@ namespace Message { export class MessageLogView extends VDomRenderer { constructor(model: KernelSpyModel) { super(model); - this.id = `kernelspy-messagelog-${UUID.uuid4()}`; + this.id = `kernelspy-messagelog-${newUuid()}`; this.addClass('dla-KernelInspector-messagelog'); } @@ -216,7 +216,7 @@ export class KernelSpyView extends Widget { super(); this._model = new KernelSpyModel(kernel); this.addClass('dla-KernelInspector-view'); - this.id = `kernelspy-${UUID.uuid4()}`; + this.id = `kernelspy-${newUuid()}`; this.title.label = 'Kernel spy'; this.title.closable = true; this.title.icon = jsonIcon; diff --git a/packages/react/src/components/notebook/Notebook.tsx b/packages/react/src/components/notebook/Notebook.tsx index 076987e1..466d924a 100644 --- a/packages/react/src/components/notebook/Notebook.tsx +++ b/packages/react/src/components/notebook/Notebook.tsx @@ -63,7 +63,7 @@ export type INotebookProps = { path?: string; readOnly: boolean; renderers: IRenderMime.IRendererFactory[]; - uid: string; + id: string; url?: string; CellSidebar?: (props: CellSidebarProps) => JSX.Element; Toolbar?: (props: any) => JSX.Element; @@ -91,30 +91,30 @@ export const Notebook = (props: INotebookProps) => { } = props; const notebookStore = useNotebookStore(); - const [uid] = useState(props.uid || newUuid()); + const [id] = useState(props.id || newUuid()); const [adapter, setAdapter] = useState(); const kernel = propsKernel || defaultKernel; - const portals = notebookStore.selectNotebookPortals(uid); + const portals = notebookStore.selectNotebookPortals(id); const newAdapterState = () => { - if (uid && serviceManager && kernelManager && kernel) { + if (id && serviceManager && kernelManager && kernel) { kernel.ready.then(() => { const adapter = new NotebookAdapter( { ...props, kernel, - uid, + id, }, serviceManager, lite ); setAdapter(adapter); - notebookStore.update({ uid, partialState: { adapter: adapter } }) + notebookStore.update({ id, partialState: { adapter: adapter } }) adapter.serviceManager.ready.then(() => { if (!readOnly) { const activeCell = adapter.notebookPanel!.content.activeCell; if (activeCell) { notebookStore.activeCellChange({ - uid, + id, cellModel: activeCell, }); } @@ -122,7 +122,7 @@ export const Notebook = (props: INotebookProps) => { adapter.notebookPanel!.content.activeCellChanged ); activeCellChanged$.subscribe((cellModel: Cell) => { - notebookStore.activeCellChange({ uid, cellModel }); + notebookStore.activeCellChange({ id, cellModel }); const panelDiv = document.getElementById( 'right-panel-id' ) as HTMLDivElement; @@ -130,7 +130,7 @@ export const Notebook = (props: INotebookProps) => { const cellMetadataOptions = ( @@ -138,7 +138,7 @@ export const Notebook = (props: INotebookProps) => { ); const portal = createPortal(cellMetadataOptions, panelDiv); notebookStore.setPortalDisplay({ - uid, + id, portalDisplay: { portal, pinned: false }, }); } @@ -146,32 +146,32 @@ export const Notebook = (props: INotebookProps) => { } adapter.notebookPanel?.model?.contentChanged.connect( (notebookModel, _) => { - notebookStore.modelChange({ uid, notebookModel }); + notebookStore.modelChange({ id, notebookModel }); } ); /* adapter.notebookPanel?.model!.sharedModel.changed.connect((_, notebookChange) => { - notebookStore.notebookChange({ uid, notebookChange }); + notebookStore.notebookChange({ id, notebookChange }); }); adapter.notebookPanel?.content.modelChanged.connect((notebook, _) => { - dispatÅch(notebookStore.notebookChange({ uid, notebook })); + dispatÅch(notebookStore.notebookChange({ id, notebook })); }); */ adapter.notebookPanel?.content.activeCellChanged.connect( (_, cellModel) => { if (cellModel === null) { notebookStore.activeCellChange({ - uid, + id, cellModel: undefined, }); } else { - notebookStore.activeCellChange({ uid, cellModel }); + notebookStore.activeCellChange({ id, cellModel }); } } ); adapter.notebookPanel?.sessionContext.statusChanged.connect( (_, kernelStatus) => { - notebookStore.kernelStatusChanged({ uid, kernelStatus }); + notebookStore.kernelStatusChanged({ id, kernelStatus }); } ); }); @@ -184,10 +184,10 @@ export const Notebook = (props: INotebookProps) => { } newAdapterState(); return () => { - notebookStore.setPortalDisplay({ uid, portalDisplay: undefined }); - notebookStore.dispose(uid); + notebookStore.setPortalDisplay({ id, portalDisplay: undefined }); + notebookStore.dispose(id); }; - }, [uid, serviceManager, kernelManager, kernel, path]); + }, [id, serviceManager, kernelManager, kernel, path]); useEffect(() => { if (adapter && nbformat) { adapter.setNbformat(nbformat); @@ -198,7 +198,7 @@ export const Notebook = (props: INotebookProps) => { style={{ height, width: '100%', position: 'relative' }} id="dla-Jupyter-Notebook" > - {Toolbar && } + {Toolbar && } JSX.Element; constructor( @@ -100,7 +100,7 @@ export class NotebookAdapter { this._path = props.path; this._readOnly = props.readOnly; this._renderers = props.renderers; - this._uid = props.uid; + this._id = props.id; this._CellSidebar = props.CellSidebar; @@ -223,14 +223,14 @@ export class NotebookAdapter { const contentFactory = this._CellSidebar ? new JupyterReactContentFactory( this._CellSidebar, - this._uid, + this._id, this._nbgrader, this._commandRegistry, { editorFactory }, ) : new NotebookPanel.ContentFactory({ editorFactory }); - this._tracker = new NotebookTracker({ namespace: this._uid }); + this._tracker = new NotebookTracker({ namespace: this._id }); const notebookWidgetFactory = new NotebookWidgetFactory({ name: 'Notebook', @@ -418,8 +418,8 @@ export class NotebookAdapter { (this._context as Context).model.dirty = false; const now = new Date().toISOString(); const model: Contents.IModel = { - path: this._uid, - name: this._uid, + path: this._id, + name: this._id, type: 'notebook', content: undefined, writable: true, @@ -492,8 +492,8 @@ export class NotebookAdapter { }); } - get uid(): string { - return this._uid; + get id(): string { + return this._id; } get notebookPanel(): NotebookPanel | undefined { diff --git a/packages/react/src/components/notebook/NotebookState.ts b/packages/react/src/components/notebook/NotebookState.ts index bd50c826..ed36cfb7 100644 --- a/packages/react/src/components/notebook/NotebookState.ts +++ b/packages/react/src/components/notebook/NotebookState.ts @@ -36,217 +36,217 @@ export interface INotebooksState { notebooks: Map; } -type UpdateUid = { - uid: string; +type UpdateId = { + id: string; partialState: Partial; }; -type NotebookChangeUid = { - uid: string; +type NotebookChangeId = { + id: string; notebookChange: NotebookChange; }; -type NotebookModelUid = { - uid: string; +type NotebookModelId = { + id: string; notebookModel: INotebookModel; }; -type CellModelUid = { - uid: string; +type CellModelId = { + id: string; cellModel?: Cell; }; type KernelStatusMutation = { - uid: string; + id: string; kernelStatus: JupyterKernel.Status; }; type KernelChangeMutation = { - uid: string; + id: string; kernel: Kernel; }; type ReactPortalsMutation = { - uid: string; + id: string; portals: ReactPortal[]; }; type PortalDisplayMutation = { - uid: string; + id: string; portalDisplay: PortalDisplay | undefined; }; type DateMutation = { - uid: string; + id: string; date: Date | undefined; }; type CellMutation = { - uid: string; + id: string; cellType: nbformat.CellType; source?: string; }; export type NotebookState = INotebooksState & { setNotebooks: (notebooks: Map) => void; - selectNotebook: (uid: string) => INotebookState | undefined; - selectNotebookModel: (uid: string) => { model: INotebookModel | undefined; changed: any } | undefined; - selectKernelStatus: (uid: string) => string | undefined; - selectActiveCell: (uid: string) => Cell | undefined; - selectNotebookPortals: (uid: string) => React.ReactPortal[] | undefined; - selectSaveRequest: (uid: string) => Date | undefined; - selectNotebookPortalDisplay: (uid: string) => PortalDisplay | undefined; - run: (uid: string) => void; - runAll: (uid: string) => void; - interrupt: (uid: string) => void; + selectNotebook: (id: string) => INotebookState | undefined; + selectNotebookModel: (id: string) => { model: INotebookModel | undefined; changed: any } | undefined; + selectKernelStatus: (id: string) => string | undefined; + selectActiveCell: (id: string) => Cell | undefined; + selectNotebookPortals: (id: string) => React.ReactPortal[] | undefined; + selectSaveRequest: (id: string) => Date | undefined; + selectNotebookPortalDisplay: (id: string) => PortalDisplay | undefined; + run: (id: string) => void; + runAll: (id: string) => void; + interrupt: (id: string) => void; insertAbove: (mutation: CellMutation) => void; insertBelow: (mutation: CellMutation) => void; - delete: (uid: string) => void; + delete: (id: string) => void; changeCellType: (mutation: CellMutation) => void; save: (mutation: DateMutation) => void; reset: () => void; - update: (update: UpdateUid) => void; - activeCellChange: (cellModelUid: CellModelUid) => void; - modelChange: (notebookModelUid: NotebookModelUid) => void; - notebookChange: (notebookChangeUid: NotebookChangeUid) => void; - kernelStatusChanged: (kernelStatusUid: KernelStatusMutation) => void; + update: (update: UpdateId) => void; + activeCellChange: (cellModelId: CellModelId) => void; + modelChange: (notebookModelId: NotebookModelId) => void; + notebookChange: (notebookChangeId: NotebookChangeId) => void; + kernelStatusChanged: (kernelStatusId: KernelStatusMutation) => void; changeKernel: (kernelChange: KernelChangeMutation) => void; - addPortals: (portalsUid: ReactPortalsMutation) => void; - dispose: (uid: string) => void; - setPortals: (portalsUid: ReactPortalsMutation) => void; - setPortalDisplay: (portalDisplayUid: PortalDisplayMutation) => void; + addPortals: (portalsId: ReactPortalsMutation) => void; + dispose: (id: string) => void; + setPortals: (portalsId: ReactPortalsMutation) => void; + setPortalDisplay: (portalDisplayId: PortalDisplayMutation) => void; }; export const notebookStore = createStore((set, get) => ({ notebooks: new Map(), setNotebooks: (notebooks: Map) => set((state: NotebookState) => ({ notebooks })), - selectNotebook: (uid: string): INotebookState | undefined => { - return get().notebooks.get(uid); + selectNotebook: (id: string): INotebookState | undefined => { + return get().notebooks.get(id); }, - selectNotebookModel: (uid: string): { model: INotebookModel | undefined; changed: any } | undefined => { - if (get().notebooks.get(uid)) { + selectNotebookModel: (id: string): { model: INotebookModel | undefined; changed: any } | undefined => { + if (get().notebooks.get(id)) { return { - model: get().notebooks.get(uid)?.model, - changed: get().notebooks.get(uid)?.model?.contentChanged, + model: get().notebooks.get(id)?.model, + changed: get().notebooks.get(id)?.model?.contentChanged, }; } return undefined; }, - selectKernelStatus: (uid: string): string | undefined => { - return get().notebooks.get(uid)?.kernelStatus; + selectKernelStatus: (id: string): string | undefined => { + return get().notebooks.get(id)?.kernelStatus; }, - selectActiveCell: (uid: string): Cell | undefined => { - return get().notebooks.get(uid)?.activeCell; + selectActiveCell: (id: string): Cell | undefined => { + return get().notebooks.get(id)?.activeCell; }, - selectNotebookPortals: (uid: string): React.ReactPortal[] | undefined => { - return get().notebooks.get(uid)?.portals; + selectNotebookPortals: (id: string): React.ReactPortal[] | undefined => { + return get().notebooks.get(id)?.portals; }, - selectSaveRequest: (uid: string): Date | undefined => { - return get().notebooks.get(uid)?.saveRequest; + selectSaveRequest: (id: string): Date | undefined => { + return get().notebooks.get(id)?.saveRequest; }, - selectNotebookPortalDisplay: (uid: string): PortalDisplay | undefined => { - return get().notebooks.get(uid)?.portalDisplay; + selectNotebookPortalDisplay: (id: string): PortalDisplay | undefined => { + return get().notebooks.get(id)?.portalDisplay; }, - run: (uid: string): void => { get().notebooks.get(uid)?.adapter?.commands.execute(cmdIds.run); }, - runAll: (uid: string): void => { get().notebooks.get(uid)?.adapter?.commands.execute(cmdIds.runAll); }, - interrupt: (uid: string): void => { get().notebooks.get(uid)?.adapter?.commands.execute(cmdIds.interrupt); }, + run: (id: string): void => { get().notebooks.get(id)?.adapter?.commands.execute(cmdIds.run); }, + runAll: (id: string): void => { get().notebooks.get(id)?.adapter?.commands.execute(cmdIds.runAll); }, + interrupt: (id: string): void => { get().notebooks.get(id)?.adapter?.commands.execute(cmdIds.interrupt); }, insertAbove: (mutation: CellMutation) => { - get().notebooks.get(mutation.uid)?.adapter?.setDefaultCellType(mutation.cellType); - get().notebooks.get(mutation.uid)?.adapter?.insertAbove(mutation.source); + get().notebooks.get(mutation.id)?.adapter?.setDefaultCellType(mutation.cellType); + get().notebooks.get(mutation.id)?.adapter?.insertAbove(mutation.source); }, insertBelow: (mutation: CellMutation) => { - get().notebooks.get(mutation.uid)?.adapter?.setDefaultCellType(mutation.cellType); - get().notebooks.get(mutation.uid)?.adapter?.insertBelow(mutation.source); + get().notebooks.get(mutation.id)?.adapter?.setDefaultCellType(mutation.cellType); + get().notebooks.get(mutation.id)?.adapter?.insertBelow(mutation.source); }, - delete: (uid: string): void => { get().notebooks.get(uid)?.adapter?.commands.execute(cmdIds.deleteCells); }, + delete: (id: string): void => { get().notebooks.get(id)?.adapter?.commands.execute(cmdIds.deleteCells); }, changeCellType: (mutation: CellMutation) => { - get().notebooks.get(mutation.uid)?.adapter?.changeCellType(mutation.cellType); + get().notebooks.get(mutation.id)?.adapter?.changeCellType(mutation.cellType); }, save: (mutation: DateMutation) => { - get().notebooks.get(mutation.uid)?.adapter?.commands.execute(cmdIds.save); + get().notebooks.get(mutation.id)?.adapter?.commands.execute(cmdIds.save); const notebooks = get().notebooks; - const notebook = notebooks.get(mutation.uid); + const notebook = notebooks.get(mutation.id); if (notebook) { notebook.saveRequest = mutation.date; set((state: NotebookState) => ({ notebooks })); } }, reset: () => set((state: NotebookState) => ({ notebooks: new Map() })), - update: (update: UpdateUid) => { + update: (update: UpdateId) => { const notebooks = get().notebooks; - let notebook = notebooks.get(update.uid); + let notebook = notebooks.get(update.id); if (notebook) { notebook = { ...notebook, ...update.partialState }; set((state: NotebookState) => ({ notebooks })); } else { - notebooks.set(update.uid, { + notebooks.set(update.id, { adapter: update.partialState.adapter, portals: [], }); set((state: NotebookState) => ({ notebooks })); } }, - activeCellChange: (cellModelUid: CellModelUid) => { + activeCellChange: (cellModelId: CellModelId) => { const notebooks = get().notebooks; - const notebook = notebooks.get(cellModelUid.uid); + const notebook = notebooks.get(cellModelId.id); if (notebook) { - notebook.activeCell = cellModelUid.cellModel; + notebook.activeCell = cellModelId.cellModel; set((state: NotebookState) => ({ notebooks })); } }, - modelChange: (notebookModelUid: NotebookModelUid) => { + modelChange: (notebookModelId: NotebookModelId) => { const notebooks = get().notebooks; - const notebook = notebooks.get(notebookModelUid.uid); + const notebook = notebooks.get(notebookModelId.id); if (notebook) { - notebook.model = notebookModelUid.notebookModel; + notebook.model = notebookModelId.notebookModel; set((state: NotebookState) => ({ notebooks })); } }, - notebookChange: (notebookChangeUid: NotebookChangeUid) => { + notebookChange: (notebookChangeId: NotebookChangeId) => { const notebooks = get().notebooks; - const notebook = notebooks.get(notebookChangeUid.uid); + const notebook = notebooks.get(notebookChangeId.id); if (notebook) { - notebook.notebookChange = notebookChangeUid.notebookChange; + notebook.notebookChange = notebookChangeId.notebookChange; set((state: NotebookState) => ({ notebooks })); } }, - kernelStatusChanged: (kernelStatusUid: KernelStatusMutation) => { + kernelStatusChanged: (kernelStatusId: KernelStatusMutation) => { const notebooks = get().notebooks; - const notebook = notebooks.get(kernelStatusUid.uid); + const notebook = notebooks.get(kernelStatusId.id); if (notebook) { - notebook.kernelStatus = kernelStatusUid.kernelStatus; + notebook.kernelStatus = kernelStatusId.kernelStatus; set((state: NotebookState) => ({ notebooks })); } }, changeKernel: (kernelChange: KernelChangeMutation) => { const notebooks = get().notebooks; - const notebook = notebooks.get(kernelChange.uid); + const notebook = notebooks.get(kernelChange.id); if (notebook) { notebook.adapter?.assignKernel(kernelChange.kernel); set((state: NotebookState) => ({ notebooks })); } }, - addPortals: (portalsUid: ReactPortalsMutation) => { + addPortals: (portalsId: ReactPortalsMutation) => { const notebooks = get().notebooks; - const notebook = notebooks.get(portalsUid.uid); + const notebook = notebooks.get(portalsId.id); if (notebook) { - notebook.portals = notebook.portals.concat(portalsUid.portals); + notebook.portals = notebook.portals.concat(portalsId.portals); set((state: NotebookState) => ({ notebooks })); } }, - dispose: (uid: string): void => { + dispose: (id: string): void => { const notebooks = get().notebooks; - const notebook = notebooks.get(uid); + const notebook = notebooks.get(id); if(notebook){ notebook.adapter?.dispose(); - notebooks.delete(uid); + notebooks.delete(id); } set((state: NotebookState) => ({ notebooks })); }, - setPortals: (portalsUid: ReactPortalsMutation) => { + setPortals: (portalsId: ReactPortalsMutation) => { const notebooks = get().notebooks; - const notebook = notebooks.get(portalsUid.uid); + const notebook = notebooks.get(portalsId.id); if (notebook) { - notebook.portals = portalsUid.portals; + notebook.portals = portalsId.portals; set((state: NotebookState) => ({ notebooks })); } }, - setPortalDisplay: (portalDisplayUid: PortalDisplayMutation) => { + setPortalDisplay: (portalDisplayId: PortalDisplayMutation) => { const notebooks = get().notebooks; - const notebook = notebooks.get(portalDisplayUid.uid); + const notebook = notebooks.get(portalDisplayId.id); if (notebook) { - notebook.portalDisplay = portalDisplayUid.portalDisplay; + notebook.portalDisplay = portalDisplayId.portalDisplay; set((state: NotebookState) => ({ notebooks })); } }, diff --git a/packages/react/src/components/notebook/cell/metadata/CellMetadataEditor.tsx b/packages/react/src/components/notebook/cell/metadata/CellMetadataEditor.tsx index 829e56d4..ec75ba00 100644 --- a/packages/react/src/components/notebook/cell/metadata/CellMetadataEditor.tsx +++ b/packages/react/src/components/notebook/cell/metadata/CellMetadataEditor.tsx @@ -4,11 +4,12 @@ * MIT License */ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { ActionList, TextInput } from '@primer/react'; import { CheckIcon } from '@primer/octicons-react'; import { Cell, ICellModel } from '@jupyterlab/cells'; import NbGraderType, { getNbGraderType } from './NbGraderCells'; +import { newUlid } from '../../../../utils'; type Props = { notebookId: string; @@ -19,17 +20,23 @@ type Props = { export const CellMetadataEditor = (props: Props) => { const { cell } = props; const [cellGradeType, setCellGradeType] = useState(getNbGraderType(cell)); - const [nbg, setNbg] = useState( - cell.model.getMetadata('nbgrader') || { grade_id: '', points: 0 } + const [nbGrade, setNbGrade] = useState<{grade_id: string; points: number}>( + cell.model.getMetadata('nbgrader') ?? { grade_id: newUlid(), points: 1 } ); + useEffect(() => { + setNbGrade({ + grade_id: nbGrade.grade_id ?? newUlid(), + points: nbGrade.points ?? 1, + }); + }, [nbGrade]); const handleGradeIdChange = (cell: Cell, gradeId: string) => { const nbgrader = cell.model.getMetadata('nbgrader') as any; cell.model.setMetadata('nbgrader', { ...nbgrader, grade_id: gradeId, }); - setNbg({ - ...nbg, + setNbGrade({ + ...nbGrade, grade_id: gradeId, }); }; @@ -41,8 +48,8 @@ export const CellMetadataEditor = (props: Props) => { ...nbgrader, points: points_number, }); - setNbg({ - ...nbg, + setNbGrade({ + ...nbGrade, points: points_number, }); } @@ -65,6 +72,8 @@ export const CellMetadataEditor = (props: Props) => { solution: true, locked: false, task: false, + grade_id: newUlid(), + points: 1, }); setCellGradeType(NbGraderType.AutogradedAnswer); break; @@ -77,6 +86,8 @@ export const CellMetadataEditor = (props: Props) => { solution: false, locked: false, task: false, + grade_id: newUlid(), + points: 1, }); setCellGradeType(NbGraderType.AutogradedTest); break; @@ -89,6 +100,8 @@ export const CellMetadataEditor = (props: Props) => { solution: true, locked: false, task: false, + grade_id: newUlid(), + points: 1, }); setCellGradeType(NbGraderType.ManuallyGradedAnswer); break; @@ -102,6 +115,8 @@ export const CellMetadataEditor = (props: Props) => { solution: false, locked: true, task: true, + grade_id: newUlid(), + points: 1, }); setCellGradeType(NbGraderType.ManuallyGradedTask); break; @@ -217,7 +232,7 @@ export const CellMetadataEditor = (props: Props) => { { { e.preventDefault(); handleGradeIdChange(cell, e.target.value); @@ -230,7 +245,7 @@ export const CellMetadataEditor = (props: Props) => { { { e.preventDefault(); handlePointsChange(cell, e.target.value); diff --git a/packages/react/src/components/notebook/cell/metadata/NbGraderCells.tsx b/packages/react/src/components/notebook/cell/metadata/NbGraderCells.tsx index c7f7ed14..a9fa8f7e 100644 --- a/packages/react/src/components/notebook/cell/metadata/NbGraderCells.tsx +++ b/packages/react/src/components/notebook/cell/metadata/NbGraderCells.tsx @@ -84,14 +84,14 @@ export const getNbGraderType = (cell: Cell) => { if (!grade && solution && !locked) { return NbGraderType.AutogradedAnswer; } - // Autograded test (only for code cells) { grade: true, solution: false, locked: false, points: ... } - if (grade && !solution && !locked) { - return NbGraderType.AutogradedTest; - } // Manually graded task { grade: false, solution: false, locked: true, task: true, points: ... } if (!grade && !solution && locked && task) { return NbGraderType.ManuallyGradedTask; } + // Autograded test (only for code cells) { grade: true, solution: false, locked: true, points: ... } + if (grade && !solution && locked) { + return NbGraderType.AutogradedTest; + } // Manually graded answer { grade: true, solution: true, locked: false, points: ... } if (grade && solution && !locked) { return NbGraderType.ManuallyGradedAnswer; diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx index 035cf8b7..7deecb23 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx @@ -92,7 +92,7 @@ export const CellSidebar = (props: CellSidebarProps) => { onClick={(e: any) => { e.preventDefault(); notebookStore.insertAbove({ - uid: notebookId, + id: notebookId, cellType: 'code', }); }} @@ -109,7 +109,7 @@ export const CellSidebar = (props: CellSidebarProps) => { onClick={(e: any) => { e.preventDefault(); notebookStore.insertAbove({ - uid: notebookId, + id: notebookId, cellType: 'markdown', }); }} @@ -127,7 +127,7 @@ export const CellSidebar = (props: CellSidebarProps) => { onClick={(e: any) => { e.preventDefault(); notebookStore.changeCellType({ - uid: notebookId, + id: notebookId, cellType: 'markdown', }); }} @@ -143,7 +143,7 @@ export const CellSidebar = (props: CellSidebarProps) => { onClick={(e: any) => { e.preventDefault(); notebookStore.changeCellType({ - uid: notebookId, + id: notebookId, cellType: 'code', }); }} @@ -161,7 +161,7 @@ export const CellSidebar = (props: CellSidebarProps) => { onClick={(e: any) => { e.preventDefault(); notebookStore.insertBelow({ - uid: notebookId, + id: notebookId, cellType: 'markdown', }); }} @@ -178,7 +178,7 @@ export const CellSidebar = (props: CellSidebarProps) => { onClick={(e: any) => { e.preventDefault(); notebookStore.insertBelow({ - uid: notebookId, + id: notebookId, cellType: 'code', }); }} diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx index c0ed6751..96f39119 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx @@ -73,7 +73,7 @@ export const CellSidebarNew = (props: CellSidebarProps) => { onClick={e => { e.preventDefault(); notebookStore.insertAbove({ - uid: notebookId, + id: notebookId, cellType: 'code', }); }} @@ -90,7 +90,7 @@ export const CellSidebarNew = (props: CellSidebarProps) => { onClick={e => { e.preventDefault(); notebookStore.insertAbove({ - uid: notebookId, + id: notebookId, cellType: 'markdown', }); }} @@ -109,7 +109,7 @@ export const CellSidebarNew = (props: CellSidebarProps) => { onClick={e => { e.preventDefault(); notebookStore.changeCellType({ - uid: notebookId, + id: notebookId, cellType: 'markdown', }); }} @@ -124,7 +124,7 @@ export const CellSidebarNew = (props: CellSidebarProps) => { onClick={(e: any) => { e.preventDefault(); notebookStore.changeCellType({ - uid: notebookId, + id: notebookId, cellType: 'code', }); }} @@ -140,7 +140,7 @@ export const CellSidebarNew = (props: CellSidebarProps) => { onClick={e => { e.preventDefault(); notebookStore.insertBelow({ - uid: notebookId, + id: notebookId, cellType: 'markdown', }); }} @@ -157,7 +157,7 @@ export const CellSidebarNew = (props: CellSidebarProps) => { onClick={e => { e.preventDefault(); notebookStore.insertBelow({ - uid: notebookId, + id: notebookId, cellType: 'code', }); }} diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebarWidget.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebarWidget.tsx index e5c715b6..26ab666c 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebarWidget.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebarWidget.tsx @@ -48,7 +48,7 @@ export class CellSidebarWidget ); const portal = createPortal(portalDiv, this.node); notebookStore.getState().addPortals({ - uid: notebookId, + id: notebookId, portals: [portal], }); } diff --git a/packages/react/src/components/output/Output.tsx b/packages/react/src/components/output/Output.tsx index 8d43b274..3dc2c8d7 100644 --- a/packages/react/src/components/output/Output.tsx +++ b/packages/react/src/components/output/Output.tsx @@ -6,18 +6,18 @@ import { useState, useEffect } from 'react'; import { Box } from '@primer/react'; -import { UUID } from '@lumino/coreutils'; import { IOutput } from '@jupyterlab/nbformat'; import { IOutputAreaModel } from '@jupyterlab/outputarea'; import { KernelMessage } from '@jupyterlab/services'; +import { newUuid } from '../../utils'; import { useJupyter } from '../../jupyter/JupyterContext'; -import Kernel from '../../jupyter/kernel/Kernel'; +import { Kernel } from '../../jupyter/kernel/Kernel'; import { KernelActionMenu, KernelProgressBar } from './../kernel'; -import Lumino from '../lumino/Lumino'; -import CodeMirrorEditor from '../codemirror/CodeMirrorEditor'; -import OutputAdapter from './OutputAdapter'; -import OutputRenderer from './OutputRenderer'; -import { useOutputStore } from './OutputState'; +import { Lumino } from '../lumino/Lumino'; +import { CodeMirrorEditor } from '../codemirror/CodeMirrorEditor'; +import { OutputAdapter } from './OutputAdapter'; +import { OutputRenderer } from './OutputRenderer'; +import { useOutputsStore } from './OutputState'; import './Output.css'; @@ -29,8 +29,9 @@ export type IOutputProps = { codePre?: string; disableRun: boolean; executeTrigger: number; + id: string; insertText?: (payload?: any) => string; - kernel: Kernel; + kernel?: Kernel; lumino: boolean; model?: IOutputAreaModel; outputs?: IOutput[]; @@ -38,13 +39,12 @@ export type IOutputProps = { showControl?: boolean; showEditor: boolean; showKernelProgressBar?: boolean; - sourceId: string; toolbarPosition: 'up' | 'middle' | 'none'; }; export const Output = (props: IOutputProps) => { - const { defaultKernel: kernel } = useJupyter(); - const outputStore = useOutputStore(); + const { defaultKernel } = useJupyter(); + const outputStore = useOutputsStore(); const { adapter: propsAdapter, autoRun, @@ -54,60 +54,72 @@ export const Output = (props: IOutputProps) => { disableRun, executeTrigger, insertText, + kernel: propsKernel, lumino, model, + outputs: propsOutputs, receipt, showControl, showEditor, showKernelProgressBar = true, - sourceId, + id: sourceId, toolbarPosition, } = props; + const kernel = propsKernel ?? defaultKernel; const [id, setId] = useState(sourceId); - const [kernelStatus, setKernelStatus] = - useState('unknown'); - const [outputs, setOutputs] = useState(props.outputs); + const [kernelStatus, setKernelStatus] = useState('unknown'); + const [outputs, setOutputs] = useState(propsOutputs); const [adapter, setAdapter] = useState(); useEffect(() => { if (!id) { - setId(UUID.uuid4()); + setId(newUuid()); } }, []); useEffect(() => { - if (id && kernel) { - const adapter = - propsAdapter ?? new OutputAdapter(kernel, outputs ?? [], model); - if (receipt) { - adapter.outputArea.model.changed.connect((sender, change) => { - if (change.type === 'add') { - change.newValues.map(val => { - if (val && val.data) { - const out = val.data['text/html']; // val.data['application/vnd.jupyter.stdout']; - if (out) { - if ((out as string).indexOf(receipt) > -1) { - outputStore.setGrade({ - sourceId, - success: true, - }); - } - } + const outputsCallback = (model: IOutputAreaModel, change: IOutputAreaModel.ChangedArgs) => { + setOutputs(model.toJSON()); + if (id) { + outputStore.setModel(id, model); + } + }; + const receiptCallback = (model: IOutputAreaModel, change: IOutputAreaModel.ChangedArgs) => { + if (receipt && change.type === 'add') { + change.newValues.map(val => { + if (val && val.data) { + const out = val.data['text/html']; // val.data['application/vnd.jupyter.stdout']; + if (out) { + if ((out as string).indexOf(receipt) > -1) { + outputStore.setGradeSuccess(sourceId, true) } - }); + } } }); } + }; + if (id && kernel) { + const adapter = propsAdapter ?? new OutputAdapter(id, kernel, outputs ?? [], model); setAdapter(adapter); - outputStore.setAdapter(sourceId, adapter); - adapter.outputArea.model.changed.connect((outputModel, args) => { - setOutputs(outputModel.toJSON()); - }); + outputStore.setAdapter(id, adapter); + if (model) { + outputStore.setModel(id, model); + } + if (code) { + outputStore.setInput(id,code); + } + adapter.outputArea.model.changed.connect(outputsCallback); + if (receipt) { + adapter.outputArea.model.changed.connect(receiptCallback) + } + } + return () => { + if (adapter) { + adapter.outputArea.model.changed.disconnect(outputsCallback); + adapter.outputArea.model.changed.disconnect(receiptCallback) + } } }, [id, kernel]); useEffect(() => { if (adapter) { - if (!adapter.kernel) { - adapter.kernel = kernel; - } if (autoRun) { adapter.execute(code); } @@ -126,10 +138,10 @@ export const Output = (props: IOutputProps) => { }; } }, [kernel]); - const executeRequest = outputStore.getExecute(sourceId); + const executeRequest = outputStore.getExecuteRequest(sourceId); useEffect(() => { - if (adapter && executeRequest && executeRequest.sourceId === id) { - adapter.execute(executeRequest.source); + if (adapter && executeRequest && executeRequest === id) { + adapter.execute(code); } }, [executeRequest, adapter]); useEffect(() => { @@ -214,6 +226,7 @@ export const Output = (props: IOutputProps) => { }; Output.defaultProps = { + autoRun: false, clearTrigger: 0, disableRun: false, executeTrigger: 0, diff --git a/packages/react/src/components/output/OutputAdapter.ts b/packages/react/src/components/output/OutputAdapter.ts index 0304a48f..36c50214 100755 --- a/packages/react/src/components/output/OutputAdapter.ts +++ b/packages/react/src/components/output/OutputAdapter.ts @@ -5,27 +5,18 @@ */ import { IOutput } from '@jupyterlab/nbformat'; -import { - IOutputAreaModel, - OutputArea, - OutputAreaModel, -} from '@jupyterlab/outputarea'; -import { - IRenderMime, - RenderMimeRegistry, - standardRendererFactories, -} from '@jupyterlab/rendermime'; +import { IOutputAreaModel, OutputArea, OutputAreaModel } from '@jupyterlab/outputarea'; +import { IRenderMime, RenderMimeRegistry, standardRendererFactories } from '@jupyterlab/rendermime'; import { rendererFactory as jsonRendererFactory } from '@jupyterlab/json-extension'; import { rendererFactory as javascriptRendererFactory } from '@jupyterlab/javascript-extension'; -import { - WIDGET_MIMETYPE, - WidgetRenderer, -} from '@jupyter-widgets/html-manager/lib/output_renderers'; +import { WIDGET_MIMETYPE, WidgetRenderer } from '@jupyter-widgets/html-manager/lib/output_renderers'; import { requireLoader as loader } from '../../jupyter/ipywidgets/libembed-amd'; import { ClassicWidgetManager } from '../../jupyter/ipywidgets/classic/manager'; -import Kernel from '../../jupyter/kernel/Kernel'; +import { Kernel } from '../../jupyter/kernel/Kernel'; +import { execute } from './OutputExecutor'; export class OutputAdapter { + private _id: string; private _kernel?: Kernel; private _renderers: IRenderMime.IRendererFactory[]; private _outputArea: OutputArea; @@ -33,10 +24,12 @@ export class OutputAdapter { private _iPyWidgetsClassicManager: ClassicWidgetManager; public constructor( + id: string, kernel?: Kernel, outputs?: IOutput[], outputAreaModel?: IOutputAreaModel ) { + this._id = id; this._kernel = kernel; this._renderers = standardRendererFactories.filter( factory => factory.mimeTypes[0] !== 'text/javascript' @@ -89,8 +82,8 @@ export class OutputAdapter { public async execute(code: string) { if (this._kernel) { this.clear(); - await this._kernel?.execute(code, { model: this._outputArea.model }) - ?.done; + const done = execute(this._id, code, this._outputArea, this._kernel); + await done; } } diff --git a/packages/react/src/components/output/OutputExecutor.ts b/packages/react/src/components/output/OutputExecutor.ts new file mode 100755 index 00000000..66d34531 --- /dev/null +++ b/packages/react/src/components/output/OutputExecutor.ts @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +import { JSONObject } from '@lumino/coreutils'; +import { KernelMessage } from '@jupyterlab/services'; +import { OutputArea } from '@jupyterlab/outputarea'; +import { Kernel } from './../../jupyter/kernel/Kernel'; + +/** + * Execute code on an output area. + */ +export async function execute( + id: string, + code: string, + output: OutputArea, + kernel: Kernel, + metadata?: JSONObject +): Promise { + // Override the default for `stop_on_error`. + let stopOnError = true; + if ( + metadata && + Array.isArray(metadata.tags) && + metadata.tags.indexOf('raises-exception') !== -1 + ) { + stopOnError = false; + } + const kernelExecutor = kernel.execute( + code, + { + model: output.model, + stopOnError, + } + ); + const future = kernelExecutor!.future; + // TODO fix in upstream jupyterlab if possible... + (output as any)._onIOPub = future!.onIOPub; + (output as any)._onExecuteReply = future!.onReply; + output.future = future!; + return future?.done; +} diff --git a/packages/react/src/components/output/OutputState.ts b/packages/react/src/components/output/OutputState.ts index 5232c6d5..c2024dcf 100644 --- a/packages/react/src/components/output/OutputState.ts +++ b/packages/react/src/components/output/OutputState.ts @@ -6,38 +6,16 @@ import { createStore } from 'zustand/vanilla'; import { useStore } from 'zustand'; -import OutputAdapter from './OutputAdapter'; - -export namespace OutputState { - export type ISource = { - sourceId: string; - source: string; - increment?: number; - }; - export type IDataset = { - sourceId: string; - dataset: any; - increment?: number; - }; - export type IExecute = { - sourceId: string; - source: string; - increment?: number; - }; - export type IGrade = { - sourceId: string; - success: boolean; - increment?: number; - }; -} +import { IOutputAreaModel } from '@jupyterlab/outputarea'; +import { OutputAdapter } from './OutputAdapter'; export type IOutputState = { adapter?: OutputAdapter; - source?: OutputState.ISource; - dataset?: OutputState.IDataset; - setSource?: OutputState.ISource; - execute?: OutputState.IExecute; - grade?: OutputState.IGrade; + model?: IOutputAreaModel; + input?: string; + dataset?: any; + code?: string; + gradeSuccess?: boolean; }; export interface IOutputsState { @@ -45,22 +23,42 @@ export interface IOutputsState { } export type OutputState = IOutputsState & { - setOutputs: (outputs: Map) => void; - setAdapter: (id: string, adapter: OutputAdapter) => void; - setDataset: (dataset: OutputState.IDataset) => void; - setExecute: (execute: OutputState.IExecute) => void; - setSource: (source: OutputState.ISource) => void; - setGrade: (grade: OutputState.IGrade) => void; getAdapter: (id: string) => OutputAdapter | undefined; - getSource: (id: string) => OutputState.ISource | undefined; - getDataset: (id: string) => OutputState.IDataset | undefined; - getExecute: (id: string) => OutputState.IExecute | undefined; - getGrade: (id: string) => OutputState.IGrade | undefined; + getDataset: (id: string) => any | undefined; + getExecuteRequest: (id: string) => string | undefined; + getGradeSuccess: (id: string) => boolean | undefined; + getInput: (id: string) => string | undefined; + getModel: (id: string) => IOutputAreaModel | undefined; + setAdapter: (id: string, adapter: OutputAdapter) => void; + setDataset: (id: string, dataset: any) => void; + setExecuteRequest: (id: string, code: string) => void; + setGradeSuccess: (id: string, gradeSuccess: boolean) => void; + setInput: (id: string, source: string) => void; + setModel: (id: string, output: IOutputAreaModel) => void; + setOutputs: (id: string, outputs: Map) => void; }; -export const outputStore = createStore((set, get) => ({ +export const outputsStore = createStore((set, get) => ({ outputs: new Map(), - setOutputs: (outputs: Map) => set((state: OutputState) => ({ outputs })), + getAdapter: (id: string) => { + return get().outputs.get(id)?.adapter; + }, + getInput: (id: string): string | undefined => { + return get().outputs.get(id)?.input; + }, + getModel: (id: string): IOutputAreaModel | undefined => { + return get().outputs.get(id)?.model; + }, + getDataset: (id: string): any | undefined => { + return get().outputs.get(id)?.dataset; + }, + getExecuteRequest: (id: string): string | undefined => { + return get().outputs.get(id)?.code; + }, + getGradeSuccess: (id: string): boolean | undefined => { + return get().outputs.get(id)?.gradeSuccess; + }, + setOutputs: (id: string, outputs: Map) => set((state: OutputState) => ({ outputs })), setAdapter: (id: string, adapter: OutputAdapter) => { const outputs = get().outputs; const d = outputs.get(id); @@ -71,71 +69,62 @@ export const outputStore = createStore((set, get) => ({ } set((state: OutputState) => ({ outputs })) }, - setDataset: (dataset: OutputState.IDataset) => { - const sourceId = dataset.sourceId; + setDataset: (id: string, dataset: string) => { const outputs = get().outputs; - const d = outputs.get(sourceId); + const d = outputs.get(id); if (d) { d.dataset = dataset; } else { - outputs.set(sourceId, { dataset }); + outputs.set(id, { dataset }); } set((state: OutputState) => ({ outputs })) }, - setExecute: (execute: OutputState.IExecute) => { - const sourceId = execute.sourceId; + setExecuteRequest: (id: string, code: string) => { const outputs = get().outputs; - const e = outputs.get(sourceId); + const e = outputs.get(id); if (e) { - e.execute = execute; + e.code = code; } else { - outputs.set(sourceId, { execute }); + outputs.set(id, { code }); } set((state: OutputState) => ({ outputs })) }, - setSource: (setSource: OutputState.ISource) => { - const sourceId = setSource.sourceId; + setModel: (id: string, model: IOutputAreaModel) => { const outputs = get().outputs; - const s = outputs.get(sourceId); - if (s) { - s.setSource = setSource; + const e = outputs.get(id); + if (e) { + e.model = model; } else { - outputs.set(sourceId, { setSource }); + outputs.set(id, { model }); } set((state: OutputState) => ({ outputs })) }, - setGrade: (grade: OutputState.IGrade) => { - const sourceId = grade.sourceId; + setInput: (id: string, input: string) => { const outputs = get().outputs; - const g = outputs.get(sourceId); - if (g) { - g.grade = grade; + const e = outputs.get(id); + if (e) { + e.input = input; } else { - outputs.set(sourceId, { grade }); + outputs.set(id, { input }); } set((state: OutputState) => ({ outputs })) }, - getAdapter: (id: string) => { - return get().outputs.get(id)?.adapter; - }, - getSource: (id: string): OutputState.ISource | undefined => { - return get().outputs.get(id)?.source; - }, - getDataset: (id: string): OutputState.IDataset | undefined => { - return get().outputs.get(id)?.dataset; - }, - getExecute: (id: string): OutputState.IExecute | undefined => { - return get().outputs.get(id)?.execute; - }, - getGrade: (id: string): OutputState.IGrade | undefined => { - return get().outputs.get(id)?.grade; + setGradeSuccess: (id: string, gradeSuccess: boolean) => { + const outputs = get().outputs; + const e = outputs.get(id); + if (e) { + e.gradeSuccess = gradeSuccess; + } else { + outputs.set(id, { gradeSuccess }); + } + set((state: OutputState) => ({ outputs })) }, })); -export function useOutputStore(): OutputState; -export function useOutputStore(selector: (state: OutputState) => T): T; -export function useOutputStore(selector?: (state: OutputState) => T) { - return useStore(outputStore, selector!); +export function useOutputsStore(): OutputState; +export function useOutputsStore(selector: (state: OutputState) => T): T; +export function useOutputsStore(selector?: (state: OutputState) => T) { + return useStore(outputsStore, selector!); } -export default useOutputStore; +export default useOutputsStore; diff --git a/packages/react/src/examples/All.tsx b/packages/react/src/examples/All.tsx index 663b7332..2f8de0af 100644 --- a/packages/react/src/examples/All.tsx +++ b/packages/react/src/examples/All.tsx @@ -19,16 +19,16 @@ import Terminal from '../components/terminal/Terminal'; import CellSidebarNew from '../components/notebook/cell/sidebar/CellSidebarButton'; import CellSidebar from '../components/notebook/cell/sidebar/CellSidebar'; import Console from '../components/console/Console'; -import { useCellStore } from '../components/cell/CellState'; +import { useCellsStore } from '../components/cell/CellState'; import useNotebookStore from '../components/notebook/NotebookState'; import notebook from './notebooks/NotebookExample1.ipynb.json'; const SOURCE_1 = '1+1'; -const NOTEBOOK_UID_1 = 'notebook-1-uid'; -const NOTEBOOK_UID_2 = 'notebook-2-uid'; -const NOTEBOOK_UID_3 = 'notebook-3-uid'; +const NOTEBOOK_ID_1 = 'notebook-1-id'; +const NOTEBOOK_ID_2 = 'notebook-2-id'; +const NOTEBOOK_ID_3 = 'notebook-3-id'; const SOURCE_1_OUTPUTS: IOutput[] = [ { @@ -54,18 +54,18 @@ interface ICellToolProps { } const CellPreview = (props: ICellToolProps) => { - const cellStore = useCellStore(); + const cellsStore = useCellsStore(); return ( <> - <>source: {cellStore.getSource(props.id)} - <>kernel available: {String(cellStore.getIsKernelSessionAvailable(props.id))} + <>source: {cellsStore.getSource(props.id)} + <>kernel available: {String(cellsStore.isKernelSessionAvailable(props.id))} ); }; const CellToolbar = (props: ICellToolProps) => { const {id} = props; - const cellStore = useCellStore(); + const cellsStore = useCellsStore(); return ( <> @@ -73,20 +73,20 @@ const CellToolbar = (props: ICellToolProps) => { - Outputs count: {cellStore.getOutputsCount(id)} + Outputs count: {cellsStore.getOutputsCount(id)} ); }; @@ -101,7 +101,7 @@ const NotebookToolbar = () => { size="small" onClick={() => notebookStore.save({ - uid: NOTEBOOK_UID_1, + id: NOTEBOOK_ID_1, date: new Date(), }) } @@ -112,7 +112,7 @@ const NotebookToolbar = () => { variant="default" size="small" onClick={() => - notebookStore.runAll(NOTEBOOK_UID_1) + notebookStore.runAll(NOTEBOOK_ID_1) } > Run all @@ -131,12 +131,11 @@ const NotebookKernelChange = () => { kernelManager, kernelName: 'defaultKernel', kernelSpecName: 'python', - kernelType: 'notebook', kernelspecsManager: serviceManager.kernelspecs, sessionManager: serviceManager.sessions, }); kernel.ready.then(() => { - notebookStore.changeKernel({ uid: NOTEBOOK_UID_2, kernel }); + notebookStore.changeKernel({ id: NOTEBOOK_ID_2, kernel }); alert('Kernel is changed.'); }); } @@ -153,7 +152,7 @@ const NotebookKernelChange = () => { ); @@ -195,7 +194,7 @@ root.render(
diff --git a/packages/react/src/examples/Bokeh.tsx b/packages/react/src/examples/Bokeh.tsx index 7d92dcb6..f2aa2df7 100644 --- a/packages/react/src/examples/Bokeh.tsx +++ b/packages/react/src/examples/Bokeh.tsx @@ -14,7 +14,7 @@ const Bokeh = () => ( ( { - const cellStore = useJupyterStore().cellStore(); - const cellId = 'cell-1' - - console.log('Cell Outputs', (cellStore.getAdapter(cellId)?.cell as CodeCell).outputArea.model.toJSON()); + const { defaultKernel } = useJupyter(); + const cellsStore = useCellsStore(); + const kernelsStore = useKernelsStore(); + console.log('Cell Outputs', (cellsStore.getAdapter(CELL_ID)?.cell as CodeCell)?.outputArea.model.toJSON()); return ( A Jupyter Cell - Outputs Count: {cellStore.getOutputsCount(cellId)} + Source: {cellsStore.getSource(CELL_ID)} + + + Outputs Count: {cellsStore.getOutputsCount(CELL_ID)} + + defaultKernel + Kernel State: {defaultKernel && kernelsStore.getExecutionState(defaultKernel.id)} - Source: {cellStore.getSource(cellId)} + Kernel Phase: {defaultKernel && kernelsStore.getExecutionPhase(defaultKernel.id)} - + - + + + + ) } +const div = document.createElement('div'); +document.body.appendChild(div); +const root = createRoot(div); + root.render(); diff --git a/packages/react/src/examples/Cells.tsx b/packages/react/src/examples/Cells.tsx index 036a48e2..08f18795 100644 --- a/packages/react/src/examples/Cells.tsx +++ b/packages/react/src/examples/Cells.tsx @@ -16,10 +16,9 @@ const root = createRoot(div); root.render( Jupyter Cells wrapped in a single Jupyter Context - - - + + diff --git a/packages/react/src/examples/Dashboard.tsx b/packages/react/src/examples/Dashboard.tsx index 17972ad3..4ad6870d 100644 --- a/packages/react/src/examples/Dashboard.tsx +++ b/packages/react/src/examples/Dashboard.tsx @@ -17,7 +17,7 @@ const Dashboard = () => ( ( ( ( ( ( { +export const KernelExecuteView = () => { const { defaultKernel } = useJupyter(); const [running, setRunning] = useState(false); const [code, setCode] = useState(''); @@ -72,10 +72,10 @@ export const KernelExecResultView = () => { ); }; -const KernelExecResult = () => { +const KernelExecute = () => { return ( - + ); }; @@ -84,4 +84,4 @@ const div = document.createElement('div'); document.body.appendChild(div); const root = createRoot(div); -root.render(); +root.render(); diff --git a/packages/react/src/examples/KernelExecutor.tsx b/packages/react/src/examples/KernelExecutor.tsx index ef583895..62e9c43d 100644 --- a/packages/react/src/examples/KernelExecutor.tsx +++ b/packages/react/src/examples/KernelExecutor.tsx @@ -36,14 +36,14 @@ const KernelExecutorView = () => { msg: KernelMessage.IIOPubMessage ) => { // Do something with the IOPub message. - console.debug('---iopubMessage', msg); + console.log('---iopubMessage', msg); return true; }; const shellMessageHook: ShellMessageHook = ( msg: KernelMessage.IShellControlMessage ) => { - // Do something with the IOPub message. - console.debug('---shellMessage', msg); + // Do something with the Shell message. + console.log('---shellMessage', msg); return true; }; const kernelExecutor = defaultKernel.execute(CODE, { diff --git a/packages/react/src/examples/Matplotlib.tsx b/packages/react/src/examples/Matplotlib.tsx index 3f2cb09e..9815f373 100644 --- a/packages/react/src/examples/Matplotlib.tsx +++ b/packages/react/src/examples/Matplotlib.tsx @@ -19,7 +19,7 @@ const Matplotlib = () => ( ( { /> { const [nbformat, setNbFormat] = useState(); const [updatedNbFormat, setUpdatedNbFormat] = useState(); - const model = notebookStore.getState().selectNotebookModel(NOTEBOOK_UID)?.model; + const model = notebookStore.getState().selectNotebookModel(NOTEBOOK_ID)?.model; useEffect(() => { // Set nbformat with any content. // This may come from an external storage that you fetch in this react effect. @@ -58,7 +58,7 @@ const NotebookExternalContent = () => {
{ kernelManager, kernelName: JUPYTER_KERNEL_NAME, kernelSpecName: JUPYTER_KERNEL_NAME, - kernelType: 'notebook', kernelspecsManager: serviceManager.kernelspecs, sessionManager: serviceManager.sessions, }); @@ -67,7 +66,7 @@ const NotebookInit: React.FC = () => { // console.log("You can use one of these commands:", notebook.adapter?.commands.listCommands()); // notebook.adapter?.commands.execute("notebook:run-all"); notebookStore.insertAbove({ - uid: NOTEBOOK_ID, + id: NOTEBOOK_ID, cellType: 'code', source: 'print("Hello 🪐 ⚛️ Jupyter React")', }); @@ -78,7 +77,7 @@ const NotebookInit: React.FC = () => { return kernel ? ( { kernelManager, kernelName: NEW_KERNEL_NAME, kernelSpecName: NEW_KERNEL_NAME, - kernelType: 'notebook', kernelspecsManager: serviceManager.kernelspecs, sessionManager: serviceManager.sessions, }); kernel.ready.then(() => { - notebookStore.changeKernel({ uid: NOTEBOOK_UID, kernel }); + notebookStore.changeKernel({ id: NOTEBOOK_ID, kernel }); alert( `The kernel is changed (was python3, now ${NEW_KERNEL_NAME}). Bummer, all your variables are lost!` ); @@ -48,7 +47,7 @@ const NotebookKernelChange = () => { diff --git a/packages/react/src/examples/NotebookLite.tsx b/packages/react/src/examples/NotebookLite.tsx index 9541ae1e..eab0cb8f 100644 --- a/packages/react/src/examples/NotebookLite.tsx +++ b/packages/react/src/examples/NotebookLite.tsx @@ -21,7 +21,7 @@ const NotebookLite = () => ( A Jupyter Notebook with a Lite Kernel ( { const notebookStore = useNotebookStore(); @@ -24,7 +24,7 @@ const NotebookNbFormatChange = () => { const changeModel = () => { console.log( 'Notebook NbFormat from store', - notebookStore.notebooks.get(NOTEBOOK_UID)?.model?.toJSON() as INotebookContent + notebookStore.notebooks.get(NOTEBOOK_ID)?.model?.toJSON() as INotebookContent ); nbformat === nbformat1 ? setNbformat(nbformat2) : setNbformat(nbformat1); }; @@ -38,7 +38,7 @@ const NotebookNbFormatChange = () => {
( {
diff --git a/packages/react/src/examples/NotebookSkeleton.tsx b/packages/react/src/examples/NotebookSkeleton.tsx index 81ac39f6..d9671b0f 100644 --- a/packages/react/src/examples/NotebookSkeleton.tsx +++ b/packages/react/src/examples/NotebookSkeleton.tsx @@ -11,7 +11,7 @@ import Notebook from '../components/notebook/Notebook'; import NotebookToolbar from './toolbars/NotebookToolbar'; import CellSidebar from '../components/notebook/cell/sidebar/CellSidebarButton'; -const NOTEBOOK_UID = 'notebook-uid'; +const NOTEBOOK_ID = 'notebook-id'; const div = document.createElement('div'); document.body.appendChild(div); @@ -21,7 +21,7 @@ root.render( }> { /> { { kernelManager, kernelName: 'defaultKernel', kernelSpecName: 'python', - kernelType: 'notebook', kernelspecsManager: serviceManager.kernelspecs, sessionManager: serviceManager.sessions, }); @@ -53,7 +52,7 @@ const NotebookUnmount = () => { { - const outputStore = useJupyterStore().outputStore(); - console.log('Outputs 1', outputStore.getAdapter(SOURCE_ID_1)?.outputArea.model.toJSON()); + const outputStore = useOutputsStore(); + console.log( + 'Outputs 1', + outputStore.getModel(SOURCE_ID_1)?.toJSON(), + outputStore.getInput(SOURCE_ID_1), + ); return ( <> Output without Code Editor ); @@ -55,23 +77,80 @@ const OutputWithoutEditor = () => { const OutputWithEditor = () => { const { defaultKernel } = useJupyter(); - const outputStore = useJupyterStore().outputStore(); - console.log('Outputs 2', outputStore.getAdapter(SOURCE_ID_2)?.outputArea.model.toJSON()); + const outputStore = useOutputsStore(); + console.log( + 'Outputs 2', + outputStore.getModel(SOURCE_ID_2)?.toJSON(), + outputStore.getInput(SOURCE_ID_2), + ); return ( <> Output with Code Editor ); }; +const OutputWithEmptyOutput = () => { + const { kernelManager, serviceManager } = useJupyter(); + const outputStore = useOutputsStore(); + const kernelsStore = useKernelsStore(); + const [kernel, setKernel] = useState(); + useEffect( () => { + if (serviceManager && kernelManager) { + const kernel = new Kernel({ + kernelManager, + kernelName: 'kernel-example', + kernelSpecName: 'python', + kernelspecsManager: serviceManager.kernelspecs, + path: newUuid(), + sessionManager: serviceManager.sessions, + }); + setKernel(kernel); + } + }, [serviceManager, kernelManager]); + console.log( + 'Outputs 3', + outputStore.getModel(SOURCE_ID_3)?.toJSON(), + outputStore.getInput(SOURCE_ID_3), + ); + return ( + <> + Output with empty Output + { kernel && + <> + + Kernel State: {kernelsStore.getExecutionState(kernel.id)} + + + Kernel Phase: {kernelsStore.getExecutionPhase(kernel.id)} + + + + + + + + + } + + ); +}; + const div = document.createElement('div'); document.body.appendChild(div); const root = createRoot(div); @@ -80,5 +159,6 @@ root.render( + ); diff --git a/packages/react/src/examples/Panel.tsx b/packages/react/src/examples/Panel.tsx index 7f9e9fdf..48cde2a8 100644 --- a/packages/react/src/examples/Panel.tsx +++ b/packages/react/src/examples/Panel.tsx @@ -15,7 +15,7 @@ const Panel = () => { ( ( { onClick={(e: any) => { e.preventDefault(); notebookStore.insertAbove({ - uid: notebookId, + id: notebookId, cellType: 'code', source: "print('Hello 🪐 ⚛️ Jupyter React, I have been inserted up ⬆️.')", @@ -90,7 +90,7 @@ export const CellSidebarSource = (props: CellSidebarProps) => { onClick={(e: any) => { e.preventDefault(); notebookStore.insertAbove({ - uid: notebookId, + id: notebookId, cellType: 'markdown', }); }} @@ -107,7 +107,7 @@ export const CellSidebarSource = (props: CellSidebarProps) => { onClick={(e: any) => { e.preventDefault(); notebookStore.changeCellType({ - uid: notebookId, + id: notebookId, cellType: 'markdown', }); }} @@ -122,7 +122,7 @@ export const CellSidebarSource = (props: CellSidebarProps) => { onClick={(e: any) => { e.preventDefault(); notebookStore.changeCellType({ - uid: notebookId, + id: notebookId, cellType: 'code', }); }} @@ -139,7 +139,7 @@ export const CellSidebarSource = (props: CellSidebarProps) => { onClick={(e: any) => { e.preventDefault(); notebookStore.insertBelow({ - uid: notebookId, + id: notebookId, cellType: 'markdown', }); }} @@ -155,7 +155,7 @@ export const CellSidebarSource = (props: CellSidebarProps) => { onClick={(e: any) => { e.preventDefault(); notebookStore.insertBelow({ - uid: notebookId, + id: notebookId, cellType: 'code', source: "print('Hello 🪐 ⚛️ Jupyter React, I have been inserted down ⬇️.')", diff --git a/packages/react/src/examples/toolbars/NotebookToolbar.tsx b/packages/react/src/examples/toolbars/NotebookToolbar.tsx index 0d685631..5a1c295c 100644 --- a/packages/react/src/examples/toolbars/NotebookToolbar.tsx +++ b/packages/react/src/examples/toolbars/NotebookToolbar.tsx @@ -53,7 +53,7 @@ export const NotebookToolbar = (props: { notebookId: string }) => { onClick={e => { e.preventDefault(); notebookStore.save({ - uid: notebookId, + id: notebookId, date: new Date(), }); }} @@ -132,17 +132,17 @@ export const NotebookToolbar = (props: { notebookId: string }) => { e.preventDefault(); if (type === 'raw') { notebookStore.insertBelow({ - uid: notebookId, + id: notebookId, cellType: 'raw', }); } else if (type === 'code') { notebookStore.insertBelow({ - uid: notebookId, + id: notebookId, cellType: 'code', }); } else if (type === 'markdown') { notebookStore.insertBelow({ - uid: notebookId, + id: notebookId, cellType: 'markdown', }); } diff --git a/packages/react/src/examples/toolbars/NotebookToolbarAutoSave.tsx b/packages/react/src/examples/toolbars/NotebookToolbarAutoSave.tsx index 2762c12b..c711c24c 100644 --- a/packages/react/src/examples/toolbars/NotebookToolbarAutoSave.tsx +++ b/packages/react/src/examples/toolbars/NotebookToolbarAutoSave.tsx @@ -57,7 +57,7 @@ export const NotebookToolbarAutoSave = (props: { notebookId: string }) => { onClick={e => { e.preventDefault(); notebookStore.save({ - uid: notebookId, + id: notebookId, date: new Date(), }); }} @@ -145,7 +145,7 @@ export const NotebookToolbarAutoSave = (props: { notebookId: string }) => { onClick={e => { e.preventDefault(); notebookStore.insertBelow({ - uid: notebookId, + id: notebookId, cellType: addType, }); }} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 400464e1..5b5b61cd 100755 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -101,7 +101,7 @@ export * from './components/notebook/cell/sidebar/CellSidebarRun'; // Outputs. // @todo CodeMirrorEditor imported by Output breaks the JupyterLab extension loading. // @see https://github.com/datalayer/jupyter-ui/issues/170 -// export * from './components/output/Output'; +export * from './components/output/Output'; export * from './components/output/OutputAdapter'; export * from './components/output/OutputState'; export * from './components/output/OutputIPyWidgets'; diff --git a/packages/react/src/jupyter/kernel/Kernel.ts b/packages/react/src/jupyter/kernel/Kernel.ts index 8859b8f0..62bcff12 100755 --- a/packages/react/src/jupyter/kernel/Kernel.ts +++ b/packages/react/src/jupyter/kernel/Kernel.ts @@ -5,7 +5,7 @@ */ import { find } from '@lumino/algorithm'; -import { PromiseDelegate, UUID } from '@lumino/coreutils'; +import { PromiseDelegate } from '@lumino/coreutils'; import { Kernel as JupyterKernel, KernelMessage, @@ -14,7 +14,7 @@ import { } from '@jupyterlab/services'; import { ISessionConnection } from '@jupyterlab/services/lib/session/session'; import { ConnectionStatus } from '@jupyterlab/services/lib/kernel/kernel'; -import { getCookie } from '../../utils/Utils'; +import { getCookie, newUuid } from '../../utils/Utils'; import KernelExecutor, { IOPubMessageHook, ShellMessageHook, @@ -51,20 +51,22 @@ export class Kernel { kernelspecsManager, kernelSpecName, kernelModel, + path, sessionManager, } = props; this._kernelSpecManager = kernelspecsManager; this._kernelManager = kernelManager; this._kernelName = kernelName; - this._kernelType = kernelType; + this._kernelType = kernelType ?? 'notebook'; this._kernelSpecName = kernelSpecName; this._sessionManager = sessionManager; this._ready = new PromiseDelegate(); - this.requestKernel(kernelModel); + this.requestKernel(kernelModel, path); } private async requestKernel( - kernelModel?: JupyterKernel.IModel + kernelModel?: JupyterKernel.IModel, + propsPath?: string, ): Promise { await this._kernelManager.ready; await this._sessionManager.ready; @@ -78,9 +80,9 @@ export class Kernel { this._session = this._sessionManager.connectTo({ model }); } } else { - let path = getCookie(this.cookieName); + let path = propsPath ?? getCookie(this.cookieName); if (!path) { - path = 'kernel-' + UUID.uuid4(); + path = 'kernel-' + newUuid(); document.cookie = this.cookieName + '=' + path; } this._path = path; @@ -185,19 +187,21 @@ export class Kernel { execute( code: string, { + model, iopubMessageHooks = [], shellMessageHooks = [], - model, silent, stopOnError, storeHistory, + allowStdin, }: { + model?: IOutputAreaModel; iopubMessageHooks?: IOPubMessageHook[]; shellMessageHooks?: ShellMessageHook[]; - model?: IOutputAreaModel; silent?: boolean; stopOnError?: boolean; storeHistory?: boolean; + allowStdin?: boolean; } = {} ): KernelExecutor | undefined { if (this._kernelConnection) { @@ -211,6 +215,7 @@ export class Kernel { silent, stopOnError, storeHistory, + allowStdin, }); return kernelExecutor; } @@ -264,6 +269,10 @@ export namespace Kernel { * Kernel options */ export type IKernelProps = { + /** + * A path + */ + path?: string; /** * Kernel manager */ @@ -283,7 +292,7 @@ export namespace Kernel { /** * Kernel type */ - kernelType: 'notebook' | 'file'; + kernelType?: 'notebook' | 'file' | undefined; /** * Session manager */ diff --git a/packages/react/src/jupyter/kernel/KernelExecutor.ts b/packages/react/src/jupyter/kernel/KernelExecutor.ts index 6f98267c..28023a97 100644 --- a/packages/react/src/jupyter/kernel/KernelExecutor.ts +++ b/packages/react/src/jupyter/kernel/KernelExecutor.ts @@ -15,9 +15,11 @@ import { IMimeBundle, } from '@jupyterlab/nbformat'; import { IOutputAreaModel, OutputAreaModel } from '@jupyterlab/outputarea'; -import { Kernel, KernelMessage } from '@jupyterlab/services'; +import { Kernel as JupyterKernel, KernelMessage } from '@jupyterlab/services'; import { IClearOutputMsg } from '@jupyterlab/services/lib/kernel/messages'; import { outputsAsString } from '../../utils/Utils'; +import { ExecutionPhase, KernelsState, kernelsStore } from './KernelState'; +import { toKernelState } from '../../components/kernel'; export type IOPubMessageHook = ( msg: KernelMessage.IIOPubMessage @@ -30,11 +32,11 @@ export type ShellMessageHook = ( /** * KernelExecutor options */ -export interface IKernelExecutorOptions { +export type IKernelExecutorOptions = { /** * Kernel connection */ - connection: Kernel.IKernelConnection; + connection: JupyterKernel.IKernelConnection; /** * Outputs model to populate with the execution results. */ @@ -42,23 +44,23 @@ export interface IKernelExecutorOptions { } export class KernelExecutor { - private _kernelConnection: Kernel.IKernelConnection; - private _outputs: IOutput[]; - private _outputsChanged = new Signal(this); + private _executed: PromiseDelegate; + private _kernelConnection: JupyterKernel.IKernelConnection; + private _kernelState: KernelsState; private _model: IOutputAreaModel; private _modelChanged = new Signal(this); - private _future?: Kernel.IFuture< - KernelMessage.IExecuteRequestMsg, - KernelMessage.IExecuteReplyMsg - >; + private _outputs: IOutput[]; + private _stopOnError: boolean; + private _outputsChanged = new Signal(this); + private _future?: JupyterKernel.IFuture; private _shellMessageHooks = new Array(); - private _executed: PromiseDelegate; - constructor({ connection, model }: IKernelExecutorOptions) { + public constructor({ connection, model }: IKernelExecutorOptions) { + this._executed = new PromiseDelegate(); this._kernelConnection = connection; - this._outputs = []; this._model = model ?? new OutputAreaModel(); - this._executed = new PromiseDelegate(); + this._outputs = []; + this._kernelState = kernelsStore.getState(); } /** @@ -84,35 +86,38 @@ export class KernelExecutor { silent = false, stopOnError = false, storeHistory = true, + allowStdin = false, }: { iopubMessageHooks?: IOPubMessageHook[]; shellMessageHooks?: ShellMessageHook[]; silent?: boolean; stopOnError?: boolean; storeHistory?: boolean; + allowStdin?: boolean; } = {} ): Promise { + this._stopOnError = stopOnError; this._shellMessageHooks = shellMessageHooks; + kernelsStore.getState().setExecutionPhase(this._kernelConnection.id, ExecutionPhase.running); this._future = this._kernelConnection.requestExecute({ code, - allow_stdin: false, + allow_stdin: allowStdin, silent, stop_on_error: stopOnError, store_history: storeHistory, }); - iopubMessageHooks.forEach(hook => this._future!.registerMessageHook(hook)); this._future.onIOPub = this._onIOPub; this._future.onReply = this._onReply; - /* - FIXME Handle stdin. It will require updating the `allow_stdin` param aboove . - future.onStdin = msg => { + iopubMessageHooks.forEach(hook => this._future!.registerMessageHook(hook)); + this._future.onStdin = msg => { if (KernelMessage.isInputRequestMsg(msg)) { - this.onInputRequest(msg, value); + // FIXME Implement this. + // this.onInputRequest(msg, value); } }; - */ - // Wait for future to be done before resolving. + // Wait for future to be done before resolving the exectud promise. this._future.done.then(() => { + kernelsStore.getState().setExecutionPhase(this._kernelConnection.id, ExecutionPhase.completed); this._executed.resolve(this._model); }); return this._executed.promise; @@ -127,10 +132,71 @@ export class KernelExecutor { this._model.clear(); } + registerIOPubMessageHook = (msg: IOPubMessageHook) => { + this._future?.registerMessageHook(msg); + }; + + /** + * + */ + get future(): JupyterKernel.IFuture< + KernelMessage.IExecuteRequestMsg, + KernelMessage.IExecuteReplyMsg + > | undefined { + return this._future; + } + + /** + * Promise that resolves when the execution is done. + */ + get done(): Promise { + return this._executed.promise.then(() => { + return; + }); + } + + /** + * Code execution result as serialized JSON + */ + get result(): Promise { + return this._executed.promise.then(model => { + return outputsAsString(model.toJSON()); + }); + } + + /** + * Kernel outputs emitted. + */ + get outputs(): IOutput[] { + return this._outputs; + } + + /** + * Kernel outputs wrapped in a model. + */ + get model(): IOutputAreaModel { + return this._model; + } + + /** + * Signal emitted when the outputs list changes. + */ + get outputsChanged(): ISignal {0 + return this._outputsChanged; + } + + /** + * Signal emitted when the outputs model changes. + */ + get modelChanged(): ISignal { + return this._modelChanged; + } + private _onIOPub = (message: KernelMessage.IIOPubMessage): void => { if (this._future?.msg.header.msg_id !== message.parent_header.msg_id) { return; } + console.debug('Kernel IOPub message', message); const messageType: KernelMessage.IOPubMessageType = message.header.msg_type; const output = { ...message.content, output_type: messageType }; switch (messageType) { @@ -152,6 +218,9 @@ export class KernelExecutor { this._outputsChanged.emit(this._outputs); this._model.add(output); this._modelChanged.emit(this._model); + if (this._stopOnError) { + kernelsStore.getState().setExecutionPhase(this._kernelConnection.id, ExecutionPhase.completed_with_error); + } break; case 'clear_output': const wait = (message as IClearOutputMsg).content.wait; @@ -165,9 +234,10 @@ export class KernelExecutor { this._modelChanged.emit(this._model); break; case 'status': - // execution_state: 'busy' 'starting' 'terminating' 'restarting' 'initializing' 'connecting' 'disconnected' 'dead' 'unknown' 'idle' - const executionState = (message.content as any).execution_state; - executionState; + const executionState = (message.content as any).execution_state as KernelMessage.Status; + const connectionStatus = this._kernelConnection.connectionStatus; + const kernelState = toKernelState(connectionStatus!, executionState); + this._kernelState.setExecutionState(this._kernelConnection.id, kernelState); break; default: break; @@ -178,6 +248,7 @@ export class KernelExecutor { if (this._future?.msg.header.msg_id !== message.parent_header.msg_id) { return; } + console.debug('Kernel Reply message', message); this._shellMessageHooks.forEach(hook => hook(message)); const content = message.content; if (content.status !== 'ok') { @@ -219,56 +290,6 @@ export class KernelExecutor { } }; - registerIOPubMessageHook = (msg: IOPubMessageHook) => { - this._future?.registerMessageHook(msg); - }; - - /** - * Promise that resolves when the execution is done. - */ - get done(): Promise { - return this._executed.promise.then(() => { - return; - }); - } - - /** - * Code execution result as serialized JSON - */ - get result(): Promise { - return this._executed.promise.then(model => { - return outputsAsString(model.toJSON()); - }); - } - - /** - * Kernel outputs emitted. - */ - get outputs(): IOutput[] { - return this._outputs; - } - - /** - * Kernel outputs wrapped in a model. - */ - get model(): IOutputAreaModel { - return this._model; - } - - /** - * Signal emitted when the outputs list changes. - */ - get outputsChanged(): ISignal {0 - return this._outputsChanged; - } - - /** - * Signal emitted when the outputs model changes. - */ - get modelChanged(): ISignal { - return this._modelChanged; - } - } export default KernelExecutor; diff --git a/packages/react/src/jupyter/kernel/KernelState.ts b/packages/react/src/jupyter/kernel/KernelState.ts new file mode 100644 index 00000000..b071c4a2 --- /dev/null +++ b/packages/react/src/jupyter/kernel/KernelState.ts @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +import { createStore } from 'zustand/vanilla'; +import { useStore } from 'zustand'; +import {ExecutionState } from './../../components/kernel/Kernelndicator'; + +export enum ExecutionPhase { + ready_to_run = 'READY_TO_RUN', + running = 'RUNNING', + completed = 'COMPLETED', + completed_with_error = 'COMPLETED_WITH_ERROR', +} + +export type IKernelState = { + id: string; + executionState?: ExecutionState; + executionPhase?: ExecutionPhase; +}; + +export interface IKernelsState { + kernels: Map; +} + +export type KernelsState = IKernelsState & { + getExecutionState: (id: string) => ExecutionState | undefined; + setExecutionState: (id: string, executionState: ExecutionState) => void; + getExecutionPhase: (id: string) => ExecutionPhase | undefined; + setExecutionPhase: (id: string, executionState: ExecutionPhase) => void; +}; + +export const kernelsStore = createStore((set, get) => ({ + kernels: new Map(), + getExecutionState: (id: string) => { + return get().kernels.get(id)?.executionState; + }, + setExecutionState: (id: string, executionState: ExecutionState) => { + const kernels = get().kernels; + const k = kernels.get(id); + if (k) { + k.executionState = executionState; + } else { + kernels.set(id, { + id, + executionState: executionState + }); + } + set((state: KernelsState) => ({ kernels })) + }, + getExecutionPhase: (id: string) => { + return get().kernels.get(id)?.executionPhase; + }, + setExecutionPhase: (id: string, executionPhase: ExecutionPhase) => { + const kernels = get().kernels; + const k = kernels.get(id); + if (k) { + k.executionPhase = executionPhase; + } else { + kernels.set(id, { + id, + executionPhase, + }); + } + set((state: KernelsState) => ({ kernels })) + }, +})); + +export function useKernelsStore(): KernelsState; +export function useKernelsStore(selector: (state: KernelsState) => T): T; +export function useKernelsStore(selector?: (state: KernelsState) => T) { + return useStore(kernelsStore, selector!); +} + +export default useKernelsStore; diff --git a/packages/react/src/state/State.ts b/packages/react/src/state/State.ts index 6f0b9fcf..0748ecda 100644 --- a/packages/react/src/state/State.ts +++ b/packages/react/src/state/State.ts @@ -10,29 +10,29 @@ import { useStore } from 'zustand'; import { ServiceManager } from '@jupyterlab/services'; import type { IDatalayerConfig } from './IState'; import { IJupyterConfig, loadJupyterConfig } from '../jupyter/JupyterConfig'; -import useCellStore from '../components/cell/CellState'; -import useConsoleStore from '../components/console/ConsoleState'; -import useNotebookStore from '../components/notebook/NotebookState'; -import useOutputStore from '../components/output/OutputState'; -import useTerminalStore from '../components/terminal/TerminalState'; +import { cellsStore, CellsState } from '../components/cell/CellState'; +import { consoleStore, ConsoleState } from '../components/console/ConsoleState'; +import { notebookStore, NotebookState } from '../components/notebook/NotebookState'; +import { outputsStore, OutputState } from '../components/output/OutputState'; +import { terminalStore, TerminalState } from '../components/terminal/TerminalState'; import { createLiteServer } from '../jupyter/lite/LiteServer'; import { getJupyterServerUrl } from '../jupyter/JupyterConfig'; import { ensureJupyterAuth, createServerSettings, JupyterContextPropsType } from '../jupyter/JupyterContext'; import Kernel from '../jupyter/kernel/Kernel'; export type JupyterState = { - cellStore: typeof useCellStore; - consoleStore: typeof useConsoleStore; + cellsStore: CellsState; + consoleStore: ConsoleState; datalayerConfig?: IDatalayerConfig; jupyterConfig?: IJupyterConfig; kernel?: Kernel; kernelIsLoading: boolean; - notebookStore: typeof useNotebookStore; - outputStore: typeof useOutputStore; + notebookStore: NotebookState; + outputStore: OutputState; serviceManager?: ServiceManager; setDatalayerConfig: (configuration?: IDatalayerConfig) => void; setVersion: (version: string) => void; - terminalStore: typeof useTerminalStore; + terminalStore: TerminalState; version: string; }; @@ -63,11 +63,11 @@ export const jupyterStore = createStore((set, get) => ({ kernel: undefined, serviceManager: undefined, serverSettings: undefined, - cellStore: useCellStore, - consoleStore: useConsoleStore, - notebookStore: useNotebookStore, - outputStore: useOutputStore, - terminalStore: useTerminalStore, + cellsStore: cellsStore.getState(), + consoleStore: consoleStore.getState(), + notebookStore: notebookStore.getState(), + outputStore: outputsStore.getState(), + terminalStore: terminalStore.getState(), })); // TODO Reuse code portions from JupyterContext @@ -187,7 +187,6 @@ export function useJupyterStoreFromContext(props: JupyterContextPropsType): Jupy kernelName: defaultKernelName, kernelSpecName: defaultKernelName, kernelModel: kernel.value, - kernelType: 'notebook', kernelspecsManager: serviceManager.kernelspecs, sessionManager: serviceManager.sessions, }); @@ -215,7 +214,6 @@ export function useJupyterStoreFromContext(props: JupyterContextPropsType): Jupy kernelManager, kernelName: defaultKernelName, kernelSpecName: defaultKernelName, - kernelType: 'notebook', kernelspecsManager: serviceManager.kernelspecs, sessionManager: serviceManager.sessions, }); diff --git a/packages/react/src/utils/Utils.ts b/packages/react/src/utils/Utils.ts index be66eb17..7d65af32 100644 --- a/packages/react/src/utils/Utils.ts +++ b/packages/react/src/utils/Utils.ts @@ -5,19 +5,16 @@ */ import { ICell, IOutput } from '@jupyterlab/nbformat'; +import { ulid } from 'ulid'; import { UUID } from '@lumino/coreutils'; -// const MAX = Number.MAX_SAFE_INTEGER; -// const MAX = 999999; - -export const newSourceId = (base: string) => { - // return base + Math.floor(Math.random() * MAX).toString(); - return base; -}; +export const newUlid = () => { + return ulid() +} export const newUuid = () => { return UUID.uuid4(); -}; +} export const cellSourceAsString = (cell: ICell) => { let source = cell.source; @@ -25,7 +22,7 @@ export const cellSourceAsString = (cell: ICell) => { source = (source as []).join('\n'); } return source; -}; +} export const outputsAsString = (outputs: IOutput[]) => { let result = ''; @@ -89,4 +86,4 @@ export const getCookie = (name: string): string | null => { return decodeURIComponent(cookie.substring(nameLenPlus)); })[0] || null ); -}; +} diff --git a/packages/react/webpack.config.js b/packages/react/webpack.config.js index 0c0345d1..65becb77 100644 --- a/packages/react/webpack.config.js +++ b/packages/react/webpack.config.js @@ -19,6 +19,7 @@ function shim(regExp) { const ENTRY = // './src/app/App'; // './src/examples/Cell'; + // './src/examples/Cells'; // './src/examples/CellLite'; // './src/examples/Console'; // './src/examples/ConsoleLite'; @@ -30,8 +31,8 @@ const ENTRY = // './src/examples/JupyterLabApp'; // './src/examples/JupyterLabHeadlessApp'; // './src/examples/Kernels'; + // './src/examples/KernelExecute'; // './src/examples/KernelExecutor'; - // './src/examples/KernelExecResult'; // './src/examples/Lumino'; // './src/examples/Matplotlib'; './src/examples/Notebook'; diff --git a/storybook/package.json b/storybook/package.json index be46b93d..12d1f6fa 100644 --- a/storybook/package.json +++ b/storybook/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@datalayer/jupyter-lexical": "^0.1.0", - "@datalayer/jupyter-react": "^0.12.0", + "@datalayer/jupyter-react": "^0.15.0", "react": "^18.2.0", "react-dom": "^18.2.0" },