From 03470cdea6753c5686062742dfae933efee92acf Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Thu, 24 Oct 2024 15:54:46 +0200 Subject: [PATCH 01/12] bump: jupyter-react --- packages/react/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/package.json b/packages/react/package.json index 76b25159..6cf0f4b4 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@datalayer/jupyter-react", - "version": "0.18.9", + "version": "0.18.10", "description": "Jupyter React - React.js components 100% compatible with Jupyter.", "license": "MIT", "main": "lib/index.js", From f1266fcf7059608bedbcb1054bb39c8afcdb3d3c Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sat, 26 Oct 2024 16:00:13 +0200 Subject: [PATCH 02/12] chore: notebook extension --- .../src/components/notebook/Notebook.tsx | 21 +- .../components/notebook/NotebookAdapter.ts | 58 +++-- .../src/components/notebook/cell/index.ts | 1 + .../notebook/cell/sidebar/CellSidebar.tsx | 4 +- .../cell/sidebar/CellSidebarButton.tsx | 4 +- .../notebook/cell/sidebar/CellSidebarRun.tsx | 4 +- .../cell/sidebar/CellSidebarWidget.tsx | 4 +- .../notebook/cell/toolbar/CellToolbar.tsx | 209 ++++++++++++++++++ .../cell/toolbar/CellToolbarButton.tsx | 188 ++++++++++++++++ .../notebook/cell/toolbar/CellToolbarRun.tsx | 62 ++++++ .../cell/toolbar/CellToolbarWidget.tsx | 57 +++++ .../components/notebook/cell/toolbar/index.ts | 10 + .../content/JupyterReactContentFactory.ts | 85 +++++-- .../extensions/exec-time/ExecTime.css | 29 +++ .../extensions/exec-time/ExecTimeExtension.ts | 21 ++ .../extensions/exec-time/ExecuteTimeWidget.ts | 175 +++++++++++++++ .../notebook/extensions/exec-time/index.ts | 9 + .../notebook/extensions/exec-time/utils.ts | 55 +++++ .../components/notebook/extensions/index.ts | 7 + .../model/JupyterReactNotebookModelFactory.ts | 6 +- .../src/examples/NotebookCellSidebar.tsx | 2 +- .../src/examples/NotebookCellToolbar.tsx | 33 +++ .../notebooks/NotebookExample1.ipynb.json | 34 ++- .../examples/sidebars/CellSidebarSource.tsx | 4 +- packages/react/webpack.config.js | 4 +- 25 files changed, 1025 insertions(+), 61 deletions(-) create mode 100644 packages/react/src/components/notebook/cell/toolbar/CellToolbar.tsx create mode 100644 packages/react/src/components/notebook/cell/toolbar/CellToolbarButton.tsx create mode 100644 packages/react/src/components/notebook/cell/toolbar/CellToolbarRun.tsx create mode 100644 packages/react/src/components/notebook/cell/toolbar/CellToolbarWidget.tsx create mode 100644 packages/react/src/components/notebook/cell/toolbar/index.ts create mode 100644 packages/react/src/components/notebook/extensions/exec-time/ExecTime.css create mode 100644 packages/react/src/components/notebook/extensions/exec-time/ExecTimeExtension.ts create mode 100644 packages/react/src/components/notebook/extensions/exec-time/ExecuteTimeWidget.ts create mode 100644 packages/react/src/components/notebook/extensions/exec-time/index.ts create mode 100644 packages/react/src/components/notebook/extensions/exec-time/utils.ts create mode 100644 packages/react/src/components/notebook/extensions/index.ts create mode 100644 packages/react/src/examples/NotebookCellToolbar.tsx diff --git a/packages/react/src/components/notebook/Notebook.tsx b/packages/react/src/components/notebook/Notebook.tsx index a083404a..967e23df 100644 --- a/packages/react/src/components/notebook/Notebook.tsx +++ b/packages/react/src/components/notebook/Notebook.tsx @@ -7,6 +7,7 @@ import { useState, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { Box } from '@primer/react'; +import { Widget } from '@lumino/widgets'; import { Cell, ICellModel } from '@jupyterlab/cells'; import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; import { INotebookContent } from '@jupyterlab/nbformat'; @@ -16,6 +17,7 @@ import { asObservable, Lumino } from '../lumino'; import { CellMetadataEditor } from './cell/metadata/CellMetadataEditor'; import { ICellSidebarProps } from './cell/sidebar/CellSidebarWidget'; import { INotebookToolbarProps } from './toolbar/NotebookToolbar'; +import { ExecuteTimeWidgetExtension } from './extensions'; import { newUuid } from '../../utils'; import { OnKernelConnection } from '../../state'; import { useNotebookStore } from './NotebookState'; @@ -34,6 +36,7 @@ export type BundledIPyWidgets = ExternalIPyWidgets & { export type INotebookProps = { CellSidebar?: (props: ICellSidebarProps) => JSX.Element; + CellToolbar?: (props: ICellSidebarProps) => JSX.Element; Toolbar?: (props: INotebookToolbarProps) => JSX.Element; cellMetadataPanel: boolean; cellSidebarMargin: number; @@ -90,6 +93,7 @@ export const Notebook = (props: INotebookProps) => { } = props; const [id, _] = useState(props.id || newUuid()); const [adapter, setAdapter] = useState(); + const [extension, setExtension] = useState(); const kernel = props.kernel ?? defaultKernel; const notebookStore = useNotebookStore(); const portals = notebookStore.selectNotebookPortals(id); @@ -104,6 +108,9 @@ export const Notebook = (props: INotebookProps) => { }); // Update the local state. setAdapter(adapter); + const execTimeExtension = new ExecuteTimeWidgetExtension(); + const e = execTimeExtension.createNew(adapter.notebookPanel!, adapter.context!); + setExtension(e); // Update the global state. notebookStore.update({ id, state: { adapter } }); // Update the global state based on events. @@ -271,7 +278,12 @@ export const Notebook = (props: INotebookProps) => { left: `${props.cellSidebarMargin + 10}px`, height: 'auto', }, - '& .jp-Cell .dla-CellHeader-Container': { + '& .jp-Cell .dla-CellSidebar-Container': { + padding: '4px 8px', + width: `${props.cellSidebarMargin + 10}px`, + marginLeft: 'auto', + }, + '& .jp-Cell .dla-CellToolbar-Container': { padding: '4px 8px', width: `${props.cellSidebarMargin + 10}px`, marginLeft: 'auto', @@ -288,11 +300,16 @@ export const Notebook = (props: INotebookProps) => { {portals?.map((portal: React.ReactPortal) => portal)} - {adapter && + {adapter && {adapter.panel} } + {extension && + + {extension} + + } diff --git a/packages/react/src/components/notebook/NotebookAdapter.ts b/packages/react/src/components/notebook/NotebookAdapter.ts index b5b6f10a..9834385a 100755 --- a/packages/react/src/components/notebook/NotebookAdapter.ts +++ b/packages/react/src/components/notebook/NotebookAdapter.ts @@ -17,7 +17,7 @@ import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; import { ISharedAttachmentsCell, IYText } from '@jupyter/ydoc'; import { INotebookContent, CellType, IAttachments } from '@jupyterlab/nbformat'; import { Completer, CompleterModel, CompletionHandler, ProviderReconciliator, KernelCompleterProvider } from '@jupyterlab/completer'; -import { Notebook, NotebookPanel, NotebookWidgetFactory, NotebookTracker, INotebookModel } from '@jupyterlab/notebook'; +import { Notebook, NotebookPanel, NotebookWidgetFactory, NotebookTracker, INotebookModel, StaticNotebook } from '@jupyterlab/notebook'; import { Cell, ICellModel, MarkdownCell } from '@jupyterlab/cells'; import { rendererFactory as jsonRendererFactory } from '@jupyterlab/json-extension'; import { rendererFactory as javascriptRendererFactory } from '@jupyterlab/javascript-extension'; @@ -58,6 +58,8 @@ export class NotebookAdapter { private _serviceManager: ServiceManager.IManager; private _tracker?: NotebookTracker; private _url?: string; + private _contentFactory: JupyterReactContentFactory; + private _mimeTypeService: CodeMirrorMimeTypeService; constructor(props: INotebookProps) { @@ -250,15 +252,16 @@ export class NotebookAdapter { }); if (!this._notebookPanel) { + // Option 1. /* - // Alternative way to create a NotebookPanel. const content = new Notebook({ rendermime: this._rendermime!, - contentFactory, - mimeTypeService, + contentFactory: this._contentFactory, + mimeTypeService: this._mimeTypeService, notebookConfig: { ...StaticNotebook.defaultNotebookConfig, - windowingMode: 'none' + windowingMode: 'none', + recordTiming: true, } }); this._notebookPanel = new NotebookPanel({ @@ -266,7 +269,10 @@ export class NotebookAdapter { content, }); */ - this._notebookPanel = this._documentRegistry?.getWidgetFactory('Notebook')?.createNew(this._context) as NotebookPanel; + // Option 2. + const notebookFactory = this._documentRegistry?.getWidgetFactory('Notebook'); + this._notebookPanel = notebookFactory?.createNew(this._context) as NotebookPanel; + } if (this._kernel) { @@ -416,7 +422,7 @@ export class NotebookAdapter { }); this._documentRegistry = new DocumentRegistry({}); - const mimeTypeService = new CodeMirrorMimeTypeService(languages); + this._mimeTypeService = new CodeMirrorMimeTypeService(languages); const themes = new EditorThemeRegistry(); for (const theme of EditorThemeRegistry.getDefaultThemes()) { @@ -424,9 +430,7 @@ export class NotebookAdapter { } const editorExtensions = () => { const registry = new EditorExtensionRegistry(); - for (const extensionFactory of EditorExtensionRegistry.getDefaultExtensions( - { themes } - )) { + for (const extensionFactory of EditorExtensionRegistry.getDefaultExtensions({ themes })) { registry.addExtension(extensionFactory); } registry.addExtension({ @@ -449,25 +453,23 @@ export class NotebookAdapter { }); const editorServices: IEditorServices = { factoryService, - mimeTypeService, + mimeTypeService: this._mimeTypeService, }; const editorFactory = editorServices.factoryService.newInlineEditor; - const contentFactory = this._CellSidebar ? - new JupyterReactContentFactory( - this._CellSidebar, - this._id, - this._nbgrader, - this._commandRegistry, - { editorFactory }, - ) - : - new NotebookPanel.ContentFactory({ editorFactory }); + this._contentFactory = new JupyterReactContentFactory( + this._id, + this._nbgrader, + this._commandRegistry, + { editorFactory }, + this._CellSidebar, + ); this._tracker = new NotebookTracker({ namespace: this._id }); const notebookWidgetFactory = new NotebookWidgetFactory({ name: 'Notebook', - modelName: 'viewer', + label: 'Notebook', + modelName: 'notebook', fileTypes: ['notebook'], defaultFor: ['notebook'], preferKernel: true, @@ -475,8 +477,12 @@ export class NotebookAdapter { canStartKernel: false, shutdownOnClose: false, rendermime: this._rendermime, - contentFactory, + contentFactory: this._contentFactory, mimeTypeService: editorServices.mimeTypeService, + notebookConfig: { + ...StaticNotebook.defaultNotebookConfig, + recordTiming: true, + } }); notebookWidgetFactory.widgetCreated.connect((sender, notebookPanel) => { notebookPanel.context.pathChanged.connect(() => { @@ -492,7 +498,7 @@ export class NotebookAdapter { }); this._documentRegistry.addModelFactory(this._notebookModelFactory); - this.initializeContext(); + this.initializeContext(); } @@ -532,6 +538,10 @@ export class NotebookAdapter { return this._notebookPanel; } + get context(): Context | undefined { + return this._context; + } + set notebookPanel(notebookPanel: NotebookPanel | undefined) { this._notebookPanel = notebookPanel; } diff --git a/packages/react/src/components/notebook/cell/index.ts b/packages/react/src/components/notebook/cell/index.ts index 28ec2032..7e259edb 100644 --- a/packages/react/src/components/notebook/cell/index.ts +++ b/packages/react/src/components/notebook/cell/index.ts @@ -7,3 +7,4 @@ export * from './metadata'; export * from './prompt'; export * from './sidebar'; +export * from './toolbar'; diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx index f734cc58..8c55bbea 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx @@ -18,7 +18,7 @@ import { ICellSidebarProps } from './CellSidebarWidget'; import CellMetadataEditor from '../metadata/CellMetadataEditor'; import useNotebookStore from '../../NotebookState'; -import { DATALAYER_CELL_HEADER_CLASS } from './CellSidebarWidget'; +import { DATALAYER_CELL_SIDEBAR_CLASS_NAME } from './CellSidebarWidget'; export const CellSidebar = (props: ICellSidebarProps) => { const { notebookId, cellId, nbgrader } = props; @@ -44,7 +44,7 @@ export const CellSidebar = (props: ICellSidebarProps) => { } return activeCell ? ( { const { notebookId, cellId } = props; @@ -43,7 +43,7 @@ export const CellSidebarButton = (props: ICellSidebarProps) => { } return activeCell ? ( { const { notebookId } = props; @@ -33,7 +33,7 @@ export const CellSidebarRun = (props: ICellSidebarProps) => { } return activeCell ? ( {sidebar} +
{sidebar}
); const portal = createPortal(portalDiv, this.node); notebookStore.getState().addPortals({ diff --git a/packages/react/src/components/notebook/cell/toolbar/CellToolbar.tsx b/packages/react/src/components/notebook/cell/toolbar/CellToolbar.tsx new file mode 100644 index 00000000..361915f0 --- /dev/null +++ b/packages/react/src/components/notebook/cell/toolbar/CellToolbar.tsx @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +import { useState } from 'react'; +import { PanelLayout } from '@lumino/widgets'; +import { ActionMenu, Button, Box } from '@primer/react'; +import { + ChevronRightIcon, + XIcon, + ChevronUpIcon, + ChevronDownIcon, + SquareIcon, +} from '@primer/octicons-react'; +import { ICellToolbarProps } from './CellToolbarWidget'; +import CellMetadataEditor from '../metadata/CellMetadataEditor'; +import useNotebookStore from '../../NotebookState'; + +import { DATALAYER_CELL_TOOLBAR_CLASS_NAME } from './CellToolbarWidget'; + +export const CellToolbar = (props: ICellToolbarProps) => { + const { notebookId, cellId, nbgrader } = props; + const [visible, setVisible] = useState(false); + const notebookStore = useNotebookStore(); + const activeCell = notebookStore.selectActiveCell(notebookId); + const layout = activeCell?.layout; + if (layout) { + const cellWidget = (layout as PanelLayout).widgets[0]; + if (cellWidget?.node.id === cellId) { + if (!visible) { + setVisible(true); + } + } + if (cellWidget?.node.id !== cellId) { + if (visible) { + setVisible(false); + } + } + } + if (!visible) { + return
; + } + return activeCell ? ( + + {nbgrader && ( + + {/* + + + + + */} + + {/* + + */} + + )} + + + + + + + + + + + {activeCell.model.type === 'code' ? ( + + ) : ( + + )} + + + + + + + + + + + + ) : ( + <> + ); +}; + +export default CellToolbar; diff --git a/packages/react/src/components/notebook/cell/toolbar/CellToolbarButton.tsx b/packages/react/src/components/notebook/cell/toolbar/CellToolbarButton.tsx new file mode 100644 index 00000000..ab04f644 --- /dev/null +++ b/packages/react/src/components/notebook/cell/toolbar/CellToolbarButton.tsx @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +import { useState } from 'react'; +import { PanelLayout } from '@lumino/widgets'; +import { Box, IconButton } from '@primer/react'; +import { + PlayIcon, + ChevronUpIcon, + ChevronDownIcon, + SquareIcon, + XIcon, +} from '@primer/octicons-react'; +import { ICellToolbarProps } from './CellToolbarWidget'; +import useNotebookStore from '../../NotebookState'; + +import { DATALAYER_CELL_TOOLBAR_CLASS_NAME } from './CellToolbarWidget'; + +export const CellToolbarButton = (props: ICellToolbarProps) => { + const { notebookId, cellId } = props; + const notebookStore = useNotebookStore(); + const [visible, setVisible] = useState(false); + const activeCell = notebookStore.selectActiveCell(notebookId); + const layout = activeCell?.layout; + if (layout) { + const cellWidget = (layout as PanelLayout).widgets[0]; + if (cellWidget?.node.id === cellId) { + if (!visible) { + setVisible(true); + } + } + if (cellWidget?.node.id !== cellId) { + if (visible) { + setVisible(false); + } + } + } + if (!visible) { + return
; + } + return activeCell ? ( + + + { + e.preventDefault(); + notebookStore.run(notebookId); + }} + icon={PlayIcon} + variant="invisible" + /> + + + { + e.preventDefault(); + notebookStore.insertAbove({ + id: notebookId, + cellType: 'code', + }); + }} + icon={ChevronUpIcon} + variant="invisible" + /> + + + { + e.preventDefault(); + notebookStore.insertAbove({ + id: notebookId, + cellType: 'markdown', + }); + }} + icon={ChevronUpIcon} + variant="invisible" + /> + + + {activeCell.model.type === 'code' ? ( + { + e.preventDefault(); + notebookStore.changeCellType({ + id: notebookId, + cellType: 'markdown', + }); + }} + /> + ) : ( + { + e.preventDefault(); + notebookStore.changeCellType({ + id: notebookId, + cellType: 'code', + }); + }} + /> + )} + + + { + e.preventDefault(); + notebookStore.insertBelow({ + id: notebookId, + cellType: 'markdown', + }); + }} + icon={ChevronDownIcon} + variant="invisible" + /> + + + { + e.preventDefault(); + notebookStore.insertBelow({ + id: notebookId, + cellType: 'code', + }); + }} + icon={ChevronDownIcon} + variant="invisible" + /> + + + { + e.preventDefault(); + notebookStore.delete(notebookId); + }} + icon={XIcon} + variant="invisible" + /> + + + ) : ( + <> + ); +}; + +export default CellToolbarButton; diff --git a/packages/react/src/components/notebook/cell/toolbar/CellToolbarRun.tsx b/packages/react/src/components/notebook/cell/toolbar/CellToolbarRun.tsx new file mode 100644 index 00000000..cc25fb70 --- /dev/null +++ b/packages/react/src/components/notebook/cell/toolbar/CellToolbarRun.tsx @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +import { useState } from 'react'; +import { PanelLayout } from '@lumino/widgets'; +import { Box, Button } from '@primer/react'; +import { PlayIcon } from '@primer/octicons-react'; +import { ICellToolbarProps } from './CellToolbarWidget'; +import useNotebookStore from '../../NotebookState'; + +import { DATALAYER_CELL_TOOLBAR_CLASS_NAME } from './CellToolbarWidget'; + +export const CellToolbarRun = (props: ICellToolbarProps) => { + const { notebookId } = props; + const notebookStore = useNotebookStore(); + const [visible, setVisible] = useState(false); + const activeCell = notebookStore.selectActiveCell(notebookId); + const layout = activeCell?.layout; + if (layout) { + const cellWidget = (layout as PanelLayout).widgets[0]; + if (!visible && cellWidget?.node.id === props.cellId) { + setVisible(true); + } + if (visible && cellWidget?.node.id !== props.cellId) { + setVisible(false); + } + } + if (!visible) { + return
; + } + return activeCell ? ( + + + + + + ) : ( + <> + ); +}; + +export default CellToolbarRun; diff --git a/packages/react/src/components/notebook/cell/toolbar/CellToolbarWidget.tsx b/packages/react/src/components/notebook/cell/toolbar/CellToolbarWidget.tsx new file mode 100644 index 00000000..a8139a53 --- /dev/null +++ b/packages/react/src/components/notebook/cell/toolbar/CellToolbarWidget.tsx @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +import { createElement } from 'react'; +import { createPortal } from 'react-dom'; +import { ICellHeader } from '@jupyterlab/cells'; +import { CommandRegistry } from '@lumino/commands'; +import { newUuid } from '../../../../utils/Utils'; +import { ReactPortalWidget } from '../../../lumino/ReactPortalWidget'; +import { notebookStore } from '../../NotebookState'; + +export const DATALAYER_CELL_TOOLBAR_CLASS_NAME = 'dla-CellToolbar-Container'; + +export type ICellToolbarProps = { + notebookId: string; + cellId: string; + command: CommandRegistry; + nbgrader: boolean; +}; + +export class CellToolbarWidget + extends ReactPortalWidget + implements ICellHeader +{ + private readonly commands: CommandRegistry; + constructor( + CellToolbar: (props: ICellToolbarProps) => JSX.Element, + notebookId: string, + nbgrader: boolean, + commands: CommandRegistry, + ) { + super(); + this.commands = commands; + this.addClass('jp-CellHeader'); + this.id = newUuid(); + const props: ICellToolbarProps = { + notebookId: notebookId, + cellId: this.id, + command: this.commands, + nbgrader, + }; + const toolbar = createElement(CellToolbar, props); + const portalDiv = ( +
{toolbar}
+ ); + const portal = createPortal(portalDiv, this.node); + notebookStore.getState().addPortals({ + id: notebookId, + portals: [portal], + }); + } +} + +export default CellToolbarWidget; diff --git a/packages/react/src/components/notebook/cell/toolbar/index.ts b/packages/react/src/components/notebook/cell/toolbar/index.ts new file mode 100644 index 00000000..4970b5ff --- /dev/null +++ b/packages/react/src/components/notebook/cell/toolbar/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +export * from './CellToolbar'; +export * from './CellToolbarButton'; +export * from './CellToolbarRun'; +export * from './CellToolbarWidget'; diff --git a/packages/react/src/components/notebook/content/JupyterReactContentFactory.ts b/packages/react/src/components/notebook/content/JupyterReactContentFactory.ts index 34e03a02..d1e11c37 100644 --- a/packages/react/src/components/notebook/content/JupyterReactContentFactory.ts +++ b/packages/react/src/components/notebook/content/JupyterReactContentFactory.ts @@ -5,49 +5,102 @@ */ import { CommandRegistry } from '@lumino/commands'; -import { NotebookPanel } from '@jupyterlab/notebook'; +import { Notebook, NotebookPanel } from '@jupyterlab/notebook'; import { ICellHeader, Cell } from '@jupyterlab/cells'; import { CellSidebarWidget, ICellSidebarProps } from '../cell/sidebar/CellSidebarWidget'; -// import { IInputPrompt } from '@jupyterlab/cells'; +import { CodeCell } from '@jupyterlab/cells'; +import { CodeEditor } from '@jupyterlab/codeeditor'; +// import { CodeMirrorEditor } from '@jupyterlab/codemirror'; +import { IInputPrompt } from '@jupyterlab/cells'; +import { ExecuteTimeWidget } from './../extensions'; // import { NotebookInputPrompt } from './../cell/InputPrompt'; +class DatalayerCell extends CodeCell { + constructor(options: CodeCell.IOptions) { + super(options); + } +} + /** * Extend the default implementation NotebookPanel.ContentFactory of `IContentFactory`. */ export class JupyterReactContentFactory extends NotebookPanel.ContentFactory { - private readonly CellSidebar: (props: ICellSidebarProps) => JSX.Element; + private readonly CellSidebar?: (props: ICellSidebarProps) => JSX.Element; private readonly notebookId: string; private readonly nbgrader: boolean; private readonly commands: CommandRegistry; constructor( - CellSidebar: (props: ICellSidebarProps) => JSX.Element, notebookId: string, nbgrader: boolean, commands: CommandRegistry, options: Cell.ContentFactory.IOptions, + CellSidebar?: (props: ICellSidebarProps) => JSX.Element, ) { super(options); this.CellSidebar = CellSidebar; - super.createCodeCell; - (this.notebookId = notebookId), (this.nbgrader = nbgrader); + this.notebookId = notebookId; + this.nbgrader = nbgrader; this.commands = commands; } + private _updateSQLEditor = (cell: CodeCell) => { + const datalayer = cell.model?.getMetadata('datalayer'); + if (datalayer && datalayer['sql']) { + (cell.editor!.model as CodeEditor.Model).mimeType = 'application/sql'; + } + } + + private _updateEditor = (cell: CodeCell) => { + if (cell.editor) { + cell.editor.model.mimeTypeChanged.connect((_, args) => { + this._updateSQLEditor(cell); + }); + this._updateSQLEditor(cell); + } + } + + /** @override */ + createNotebook(options: Notebook.IOptions): Notebook { + const notebook = super.createNotebook(options); + return notebook; + } + + /** @override */ createCellHeader(): ICellHeader { - return new CellSidebarWidget( - this.CellSidebar, - this.notebookId, - this.nbgrader, - this.commands, - ); - } - /* + if (this.CellSidebar) { + return new CellSidebarWidget( + this.CellSidebar, + this.notebookId, + this.nbgrader, + this.commands, + ); + } + return super.createCellHeader(); + } + + /** @override */ + createCodeCell(options: CodeCell.IOptions): CodeCell { + const cell = new DatalayerCell(options); + if (cell.inViewport) { + this._updateEditor(cell); + } + cell.displayChanged.connect(() => { + this._updateEditor(cell); + }); + cell.inViewportChanged.connect(() => { + this._updateEditor(cell); + }); + return cell; + } + + /** @override */ createInputPrompt(): IInputPrompt { - return new InputPrompt(); +// return new InputPrompt(); + return super.createInputPrompt(); } - */ + } export default JupyterReactContentFactory; diff --git a/packages/react/src/components/notebook/extensions/exec-time/ExecTime.css b/packages/react/src/components/notebook/extensions/exec-time/ExecTime.css new file mode 100644 index 00000000..d9dc21aa --- /dev/null +++ b/packages/react/src/components/notebook/extensions/exec-time/ExecTime.css @@ -0,0 +1,29 @@ +/* Transition to highlight a cell change */ +@keyframes executeHighlight { + from { + background-color: #9fccff; + } + + to { + background-color: var(--jp-cell-editor-background); + } +} + +.execute-time { + background-color: var(--jp-cell-editor-background); + color: var(--jp-mirror-editor-variable-color); + display: block; + margin-top: 2px; + font-family: monospace; + font-size: 80%; + border-top: 1px solid #cfcfcf; + padding: 0 2px; +} + +.execute-time-positioning-left { + text-align: left; +} + +.execute-time-positioning-right { + text-align: right; +} diff --git a/packages/react/src/components/notebook/extensions/exec-time/ExecTimeExtension.ts b/packages/react/src/components/notebook/extensions/exec-time/ExecTimeExtension.ts new file mode 100644 index 00000000..25780862 --- /dev/null +++ b/packages/react/src/components/notebook/extensions/exec-time/ExecTimeExtension.ts @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +import { INotebookModel, NotebookPanel } from '@jupyterlab/notebook'; +import { DocumentRegistry } from '@jupyterlab/docregistry'; +import { ExecuteTimeWidget } from './ExecuteTimeWidget'; + +import './ExecTime.css'; + +export class ExecuteTimeWidgetExtension implements DocumentRegistry.WidgetExtension { + constructor() { + } + createNew(notebookPanel: NotebookPanel, context: DocumentRegistry.IContext) { + return new ExecuteTimeWidget(notebookPanel); + } +} + +export default ExecuteTimeWidgetExtension; diff --git a/packages/react/src/components/notebook/extensions/exec-time/ExecuteTimeWidget.ts b/packages/react/src/components/notebook/extensions/exec-time/ExecuteTimeWidget.ts new file mode 100644 index 00000000..a61bbbc1 --- /dev/null +++ b/packages/react/src/components/notebook/extensions/exec-time/ExecuteTimeWidget.ts @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +import { Widget } from '@lumino/widgets'; +import { JSONExt, JSONObject } from '@lumino/coreutils'; +import { NotebookPanel } from '@jupyterlab/notebook'; +import { IObservableList } from '@jupyterlab/observables'; +import { Cell, CodeCell, ICellModel } from '@jupyterlab/cells'; +import { IMapChange } from '@jupyter/ydoc'; +import { getTimeDiff, getTimeString } from './utils'; + +const EXECUTE_TIME_CLASS = 'execute-time'; + +const ANIMATE_TIME_MS = 1000; + +const ANIMATE_CSS = `executeHighlight ${ANIMATE_TIME_MS}ms`; + +export interface IExecuteTimeSettings { + highlight: boolean; + positioning: string; +} + +export class ExecuteTimeWidget extends Widget { + + private _panel: NotebookPanel; + + private _cellSlotMap: { + [id: string]: ( + sender: ICellModel, + args: IMapChange + ) => void; + } = {}; + + private _settings: IExecuteTimeSettings = { + highlight: true, + positioning: 'left' + }; + + constructor(panel: NotebookPanel) { + super(); + this._panel = panel; + this.updateConnectedCell = this.updateConnectedCell.bind(this); + const cells = this._panel.context.model.cells; + cells.changed.connect(this.updateConnectedCell); + for (let i = 0; i < cells.length; ++i) { + this._registerMetadataChanges(cells.get(i)); + } + } + + private updateConnectedCell(cells: any, changed: IObservableList.IChangedArgs) { + changed.oldValues.forEach(this._deregisterMetadataChanges.bind(this)); + changed.newValues.forEach(this._registerMetadataChanges.bind(this)); + } + + private _registerMetadataChanges(cellModel: ICellModel) { + if (!(cellModel.id in this._cellSlotMap)) { + const fn = () => this._cellMetadataChanged(cellModel); + this._cellSlotMap[cellModel.id] = fn; + cellModel.metadataChanged.connect(fn); + } + this._cellMetadataChanged(cellModel, true); + } + + private _deregisterMetadataChanges(cellModel: ICellModel) { + const fn = this._cellSlotMap[cellModel.id]; + if (fn) { + cellModel.metadataChanged.disconnect(fn); + const codeCell = this._getCodeCell(cellModel); + if (codeCell) { + this._removeExecuteNode(codeCell); + } + } + delete this._cellSlotMap[cellModel.id]; + } + + private _cellMetadataChanged(cellModel: ICellModel, disableHighlight = false) { + const codeCell = this._getCodeCell(cellModel); + if (codeCell) { + this._updateCodeCell(codeCell, disableHighlight); + } else { + if (cellModel.type === 'code') { + console.error(`Could not find code cell for model: ${cellModel}`); + } + } + } + + private _getCodeCell(cellModel: ICellModel): CodeCell | null { + if (cellModel.type === 'code') { + const cell = this._panel.content.widgets.find( + (widget: Cell) => widget.model === cellModel + ); + return cell as CodeCell; + } + return null; + } + + private _removeExecuteNode(cell: CodeCell) { + if (cell.inputArea) { + const editorWidget = cell.inputArea.editorWidget; + const executionTimeNode = editorWidget.node.querySelector( + `.${EXECUTE_TIME_CLASS}` + ); + if (executionTimeNode) { + executionTimeNode.remove(); + } + } + } + + private _updateCodeCell(cell: CodeCell, disableHighlight: boolean) { + const executionMetadata = cell.model.getMetadata('execution') as JSONObject; + if (executionMetadata && JSONExt.isObject(executionMetadata)) { + const editorWidget = cell.inputArea!.editorWidget; + let executionTimeNode: HTMLDivElement | null = editorWidget.node.querySelector( + `.${EXECUTE_TIME_CLASS}` + ); + if (!executionTimeNode) { + executionTimeNode = document.createElement('div') as HTMLDivElement; + editorWidget.node.append(executionTimeNode); + } + let positioning; + switch (this._settings.positioning) { + case 'left': + positioning = 'left'; + break; + case 'right': + positioning = 'right'; + break; + default: + console.error( + `'${positioning}' is not a valid type for the setting 'positioning'` + ); + } + const positioningClass = `${EXECUTE_TIME_CLASS}-positioning-${this._settings.positioning}`; + executionTimeNode.className = `${EXECUTE_TIME_CLASS} ${positioningClass}`; + const queuedTimeStr = executionMetadata['iopub.status.busy'] as + | string + | null; + const queuedTime = queuedTimeStr ? new Date(queuedTimeStr) : null; + const startTimeStr = (executionMetadata['shell.execute_reply.started'] || + executionMetadata['iopub.execute_input']) as string | null; + const startTime = startTimeStr ? new Date(startTimeStr) : null; + const endTimeStr = executionMetadata['shell.execute_reply'] as + | string + | null; + const endTime = endTimeStr ? new Date(endTimeStr) : null; + let msg = ''; + if (endTime) { + msg = `Last executed at ${getTimeString(endTime)} in ${getTimeDiff( + endTime, + startTime! + )}`; + } else if (startTime) { + msg = `Execution started at ${getTimeString(startTime)}`; + } else if (queuedTime) { + msg = `Execution queued at ${getTimeString(queuedTime)}`; + } + if (executionTimeNode.innerText !== msg) { + executionTimeNode.innerText = msg; + if (!disableHighlight && this._settings.highlight && endTimeStr) { + executionTimeNode.style.setProperty('animation', ANIMATE_CSS); + setTimeout( + () => executionTimeNode!.style.removeProperty('animation'), + ANIMATE_TIME_MS + ); + } + } + } else { + this._removeExecuteNode(cell); + } + } + +} diff --git a/packages/react/src/components/notebook/extensions/exec-time/index.ts b/packages/react/src/components/notebook/extensions/exec-time/index.ts new file mode 100644 index 00000000..3146c331 --- /dev/null +++ b/packages/react/src/components/notebook/extensions/exec-time/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +export * from './ExecTimeExtension'; +export * from './ExecuteTimeWidget'; +export * from './utils'; diff --git a/packages/react/src/components/notebook/extensions/exec-time/utils.ts b/packages/react/src/components/notebook/extensions/exec-time/utils.ts new file mode 100644 index 00000000..b07bb34a --- /dev/null +++ b/packages/react/src/components/notebook/extensions/exec-time/utils.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +export const getTimeString = (date: Date): string => { + return date.toLocaleDateString( + 'en-us', + { weekday:"long", year:"numeric", month:"short", day:"numeric"}, + ); +} + +export const getTimeDiff = (end: Date, start: Date): string => { + + const MS_IN_SEC = 1000; + const MS_IN_MIN = 60 * MS_IN_SEC; + const MS_IN_HR = 60 * MS_IN_MIN; + const MS_IN_DAY = 24 * MS_IN_HR; + + let ms = end.getTime() - start.getTime(); + if (ms < MS_IN_SEC) { + return `${ms}ms`; + } + const days = Math.floor(ms / MS_IN_DAY); + ms = ms % MS_IN_DAY; + + const hours = Math.floor(ms / MS_IN_HR); + ms = ms % MS_IN_HR; + + const mins = Math.floor(ms / MS_IN_MIN); + ms = ms % MS_IN_MIN; + + // We want to show this as fractional + const secs = ms / MS_IN_SEC; + + let timeDiff = ''; + if (days) { + timeDiff += `${days}d `; + } + if (days || hours) { + timeDiff += `${hours}h `; + } + if (days || hours || mins) { + timeDiff += `${mins}m `; + } + // Only show s if its < 1 day + if (!days) { + // Only show ms if is < 1 hr + timeDiff += `${secs.toFixed(hours ? 0 : 2)}s`; + } + + return timeDiff.trim(); + +} diff --git a/packages/react/src/components/notebook/extensions/index.ts b/packages/react/src/components/notebook/extensions/index.ts new file mode 100644 index 00000000..3f2b1e2d --- /dev/null +++ b/packages/react/src/components/notebook/extensions/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +export * from './exec-time'; diff --git a/packages/react/src/components/notebook/model/JupyterReactNotebookModelFactory.ts b/packages/react/src/components/notebook/model/JupyterReactNotebookModelFactory.ts index 4cd443bd..b55b689a 100644 --- a/packages/react/src/components/notebook/model/JupyterReactNotebookModelFactory.ts +++ b/packages/react/src/components/notebook/model/JupyterReactNotebookModelFactory.ts @@ -5,11 +5,7 @@ */ import { INotebookContent } from '@jupyterlab/nbformat'; -import { - INotebookModel, - NotebookModelFactory, - NotebookModel, -} from '@jupyterlab/notebook'; +import { INotebookModel, NotebookModelFactory, NotebookModel } from '@jupyterlab/notebook'; import { DocumentRegistry } from '@jupyterlab/docregistry'; import type { ISharedNotebook } from '@jupyter/ydoc'; diff --git a/packages/react/src/examples/NotebookCellSidebar.tsx b/packages/react/src/examples/NotebookCellSidebar.tsx index 76bdc3a2..8fd7e86d 100644 --- a/packages/react/src/examples/NotebookCellSidebar.tsx +++ b/packages/react/src/examples/NotebookCellSidebar.tsx @@ -17,7 +17,7 @@ const NotebookCellSidebar = () => ( ( + + + +); + +const div = document.createElement('div'); +document.body.appendChild(div); +const root = createRoot(div); + +root.render(); diff --git a/packages/react/src/examples/notebooks/NotebookExample1.ipynb.json b/packages/react/src/examples/notebooks/NotebookExample1.ipynb.json index 85c1f877..c43d80ba 100644 --- a/packages/react/src/examples/notebooks/NotebookExample1.ipynb.json +++ b/packages/react/src/examples/notebooks/NotebookExample1.ipynb.json @@ -19,7 +19,34 @@ { "cell_type": "code", "execution_count": 1, - "metadata": {}, + "metadata": { + "datalayer": { + "sql": true + } + }, + "outputs": [ + { + "data": { + "text/plain": [""] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "-- I am a SQL cell.\n", + "SELECT * FROM LOGS" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "datalayer": { + "_about": "Accelerated and Trusted Jupyter" + } + }, "outputs": [ { "data": { @@ -30,7 +57,10 @@ "output_type": "execute_result" } ], - "source": ["2+2"] + "source": [ + "# I am a Python cell.\n", + "print('2+2')" + ] }, { "cell_type": "code", diff --git a/packages/react/src/examples/sidebars/CellSidebarSource.tsx b/packages/react/src/examples/sidebars/CellSidebarSource.tsx index 660fd26a..4df576e7 100644 --- a/packages/react/src/examples/sidebars/CellSidebarSource.tsx +++ b/packages/react/src/examples/sidebars/CellSidebarSource.tsx @@ -18,7 +18,7 @@ import { ICellSidebarProps } from '../../components/notebook/cell/sidebar/CellSi import CellMetadataEditor from '../../components/notebook/cell/metadata/CellMetadataEditor'; import useNotebookStore from '../../components/notebook/NotebookState'; -import { DATALAYER_CELL_HEADER_CLASS } from '../../components/notebook/cell/sidebar/CellSidebarWidget'; +import { DATALAYER_CELL_SIDEBAR_CLASS_NAME } from '../../components/notebook/cell/sidebar/CellSidebarWidget'; export const CellSidebarSource = (props: ICellSidebarProps) => { const { notebookId, cellId, nbgrader } = props; @@ -44,7 +44,7 @@ export const CellSidebarSource = (props: ICellSidebarProps) => { } return activeCell ? ( Date: Sat, 26 Oct 2024 17:33:16 +0200 Subject: [PATCH 03/12] chore: extension examples --- .../src/components/notebook/Notebook.tsx | 37 ++-- .../src/components/notebook/cell/index.ts | 1 - .../notebook/cell/toolbar/CellToolbar.tsx | 209 ------------------ .../cell/toolbar/CellToolbarButton.tsx | 188 ---------------- .../notebook/cell/toolbar/CellToolbarRun.tsx | 62 ------ .../cell/toolbar/CellToolbarWidget.tsx | 57 ----- .../content/JupyterReactContentFactory.ts | 5 +- .../extensions/exec-time/ExecTimeExtension.ts | 21 -- .../src/examples/NotebookCellToolbar.tsx | 30 +-- .../react/src/examples/NotebookExtension.tsx | 37 ++++ .../celltoolbar/CellToolbarExtension.css | 29 +++ .../celltoolbar/CellToolbarExtension.tsx | 29 +++ .../celltoolbar/CellToolbarWidget.ts | 174 +++++++++++++++ .../extensions/celltoolbar}/index.ts | 4 +- .../exectime/ExecTimeExtension.css} | 6 +- .../extensions/exectime/ExecTimeExtension.tsx | 29 +++ .../extensions/exectime/ExecTimeWidget.ts} | 9 +- .../extensions/exectime}/index.ts | 2 +- .../extensions/exectime}/utils.ts | 8 - .../notebook => examples}/extensions/index.ts | 3 +- packages/react/webpack.config.js | 1 + 21 files changed, 352 insertions(+), 589 deletions(-) delete mode 100644 packages/react/src/components/notebook/cell/toolbar/CellToolbar.tsx delete mode 100644 packages/react/src/components/notebook/cell/toolbar/CellToolbarButton.tsx delete mode 100644 packages/react/src/components/notebook/cell/toolbar/CellToolbarRun.tsx delete mode 100644 packages/react/src/components/notebook/cell/toolbar/CellToolbarWidget.tsx delete mode 100644 packages/react/src/components/notebook/extensions/exec-time/ExecTimeExtension.ts create mode 100644 packages/react/src/examples/NotebookExtension.tsx create mode 100644 packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.css create mode 100644 packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx create mode 100644 packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.ts rename packages/react/src/{components/notebook/cell/toolbar => examples/extensions/celltoolbar}/index.ts (50%) rename packages/react/src/{components/notebook/extensions/exec-time/ExecTime.css => examples/extensions/exectime/ExecTimeExtension.css} (85%) create mode 100644 packages/react/src/examples/extensions/exectime/ExecTimeExtension.tsx rename packages/react/src/{components/notebook/extensions/exec-time/ExecuteTimeWidget.ts => examples/extensions/exectime/ExecTimeWidget.ts} (96%) rename packages/react/src/{components/notebook/extensions/exec-time => examples/extensions/exectime}/index.ts (77%) rename packages/react/src/{components/notebook/extensions/exec-time => examples/extensions/exectime}/utils.ts (99%) rename packages/react/src/{components/notebook => examples}/extensions/index.ts (53%) diff --git a/packages/react/src/components/notebook/Notebook.tsx b/packages/react/src/components/notebook/Notebook.tsx index 967e23df..e0bfcaf7 100644 --- a/packages/react/src/components/notebook/Notebook.tsx +++ b/packages/react/src/components/notebook/Notebook.tsx @@ -7,8 +7,9 @@ import { useState, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { Box } from '@primer/react'; -import { Widget } from '@lumino/widgets'; +import { NotebookPanel, INotebookModel } from '@jupyterlab/notebook'; import { Cell, ICellModel } from '@jupyterlab/cells'; +import { DocumentRegistry } from '@jupyterlab/docregistry'; import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; import { INotebookContent } from '@jupyterlab/nbformat'; import { ServiceManager } from '@jupyterlab/services'; @@ -17,7 +18,6 @@ import { asObservable, Lumino } from '../lumino'; import { CellMetadataEditor } from './cell/metadata/CellMetadataEditor'; import { ICellSidebarProps } from './cell/sidebar/CellSidebarWidget'; import { INotebookToolbarProps } from './toolbar/NotebookToolbar'; -import { ExecuteTimeWidgetExtension } from './extensions'; import { newUuid } from '../../utils'; import { OnKernelConnection } from '../../state'; import { useNotebookStore } from './NotebookState'; @@ -28,18 +28,22 @@ import './Notebook.css'; export type ExternalIPyWidgets = { name: string; version: string; -}; +} export type BundledIPyWidgets = ExternalIPyWidgets & { module: any; -}; +} + +export type DatalayerNotebookExtension = DocumentRegistry.IWidgetExtension & { + get component(): JSX.Element | undefined; +} export type INotebookProps = { CellSidebar?: (props: ICellSidebarProps) => JSX.Element; - CellToolbar?: (props: ICellSidebarProps) => JSX.Element; Toolbar?: (props: INotebookToolbarProps) => JSX.Element; cellMetadataPanel: boolean; cellSidebarMargin: number; + extensions: DatalayerNotebookExtension[] height?: string; id: string; lite?: Lite; @@ -82,6 +86,7 @@ export const Notebook = (props: INotebookProps) => { }); const { Toolbar, + extensions, height, maxHeight, nbformat, @@ -93,7 +98,7 @@ export const Notebook = (props: INotebookProps) => { } = props; const [id, _] = useState(props.id || newUuid()); const [adapter, setAdapter] = useState(); - const [extension, setExtension] = useState(); + const [extensionComponents, setExtensionWidgets] = useState(new Array()); const kernel = props.kernel ?? defaultKernel; const notebookStore = useNotebookStore(); const portals = notebookStore.selectNotebookPortals(id); @@ -108,9 +113,10 @@ export const Notebook = (props: INotebookProps) => { }); // Update the local state. setAdapter(adapter); - const execTimeExtension = new ExecuteTimeWidgetExtension(); - const e = execTimeExtension.createNew(adapter.notebookPanel!, adapter.context!); - setExtension(e); + extensions.forEach(extension => { + extension.createNew(adapter.notebookPanel!, adapter.context!); + setExtensionWidgets(extensionComponents.concat(extension.component ?? <>)); + }); // Update the global state. notebookStore.update({ id, state: { adapter } }); // Update the global state based on events. @@ -305,11 +311,13 @@ export const Notebook = (props: INotebookProps) => { {adapter.panel} } - {extension && - - {extension} - - } + {extensionComponents.map((extensionComponent, index) => { + return ( + + {extensionComponent} + + )} + )}
@@ -319,6 +327,7 @@ export const Notebook = (props: INotebookProps) => { Notebook.defaultProps = { cellMetadataPanel: false, cellSidebarMargin: 120, + extensions: [], height: '100vh', maxHeight: '100vh', nbgrader: false, diff --git a/packages/react/src/components/notebook/cell/index.ts b/packages/react/src/components/notebook/cell/index.ts index 7e259edb..28ec2032 100644 --- a/packages/react/src/components/notebook/cell/index.ts +++ b/packages/react/src/components/notebook/cell/index.ts @@ -7,4 +7,3 @@ export * from './metadata'; export * from './prompt'; export * from './sidebar'; -export * from './toolbar'; diff --git a/packages/react/src/components/notebook/cell/toolbar/CellToolbar.tsx b/packages/react/src/components/notebook/cell/toolbar/CellToolbar.tsx deleted file mode 100644 index 361915f0..00000000 --- a/packages/react/src/components/notebook/cell/toolbar/CellToolbar.tsx +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (c) 2021-2023 Datalayer, Inc. - * - * MIT License - */ - -import { useState } from 'react'; -import { PanelLayout } from '@lumino/widgets'; -import { ActionMenu, Button, Box } from '@primer/react'; -import { - ChevronRightIcon, - XIcon, - ChevronUpIcon, - ChevronDownIcon, - SquareIcon, -} from '@primer/octicons-react'; -import { ICellToolbarProps } from './CellToolbarWidget'; -import CellMetadataEditor from '../metadata/CellMetadataEditor'; -import useNotebookStore from '../../NotebookState'; - -import { DATALAYER_CELL_TOOLBAR_CLASS_NAME } from './CellToolbarWidget'; - -export const CellToolbar = (props: ICellToolbarProps) => { - const { notebookId, cellId, nbgrader } = props; - const [visible, setVisible] = useState(false); - const notebookStore = useNotebookStore(); - const activeCell = notebookStore.selectActiveCell(notebookId); - const layout = activeCell?.layout; - if (layout) { - const cellWidget = (layout as PanelLayout).widgets[0]; - if (cellWidget?.node.id === cellId) { - if (!visible) { - setVisible(true); - } - } - if (cellWidget?.node.id !== cellId) { - if (visible) { - setVisible(false); - } - } - } - if (!visible) { - return
; - } - return activeCell ? ( - - {nbgrader && ( - - {/* - - - - - */} - - {/* - - */} - - )} - - - - - - - - - - - {activeCell.model.type === 'code' ? ( - - ) : ( - - )} - - - - - - - - - - - - ) : ( - <> - ); -}; - -export default CellToolbar; diff --git a/packages/react/src/components/notebook/cell/toolbar/CellToolbarButton.tsx b/packages/react/src/components/notebook/cell/toolbar/CellToolbarButton.tsx deleted file mode 100644 index ab04f644..00000000 --- a/packages/react/src/components/notebook/cell/toolbar/CellToolbarButton.tsx +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (c) 2021-2023 Datalayer, Inc. - * - * MIT License - */ - -import { useState } from 'react'; -import { PanelLayout } from '@lumino/widgets'; -import { Box, IconButton } from '@primer/react'; -import { - PlayIcon, - ChevronUpIcon, - ChevronDownIcon, - SquareIcon, - XIcon, -} from '@primer/octicons-react'; -import { ICellToolbarProps } from './CellToolbarWidget'; -import useNotebookStore from '../../NotebookState'; - -import { DATALAYER_CELL_TOOLBAR_CLASS_NAME } from './CellToolbarWidget'; - -export const CellToolbarButton = (props: ICellToolbarProps) => { - const { notebookId, cellId } = props; - const notebookStore = useNotebookStore(); - const [visible, setVisible] = useState(false); - const activeCell = notebookStore.selectActiveCell(notebookId); - const layout = activeCell?.layout; - if (layout) { - const cellWidget = (layout as PanelLayout).widgets[0]; - if (cellWidget?.node.id === cellId) { - if (!visible) { - setVisible(true); - } - } - if (cellWidget?.node.id !== cellId) { - if (visible) { - setVisible(false); - } - } - } - if (!visible) { - return
; - } - return activeCell ? ( - - - { - e.preventDefault(); - notebookStore.run(notebookId); - }} - icon={PlayIcon} - variant="invisible" - /> - - - { - e.preventDefault(); - notebookStore.insertAbove({ - id: notebookId, - cellType: 'code', - }); - }} - icon={ChevronUpIcon} - variant="invisible" - /> - - - { - e.preventDefault(); - notebookStore.insertAbove({ - id: notebookId, - cellType: 'markdown', - }); - }} - icon={ChevronUpIcon} - variant="invisible" - /> - - - {activeCell.model.type === 'code' ? ( - { - e.preventDefault(); - notebookStore.changeCellType({ - id: notebookId, - cellType: 'markdown', - }); - }} - /> - ) : ( - { - e.preventDefault(); - notebookStore.changeCellType({ - id: notebookId, - cellType: 'code', - }); - }} - /> - )} - - - { - e.preventDefault(); - notebookStore.insertBelow({ - id: notebookId, - cellType: 'markdown', - }); - }} - icon={ChevronDownIcon} - variant="invisible" - /> - - - { - e.preventDefault(); - notebookStore.insertBelow({ - id: notebookId, - cellType: 'code', - }); - }} - icon={ChevronDownIcon} - variant="invisible" - /> - - - { - e.preventDefault(); - notebookStore.delete(notebookId); - }} - icon={XIcon} - variant="invisible" - /> - - - ) : ( - <> - ); -}; - -export default CellToolbarButton; diff --git a/packages/react/src/components/notebook/cell/toolbar/CellToolbarRun.tsx b/packages/react/src/components/notebook/cell/toolbar/CellToolbarRun.tsx deleted file mode 100644 index cc25fb70..00000000 --- a/packages/react/src/components/notebook/cell/toolbar/CellToolbarRun.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2021-2023 Datalayer, Inc. - * - * MIT License - */ - -import { useState } from 'react'; -import { PanelLayout } from '@lumino/widgets'; -import { Box, Button } from '@primer/react'; -import { PlayIcon } from '@primer/octicons-react'; -import { ICellToolbarProps } from './CellToolbarWidget'; -import useNotebookStore from '../../NotebookState'; - -import { DATALAYER_CELL_TOOLBAR_CLASS_NAME } from './CellToolbarWidget'; - -export const CellToolbarRun = (props: ICellToolbarProps) => { - const { notebookId } = props; - const notebookStore = useNotebookStore(); - const [visible, setVisible] = useState(false); - const activeCell = notebookStore.selectActiveCell(notebookId); - const layout = activeCell?.layout; - if (layout) { - const cellWidget = (layout as PanelLayout).widgets[0]; - if (!visible && cellWidget?.node.id === props.cellId) { - setVisible(true); - } - if (visible && cellWidget?.node.id !== props.cellId) { - setVisible(false); - } - } - if (!visible) { - return
; - } - return activeCell ? ( - - - - - - ) : ( - <> - ); -}; - -export default CellToolbarRun; diff --git a/packages/react/src/components/notebook/cell/toolbar/CellToolbarWidget.tsx b/packages/react/src/components/notebook/cell/toolbar/CellToolbarWidget.tsx deleted file mode 100644 index a8139a53..00000000 --- a/packages/react/src/components/notebook/cell/toolbar/CellToolbarWidget.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2021-2023 Datalayer, Inc. - * - * MIT License - */ - -import { createElement } from 'react'; -import { createPortal } from 'react-dom'; -import { ICellHeader } from '@jupyterlab/cells'; -import { CommandRegistry } from '@lumino/commands'; -import { newUuid } from '../../../../utils/Utils'; -import { ReactPortalWidget } from '../../../lumino/ReactPortalWidget'; -import { notebookStore } from '../../NotebookState'; - -export const DATALAYER_CELL_TOOLBAR_CLASS_NAME = 'dla-CellToolbar-Container'; - -export type ICellToolbarProps = { - notebookId: string; - cellId: string; - command: CommandRegistry; - nbgrader: boolean; -}; - -export class CellToolbarWidget - extends ReactPortalWidget - implements ICellHeader -{ - private readonly commands: CommandRegistry; - constructor( - CellToolbar: (props: ICellToolbarProps) => JSX.Element, - notebookId: string, - nbgrader: boolean, - commands: CommandRegistry, - ) { - super(); - this.commands = commands; - this.addClass('jp-CellHeader'); - this.id = newUuid(); - const props: ICellToolbarProps = { - notebookId: notebookId, - cellId: this.id, - command: this.commands, - nbgrader, - }; - const toolbar = createElement(CellToolbar, props); - const portalDiv = ( -
{toolbar}
- ); - const portal = createPortal(portalDiv, this.node); - notebookStore.getState().addPortals({ - id: notebookId, - portals: [portal], - }); - } -} - -export default CellToolbarWidget; diff --git a/packages/react/src/components/notebook/content/JupyterReactContentFactory.ts b/packages/react/src/components/notebook/content/JupyterReactContentFactory.ts index d1e11c37..bdf9e6c7 100644 --- a/packages/react/src/components/notebook/content/JupyterReactContentFactory.ts +++ b/packages/react/src/components/notebook/content/JupyterReactContentFactory.ts @@ -12,10 +12,9 @@ import { CodeCell } from '@jupyterlab/cells'; import { CodeEditor } from '@jupyterlab/codeeditor'; // import { CodeMirrorEditor } from '@jupyterlab/codemirror'; import { IInputPrompt } from '@jupyterlab/cells'; -import { ExecuteTimeWidget } from './../extensions'; // import { NotebookInputPrompt } from './../cell/InputPrompt'; -class DatalayerCell extends CodeCell { +class DatalayerCodeCell extends CodeCell { constructor(options: CodeCell.IOptions) { super(options); } @@ -82,7 +81,7 @@ export class JupyterReactContentFactory extends NotebookPanel.ContentFactory { /** @override */ createCodeCell(options: CodeCell.IOptions): CodeCell { - const cell = new DatalayerCell(options); + const cell = new DatalayerCodeCell(options); if (cell.inViewport) { this._updateEditor(cell); } diff --git a/packages/react/src/components/notebook/extensions/exec-time/ExecTimeExtension.ts b/packages/react/src/components/notebook/extensions/exec-time/ExecTimeExtension.ts deleted file mode 100644 index 25780862..00000000 --- a/packages/react/src/components/notebook/extensions/exec-time/ExecTimeExtension.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2021-2023 Datalayer, Inc. - * - * MIT License - */ - -import { INotebookModel, NotebookPanel } from '@jupyterlab/notebook'; -import { DocumentRegistry } from '@jupyterlab/docregistry'; -import { ExecuteTimeWidget } from './ExecuteTimeWidget'; - -import './ExecTime.css'; - -export class ExecuteTimeWidgetExtension implements DocumentRegistry.WidgetExtension { - constructor() { - } - createNew(notebookPanel: NotebookPanel, context: DocumentRegistry.IContext) { - return new ExecuteTimeWidget(notebookPanel); - } -} - -export default ExecuteTimeWidgetExtension; diff --git a/packages/react/src/examples/NotebookCellToolbar.tsx b/packages/react/src/examples/NotebookCellToolbar.tsx index 38910154..7786f02e 100644 --- a/packages/react/src/examples/NotebookCellToolbar.tsx +++ b/packages/react/src/examples/NotebookCellToolbar.tsx @@ -4,27 +4,31 @@ * MIT License */ +import { useState } from 'react'; import { createRoot } from 'react-dom/client'; import { INotebookContent } from '@jupyterlab/nbformat'; import { JupyterReactTheme } from '../theme/JupyterReactTheme'; import { Notebook } from '../components/notebook/Notebook'; +import { ExecTimeExtension } from './extensions'; import { NotebookToolbar } from './../components/notebook/toolbar/NotebookToolbar'; -import CellToolbarButton from './../components/notebook/cell/toolbar/CellToolbarButton'; import nbformat from './notebooks/NotebookExample1.ipynb.json'; -const NotebookCellToolbar = () => ( - - - -); +const NotebookCellToolbar = () => { + const [extension, _] = useState(new ExecTimeExtension()); + return ( + + + + ) +} const div = document.createElement('div'); document.body.appendChild(div); diff --git a/packages/react/src/examples/NotebookExtension.tsx b/packages/react/src/examples/NotebookExtension.tsx new file mode 100644 index 00000000..2c672e87 --- /dev/null +++ b/packages/react/src/examples/NotebookExtension.tsx @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +import { useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import { INotebookContent } from '@jupyterlab/nbformat'; +import { JupyterReactTheme } from '../theme/JupyterReactTheme'; +import { Notebook } from '../components/notebook/Notebook'; +import { CellToolbarExtension } from './extensions'; +import { NotebookToolbar } from './../components/notebook/toolbar/NotebookToolbar'; + +import nbformat from './notebooks/NotebookExample1.ipynb.json'; + +const NotebookExtension = () => { + const [extension, _] = useState(new CellToolbarExtension()); + return ( + + + + ) +} + +const div = document.createElement('div'); +document.body.appendChild(div); +const root = createRoot(div); + +root.render(); diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.css b/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.css new file mode 100644 index 00000000..b2fd47b1 --- /dev/null +++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.css @@ -0,0 +1,29 @@ +/* Transition to highlight a cell change */ +@keyframes executeHighlight { + from { + background-color: #9fccff; + } + + to { + background-color: var(--jp-cell-editor-background); + } +} + +.dla-CellToolbar { + background-color: var(--jp-cell-editor-background); + color: var(--jp-mirror-editor-variable-color); + display: block; + margin-top: 2px; + font-family: monospace; + font-size: 80%; + border-top: 1px solid #cfcfcf; + padding: 0 2px; +} + +.dla-CellToolbar-positioning-left { + text-align: left; +} + +.dla-CellToolbar-positioning-right { + text-align: right; +} diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx new file mode 100644 index 00000000..72b363ce --- /dev/null +++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +import { INotebookModel, NotebookPanel } from '@jupyterlab/notebook'; +import { DocumentRegistry } from '@jupyterlab/docregistry'; +import { DatalayerNotebookExtension } from '../../../components'; +import { CellToolbarWidget } from './CellToolbarWidget'; + +import './CellToolbarExtension.css'; + +export class CellToolbarExtension implements DatalayerNotebookExtension { + constructor() {} + + /* @override */ + createNew(notebookPanel: NotebookPanel, context: DocumentRegistry.IContext) { + new CellToolbarWidget(notebookPanel); + } + + /* @override */ + get component(): JSX.Element | undefined { + return <>Hello + } + +} + +export default CellToolbarExtension; diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.ts b/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.ts new file mode 100644 index 00000000..ff8c5e02 --- /dev/null +++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.ts @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +import { Widget } from '@lumino/widgets'; +import { JSONExt, JSONObject } from '@lumino/coreutils'; +import { NotebookPanel } from '@jupyterlab/notebook'; +import { IObservableList } from '@jupyterlab/observables'; +import { Cell, CodeCell, ICellModel } from '@jupyterlab/cells'; +import { IMapChange } from '@jupyter/ydoc'; +import { getTimeDiff, getTimeString } from './../exectime/utils'; + +const EXECUTE_TIME_CLASS = 'dla-CellToolbar'; + +const ANIMATE_TIME_MS = 1000; + +const ANIMATE_CSS = `executeHighlight ${ANIMATE_TIME_MS}ms`; + +export interface ICellToolbarSettings { + highlight: boolean; + positioning: string; +} + +export class CellToolbarWidget extends Widget { + private _panel: NotebookPanel; + + private _cellSlotMap: { + [id: string]: ( + sender: ICellModel, + args: IMapChange + ) => void; + } = {}; + + private _settings: ICellToolbarSettings = { + highlight: true, + positioning: 'left' + }; + + constructor(panel: NotebookPanel) { + super(); + this._panel = panel; + this.updateConnectedCell = this.updateConnectedCell.bind(this); + const cells = this._panel.context.model.cells; + cells.changed.connect(this.updateConnectedCell); + for (let i = 0; i < cells.length; ++i) { + this._registerMetadataChanges(cells.get(i)); + } + } + + private updateConnectedCell(cells: any, changed: IObservableList.IChangedArgs) { + changed.oldValues.forEach(this._deregisterMetadataChanges.bind(this)); + changed.newValues.forEach(this._registerMetadataChanges.bind(this)); + } + + private _registerMetadataChanges(cellModel: ICellModel) { + if (!(cellModel.id in this._cellSlotMap)) { + const fn = () => this._cellMetadataChanged(cellModel); + this._cellSlotMap[cellModel.id] = fn; + cellModel.metadataChanged.connect(fn); + } + this._cellMetadataChanged(cellModel, true); + } + + private _deregisterMetadataChanges(cellModel: ICellModel) { + const fn = this._cellSlotMap[cellModel.id]; + if (fn) { + cellModel.metadataChanged.disconnect(fn); + const codeCell = this._getCodeCell(cellModel); + if (codeCell) { + this._removeExecuteNode(codeCell); + } + } + delete this._cellSlotMap[cellModel.id]; + } + + private _cellMetadataChanged(cellModel: ICellModel, disableHighlight = false) { + const codeCell = this._getCodeCell(cellModel); + if (codeCell) { + this._updateCodeCell(codeCell, disableHighlight); + } else { + if (cellModel.type === 'code') { + console.error(`Could not find code cell for model: ${cellModel}`); + } + } + } + + private _getCodeCell(cellModel: ICellModel): CodeCell | null { + if (cellModel.type === 'code') { + const cell = this._panel.content.widgets.find( + (widget: Cell) => widget.model === cellModel + ); + return cell as CodeCell; + } + return null; + } + + private _removeExecuteNode(cell: CodeCell) { + if (cell.inputArea) { + const editorWidget = cell.inputArea.editorWidget; + const executionTimeNode = editorWidget.node.querySelector( + `.${EXECUTE_TIME_CLASS}` + ); + if (executionTimeNode) { + executionTimeNode.remove(); + } + } + } + + private _updateCodeCell(cell: CodeCell, disableHighlight: boolean) { + const executionMetadata = cell.model.getMetadata('execution') as JSONObject; + if (executionMetadata && JSONExt.isObject(executionMetadata)) { + const editorWidget = cell.inputArea!.editorWidget; + let executionTimeNode: HTMLDivElement | null = editorWidget.node.querySelector( + `.${EXECUTE_TIME_CLASS}` + ); + if (!executionTimeNode) { + executionTimeNode = document.createElement('div') as HTMLDivElement; + editorWidget.node.append(executionTimeNode); + } + let positioning; + switch (this._settings.positioning) { + case 'left': + positioning = 'left'; + break; + case 'right': + positioning = 'right'; + break; + default: + console.error( + `'${positioning}' is not a valid type for the setting 'positioning'` + ); + } + const positioningClass = `${EXECUTE_TIME_CLASS}-positioning-${this._settings.positioning}`; + executionTimeNode.className = `${EXECUTE_TIME_CLASS} ${positioningClass}`; + const queuedTimeStr = executionMetadata['iopub.status.busy'] as + | string + | null; + const queuedTime = queuedTimeStr ? new Date(queuedTimeStr) : null; + const startTimeStr = (executionMetadata['shell.execute_reply.started'] || + executionMetadata['iopub.execute_input']) as string | null; + const startTime = startTimeStr ? new Date(startTimeStr) : null; + const endTimeStr = executionMetadata['shell.execute_reply'] as + | string + | null; + const endTime = endTimeStr ? new Date(endTimeStr) : null; + let msg = ''; + if (endTime) { + msg = `Last executed at ${getTimeString(endTime)} in ${getTimeDiff( + endTime, + startTime! + )}`; + } else if (startTime) { + msg = `Execution started at ${getTimeString(startTime)}`; + } else if (queuedTime) { + msg = `Execution queued at ${getTimeString(queuedTime)}`; + } + if (executionTimeNode.innerText !== msg) { + executionTimeNode.innerText = msg; + if (!disableHighlight && this._settings.highlight && endTimeStr) { + executionTimeNode.style.setProperty('animation', ANIMATE_CSS); + setTimeout( + () => executionTimeNode!.style.removeProperty('animation'), + ANIMATE_TIME_MS + ); + } + } + } else { + this._removeExecuteNode(cell); + } + } + +} diff --git a/packages/react/src/components/notebook/cell/toolbar/index.ts b/packages/react/src/examples/extensions/celltoolbar/index.ts similarity index 50% rename from packages/react/src/components/notebook/cell/toolbar/index.ts rename to packages/react/src/examples/extensions/celltoolbar/index.ts index 4970b5ff..8685d544 100644 --- a/packages/react/src/components/notebook/cell/toolbar/index.ts +++ b/packages/react/src/examples/extensions/celltoolbar/index.ts @@ -4,7 +4,5 @@ * MIT License */ -export * from './CellToolbar'; -export * from './CellToolbarButton'; -export * from './CellToolbarRun'; +export * from './CellToolbarExtension'; export * from './CellToolbarWidget'; diff --git a/packages/react/src/components/notebook/extensions/exec-time/ExecTime.css b/packages/react/src/examples/extensions/exectime/ExecTimeExtension.css similarity index 85% rename from packages/react/src/components/notebook/extensions/exec-time/ExecTime.css rename to packages/react/src/examples/extensions/exectime/ExecTimeExtension.css index d9dc21aa..8ebfc264 100644 --- a/packages/react/src/components/notebook/extensions/exec-time/ExecTime.css +++ b/packages/react/src/examples/extensions/exectime/ExecTimeExtension.css @@ -9,7 +9,7 @@ } } -.execute-time { +.dla-ExecTime { background-color: var(--jp-cell-editor-background); color: var(--jp-mirror-editor-variable-color); display: block; @@ -20,10 +20,10 @@ padding: 0 2px; } -.execute-time-positioning-left { +.dla-ExecTime-positioning-left { text-align: left; } -.execute-time-positioning-right { +.dla-ExecTime-positioning-right { text-align: right; } diff --git a/packages/react/src/examples/extensions/exectime/ExecTimeExtension.tsx b/packages/react/src/examples/extensions/exectime/ExecTimeExtension.tsx new file mode 100644 index 00000000..ebb36f23 --- /dev/null +++ b/packages/react/src/examples/extensions/exectime/ExecTimeExtension.tsx @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +import { INotebookModel, NotebookPanel } from '@jupyterlab/notebook'; +import { DocumentRegistry } from '@jupyterlab/docregistry'; +import { DatalayerNotebookExtension } from '../../../components'; +import { ExecTimeWidget } from './ExecTimeWidget'; + +import './ExecTimeExtension.css'; + +export class ExecTimeExtension implements DatalayerNotebookExtension { + constructor() {} + + /* @override */ + createNew(notebookPanel: NotebookPanel, context: DocumentRegistry.IContext) { + new ExecTimeWidget(notebookPanel); + } + + /* @override */ + get component(): JSX.Element | undefined { + return undefined + } + +} + +export default ExecTimeExtension; diff --git a/packages/react/src/components/notebook/extensions/exec-time/ExecuteTimeWidget.ts b/packages/react/src/examples/extensions/exectime/ExecTimeWidget.ts similarity index 96% rename from packages/react/src/components/notebook/extensions/exec-time/ExecuteTimeWidget.ts rename to packages/react/src/examples/extensions/exectime/ExecTimeWidget.ts index a61bbbc1..26d2c2d5 100644 --- a/packages/react/src/components/notebook/extensions/exec-time/ExecuteTimeWidget.ts +++ b/packages/react/src/examples/extensions/exectime/ExecTimeWidget.ts @@ -12,19 +12,18 @@ import { Cell, CodeCell, ICellModel } from '@jupyterlab/cells'; import { IMapChange } from '@jupyter/ydoc'; import { getTimeDiff, getTimeString } from './utils'; -const EXECUTE_TIME_CLASS = 'execute-time'; +const EXECUTE_TIME_CLASS = 'dla-ExecTime'; const ANIMATE_TIME_MS = 1000; const ANIMATE_CSS = `executeHighlight ${ANIMATE_TIME_MS}ms`; -export interface IExecuteTimeSettings { +export interface IExecTimeSettings { highlight: boolean; positioning: string; } -export class ExecuteTimeWidget extends Widget { - +export class ExecTimeWidget extends Widget { private _panel: NotebookPanel; private _cellSlotMap: { @@ -34,7 +33,7 @@ export class ExecuteTimeWidget extends Widget { ) => void; } = {}; - private _settings: IExecuteTimeSettings = { + private _settings: IExecTimeSettings = { highlight: true, positioning: 'left' }; diff --git a/packages/react/src/components/notebook/extensions/exec-time/index.ts b/packages/react/src/examples/extensions/exectime/index.ts similarity index 77% rename from packages/react/src/components/notebook/extensions/exec-time/index.ts rename to packages/react/src/examples/extensions/exectime/index.ts index 3146c331..1ab08e66 100644 --- a/packages/react/src/components/notebook/extensions/exec-time/index.ts +++ b/packages/react/src/examples/extensions/exectime/index.ts @@ -5,5 +5,5 @@ */ export * from './ExecTimeExtension'; -export * from './ExecuteTimeWidget'; +export * from './ExecTimeWidget'; export * from './utils'; diff --git a/packages/react/src/components/notebook/extensions/exec-time/utils.ts b/packages/react/src/examples/extensions/exectime/utils.ts similarity index 99% rename from packages/react/src/components/notebook/extensions/exec-time/utils.ts rename to packages/react/src/examples/extensions/exectime/utils.ts index b07bb34a..3a0238a6 100644 --- a/packages/react/src/components/notebook/extensions/exec-time/utils.ts +++ b/packages/react/src/examples/extensions/exectime/utils.ts @@ -12,28 +12,22 @@ export const getTimeString = (date: Date): string => { } export const getTimeDiff = (end: Date, start: Date): string => { - const MS_IN_SEC = 1000; const MS_IN_MIN = 60 * MS_IN_SEC; const MS_IN_HR = 60 * MS_IN_MIN; const MS_IN_DAY = 24 * MS_IN_HR; - let ms = end.getTime() - start.getTime(); if (ms < MS_IN_SEC) { return `${ms}ms`; } const days = Math.floor(ms / MS_IN_DAY); ms = ms % MS_IN_DAY; - const hours = Math.floor(ms / MS_IN_HR); ms = ms % MS_IN_HR; - const mins = Math.floor(ms / MS_IN_MIN); ms = ms % MS_IN_MIN; - // We want to show this as fractional const secs = ms / MS_IN_SEC; - let timeDiff = ''; if (days) { timeDiff += `${days}d `; @@ -49,7 +43,5 @@ export const getTimeDiff = (end: Date, start: Date): string => { // Only show ms if is < 1 hr timeDiff += `${secs.toFixed(hours ? 0 : 2)}s`; } - return timeDiff.trim(); - } diff --git a/packages/react/src/components/notebook/extensions/index.ts b/packages/react/src/examples/extensions/index.ts similarity index 53% rename from packages/react/src/components/notebook/extensions/index.ts rename to packages/react/src/examples/extensions/index.ts index 3f2b1e2d..6552e2d4 100644 --- a/packages/react/src/components/notebook/extensions/index.ts +++ b/packages/react/src/examples/extensions/index.ts @@ -4,4 +4,5 @@ * MIT License */ -export * from './exec-time'; +export * from './celltoolbar'; +export * from './exectime'; diff --git a/packages/react/webpack.config.js b/packages/react/webpack.config.js index dc433f0a..50d0f430 100644 --- a/packages/react/webpack.config.js +++ b/packages/react/webpack.config.js @@ -43,6 +43,7 @@ const ENTRY = // './src/examples/Lumino'; // './src/examples/Matplotlib'; // './src/examples/Notebook'; + // './src/examples/NotebookExtension'; // './src/examples/NotebookURL'; // './src/examples/NotebookCellSidebar'; './src/examples/NotebookCellToolbar'; From 3022e80ab926b8ac567f10ef1498c77d5b10caad Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sat, 26 Oct 2024 20:03:54 +0200 Subject: [PATCH 04/12] chore: cell toolbar --- .../src/components/notebook/Notebook.tsx | 18 +- .../cell/sidebar/CellSidebarButton.tsx | 2 +- .../content/JupyterReactContentFactory.ts | 5 +- .../src/examples/NotebookCellToolbar.tsx | 6 +- .../react/src/examples/NotebookExtension.tsx | 6 +- .../extensions/celltoolbar/CellToolbar.tsx | 194 ++++++++++++++++++ .../extensions/celltoolbar/CellToolbar2.tsx | 21 ++ .../celltoolbar/CellToolbarExtension.tsx | 8 +- .../celltoolbar/CellToolbarWidget.ts | 174 ---------------- .../celltoolbar/CellToolbarWidget.tsx | 80 ++++++++ .../examples/extensions/celltoolbar/index.ts | 2 +- .../extensions/exectime/ExecTimeWidget.ts | 13 +- 12 files changed, 327 insertions(+), 202 deletions(-) create mode 100644 packages/react/src/examples/extensions/celltoolbar/CellToolbar.tsx create mode 100644 packages/react/src/examples/extensions/celltoolbar/CellToolbar2.tsx delete mode 100644 packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.ts create mode 100644 packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx diff --git a/packages/react/src/components/notebook/Notebook.tsx b/packages/react/src/components/notebook/Notebook.tsx index e0bfcaf7..8e714c79 100644 --- a/packages/react/src/components/notebook/Notebook.tsx +++ b/packages/react/src/components/notebook/Notebook.tsx @@ -98,7 +98,7 @@ export const Notebook = (props: INotebookProps) => { } = props; const [id, _] = useState(props.id || newUuid()); const [adapter, setAdapter] = useState(); - const [extensionComponents, setExtensionWidgets] = useState(new Array()); + const [extensionComponents, setExtensionComponents] = useState(new Array()); const kernel = props.kernel ?? defaultKernel; const notebookStore = useNotebookStore(); const portals = notebookStore.selectNotebookPortals(id); @@ -115,7 +115,7 @@ export const Notebook = (props: INotebookProps) => { setAdapter(adapter); extensions.forEach(extension => { extension.createNew(adapter.notebookPanel!, adapter.context!); - setExtensionWidgets(extensionComponents.concat(extension.component ?? <>)); + setExtensionComponents(extensionComponents.concat(extension.component ?? <>)); }); // Update the global state. notebookStore.update({ id, state: { adapter } }); @@ -306,19 +306,21 @@ export const Notebook = (props: INotebookProps) => { {portals?.map((portal: React.ReactPortal) => portal)} - {adapter && - - {adapter.panel} - - } {extensionComponents.map((extensionComponent, index) => { return ( {extensionComponent} - + )} )}
+ + {adapter && + + {adapter.panel} + + } + ); diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx index 2710e704..274db05d 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx @@ -39,7 +39,7 @@ export const CellSidebarButton = (props: ICellSidebarProps) => { } } if (!visible) { - return
; + return <> } return activeCell ? ( { - const [extension, _] = useState(new ExecTimeExtension()); + const [extension, _] = useState(new CellToolbarExtension()); return ( { id="notebook-cell-toolbar-id" height="calc(100vh - 2.6rem)" // (Height - Toolbar Height). cellSidebarMargin={160} + CellSidebar={CellSidebarButton} Toolbar={NotebookToolbar} /> diff --git a/packages/react/src/examples/NotebookExtension.tsx b/packages/react/src/examples/NotebookExtension.tsx index 2c672e87..9611cfba 100644 --- a/packages/react/src/examples/NotebookExtension.tsx +++ b/packages/react/src/examples/NotebookExtension.tsx @@ -9,13 +9,14 @@ import { createRoot } from 'react-dom/client'; import { INotebookContent } from '@jupyterlab/nbformat'; import { JupyterReactTheme } from '../theme/JupyterReactTheme'; import { Notebook } from '../components/notebook/Notebook'; -import { CellToolbarExtension } from './extensions'; +import { ExecTimeExtension } from './extensions'; +import { CellSidebarButton } from '../components/notebook/cell/sidebar/CellSidebarButton'; import { NotebookToolbar } from './../components/notebook/toolbar/NotebookToolbar'; import nbformat from './notebooks/NotebookExample1.ipynb.json'; const NotebookExtension = () => { - const [extension, _] = useState(new CellToolbarExtension()); + const [extension, _] = useState(new ExecTimeExtension()); return ( { id="notebook-extension-id" height="calc(100vh - 2.6rem)" // (Height - Toolbar Height). cellSidebarMargin={160} + CellSidebar={CellSidebarButton} Toolbar={NotebookToolbar} /> diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbar.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbar.tsx new file mode 100644 index 00000000..8e34762e --- /dev/null +++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbar.tsx @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +import { useState } from 'react'; +import { CommandRegistry } from '@lumino/commands'; +import { PanelLayout } from '@lumino/widgets'; +import { Box, IconButton } from '@primer/react'; +import { + PlayIcon, + ChevronUpIcon, + ChevronDownIcon, + SquareIcon, + XIcon, +} from '@primer/octicons-react'; +import { useNotebookStore } from '../../../components'; + +export type ICellToolbarProps = { + notebookId: string; + cellId: string; + command: CommandRegistry; +}; + +const DATALAYER_CELL_SIDEBAR_CLASS_NAME = 'dla-CellSidebar-Container'; + +export const CellToolbar = (props: ICellToolbarProps) => { + const { notebookId, cellId } = props; + const notebookStore = useNotebookStore(); + const [visible, setVisible] = useState(false); + const activeCell = notebookStore.selectActiveCell(notebookId); + const layout = activeCell?.layout; + if (layout) { + const cellWidget = (layout as PanelLayout).widgets[0]; + if (cellWidget?.node.id === cellId) { + if (!visible) { + setVisible(true); + } + } + if (cellWidget?.node.id !== cellId) { + if (visible) { + setVisible(false); + } + } + } + if (!visible) { + return
; + } + return activeCell ? ( + + + { + e.preventDefault(); + notebookStore.run(notebookId); + }} + icon={PlayIcon} + variant="invisible" + /> + + + { + e.preventDefault(); + notebookStore.insertAbove({ + id: notebookId, + cellType: 'code', + }); + }} + icon={ChevronUpIcon} + variant="invisible" + /> + + + { + e.preventDefault(); + notebookStore.insertAbove({ + id: notebookId, + cellType: 'markdown', + }); + }} + icon={ChevronUpIcon} + variant="invisible" + /> + + + {activeCell.model.type === 'code' ? ( + { + e.preventDefault(); + notebookStore.changeCellType({ + id: notebookId, + cellType: 'markdown', + }); + }} + /> + ) : ( + { + e.preventDefault(); + notebookStore.changeCellType({ + id: notebookId, + cellType: 'code', + }); + }} + /> + )} + + + { + e.preventDefault(); + notebookStore.insertBelow({ + id: notebookId, + cellType: 'markdown', + }); + }} + icon={ChevronDownIcon} + variant="invisible" + /> + + + { + e.preventDefault(); + notebookStore.insertBelow({ + id: notebookId, + cellType: 'code', + }); + }} + icon={ChevronDownIcon} + variant="invisible" + /> + + + { + e.preventDefault(); + notebookStore.delete(notebookId); + }} + icon={XIcon} + variant="invisible" + /> + + + ) : ( + <> + ); +}; + +export default CellToolbar; diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbar2.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbar2.tsx new file mode 100644 index 00000000..4e9a4b36 --- /dev/null +++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbar2.tsx @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +import { ReactWidget } from '@jupyterlab/apputils'; +import { CodeCell } from '@jupyterlab/cells'; + +export class CellToolbar extends ReactWidget { + private _cell: CodeCell; + constructor(cell: CodeCell) { + super(); + this._cell = cell; + this.addClass('dla-Container'); + } + + render(): JSX.Element { + return <>id: {this._cell.model.id} + } +} diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx index 72b363ce..8ee449a2 100644 --- a/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx +++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx @@ -8,11 +8,15 @@ import { INotebookModel, NotebookPanel } from '@jupyterlab/notebook'; import { DocumentRegistry } from '@jupyterlab/docregistry'; import { DatalayerNotebookExtension } from '../../../components'; import { CellToolbarWidget } from './CellToolbarWidget'; +import { ICellToolbarProps } from './CellToolbar'; import './CellToolbarExtension.css'; export class CellToolbarExtension implements DatalayerNotebookExtension { - constructor() {} + private _props: ICellToolbarProps; + constructor(props: ICellToolbarProps) { + this._props= props; + } /* @override */ createNew(notebookPanel: NotebookPanel, context: DocumentRegistry.IContext) { @@ -21,7 +25,7 @@ export class CellToolbarExtension implements DatalayerNotebookExtension { /* @override */ get component(): JSX.Element | undefined { - return <>Hello + return <> } } diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.ts b/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.ts deleted file mode 100644 index ff8c5e02..00000000 --- a/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (c) 2021-2023 Datalayer, Inc. - * - * MIT License - */ - -import { Widget } from '@lumino/widgets'; -import { JSONExt, JSONObject } from '@lumino/coreutils'; -import { NotebookPanel } from '@jupyterlab/notebook'; -import { IObservableList } from '@jupyterlab/observables'; -import { Cell, CodeCell, ICellModel } from '@jupyterlab/cells'; -import { IMapChange } from '@jupyter/ydoc'; -import { getTimeDiff, getTimeString } from './../exectime/utils'; - -const EXECUTE_TIME_CLASS = 'dla-CellToolbar'; - -const ANIMATE_TIME_MS = 1000; - -const ANIMATE_CSS = `executeHighlight ${ANIMATE_TIME_MS}ms`; - -export interface ICellToolbarSettings { - highlight: boolean; - positioning: string; -} - -export class CellToolbarWidget extends Widget { - private _panel: NotebookPanel; - - private _cellSlotMap: { - [id: string]: ( - sender: ICellModel, - args: IMapChange - ) => void; - } = {}; - - private _settings: ICellToolbarSettings = { - highlight: true, - positioning: 'left' - }; - - constructor(panel: NotebookPanel) { - super(); - this._panel = panel; - this.updateConnectedCell = this.updateConnectedCell.bind(this); - const cells = this._panel.context.model.cells; - cells.changed.connect(this.updateConnectedCell); - for (let i = 0; i < cells.length; ++i) { - this._registerMetadataChanges(cells.get(i)); - } - } - - private updateConnectedCell(cells: any, changed: IObservableList.IChangedArgs) { - changed.oldValues.forEach(this._deregisterMetadataChanges.bind(this)); - changed.newValues.forEach(this._registerMetadataChanges.bind(this)); - } - - private _registerMetadataChanges(cellModel: ICellModel) { - if (!(cellModel.id in this._cellSlotMap)) { - const fn = () => this._cellMetadataChanged(cellModel); - this._cellSlotMap[cellModel.id] = fn; - cellModel.metadataChanged.connect(fn); - } - this._cellMetadataChanged(cellModel, true); - } - - private _deregisterMetadataChanges(cellModel: ICellModel) { - const fn = this._cellSlotMap[cellModel.id]; - if (fn) { - cellModel.metadataChanged.disconnect(fn); - const codeCell = this._getCodeCell(cellModel); - if (codeCell) { - this._removeExecuteNode(codeCell); - } - } - delete this._cellSlotMap[cellModel.id]; - } - - private _cellMetadataChanged(cellModel: ICellModel, disableHighlight = false) { - const codeCell = this._getCodeCell(cellModel); - if (codeCell) { - this._updateCodeCell(codeCell, disableHighlight); - } else { - if (cellModel.type === 'code') { - console.error(`Could not find code cell for model: ${cellModel}`); - } - } - } - - private _getCodeCell(cellModel: ICellModel): CodeCell | null { - if (cellModel.type === 'code') { - const cell = this._panel.content.widgets.find( - (widget: Cell) => widget.model === cellModel - ); - return cell as CodeCell; - } - return null; - } - - private _removeExecuteNode(cell: CodeCell) { - if (cell.inputArea) { - const editorWidget = cell.inputArea.editorWidget; - const executionTimeNode = editorWidget.node.querySelector( - `.${EXECUTE_TIME_CLASS}` - ); - if (executionTimeNode) { - executionTimeNode.remove(); - } - } - } - - private _updateCodeCell(cell: CodeCell, disableHighlight: boolean) { - const executionMetadata = cell.model.getMetadata('execution') as JSONObject; - if (executionMetadata && JSONExt.isObject(executionMetadata)) { - const editorWidget = cell.inputArea!.editorWidget; - let executionTimeNode: HTMLDivElement | null = editorWidget.node.querySelector( - `.${EXECUTE_TIME_CLASS}` - ); - if (!executionTimeNode) { - executionTimeNode = document.createElement('div') as HTMLDivElement; - editorWidget.node.append(executionTimeNode); - } - let positioning; - switch (this._settings.positioning) { - case 'left': - positioning = 'left'; - break; - case 'right': - positioning = 'right'; - break; - default: - console.error( - `'${positioning}' is not a valid type for the setting 'positioning'` - ); - } - const positioningClass = `${EXECUTE_TIME_CLASS}-positioning-${this._settings.positioning}`; - executionTimeNode.className = `${EXECUTE_TIME_CLASS} ${positioningClass}`; - const queuedTimeStr = executionMetadata['iopub.status.busy'] as - | string - | null; - const queuedTime = queuedTimeStr ? new Date(queuedTimeStr) : null; - const startTimeStr = (executionMetadata['shell.execute_reply.started'] || - executionMetadata['iopub.execute_input']) as string | null; - const startTime = startTimeStr ? new Date(startTimeStr) : null; - const endTimeStr = executionMetadata['shell.execute_reply'] as - | string - | null; - const endTime = endTimeStr ? new Date(endTimeStr) : null; - let msg = ''; - if (endTime) { - msg = `Last executed at ${getTimeString(endTime)} in ${getTimeDiff( - endTime, - startTime! - )}`; - } else if (startTime) { - msg = `Execution started at ${getTimeString(startTime)}`; - } else if (queuedTime) { - msg = `Execution queued at ${getTimeString(queuedTime)}`; - } - if (executionTimeNode.innerText !== msg) { - executionTimeNode.innerText = msg; - if (!disableHighlight && this._settings.highlight && endTimeStr) { - executionTimeNode.style.setProperty('animation', ANIMATE_CSS); - setTimeout( - () => executionTimeNode!.style.removeProperty('animation'), - ANIMATE_TIME_MS - ); - } - } - } else { - this._removeExecuteNode(cell); - } - } - -} diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx new file mode 100644 index 00000000..77e81f4e --- /dev/null +++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +// import { createPortal } from 'react-dom'; +import { Widget, PanelLayout } from '@lumino/widgets'; +import { NotebookPanel } from '@jupyterlab/notebook'; +import { IObservableList } from '@jupyterlab/observables'; +import { Cell, CodeCell, ICellModel } from '@jupyterlab/cells'; +import { CellToolbar } from './CellToolbar2'; + +const CELL_TOOLBAR_CLASS = 'dla-CellToolbar'; + +export interface ICellToolbarSettings { + highlight: boolean; + positioning: string; +} + +export class CellToolbarWidget extends Widget { + private _panel: NotebookPanel; + + constructor(panel: NotebookPanel) { + super(); + this._panel = panel; + this.updateConnectedCell = this.updateConnectedCell.bind(this); + const cells = this._panel.context.model.cells; + cells.changed.connect(this.updateConnectedCell); + for (let i = 0; i < cells.length; ++i) { + this._registerCellChanges(cells.get(i)); + } + } + + private updateConnectedCell(cells: any, changed: IObservableList.IChangedArgs) { + changed.oldValues.forEach(this._deregisterCellChanges.bind(this)); + changed.newValues.forEach(this._registerCellChanges.bind(this)); + } + + private _deregisterCellChanges(cellModel: ICellModel) { + } + + private _registerCellChanges(cellModel: ICellModel, disableHighlight = false) { + const codeCell = this._getCodeCell(cellModel); + if (codeCell) { + codeCell.displayChanged.connect(() => { + this._updateCodeCell(codeCell, disableHighlight); + }); + codeCell.inViewportChanged.connect(() => { + this._updateCodeCell(codeCell, disableHighlight); + }); + this._updateCodeCell(codeCell, disableHighlight); + } else { + if (cellModel.type === 'code') { + console.error(`Could not find code cell for model: ${cellModel}`); + } + } + } + + private _getCodeCell(cellModel: ICellModel): CodeCell | null { + if (cellModel.type === 'code') { + const cell = this._panel.content.widgets.find( + (widget: Cell) => widget.model === cellModel + ); + return cell as CodeCell; + } + return null; + } + + private _updateCodeCell(cell: CodeCell, disableHighlight: boolean) { + if (cell.inputArea) { + const layout = cell.layout; + if (layout) { + const panelLayout = layout as PanelLayout; + panelLayout.insertWidget(0, new CellToolbar(cell)); + } + } + } + +} diff --git a/packages/react/src/examples/extensions/celltoolbar/index.ts b/packages/react/src/examples/extensions/celltoolbar/index.ts index 8685d544..91b139ee 100644 --- a/packages/react/src/examples/extensions/celltoolbar/index.ts +++ b/packages/react/src/examples/extensions/celltoolbar/index.ts @@ -4,5 +4,5 @@ * MIT License */ +export * from './CellToolbar'; export * from './CellToolbarExtension'; -export * from './CellToolbarWidget'; diff --git a/packages/react/src/examples/extensions/exectime/ExecTimeWidget.ts b/packages/react/src/examples/extensions/exectime/ExecTimeWidget.ts index 26d2c2d5..8b16a2f4 100644 --- a/packages/react/src/examples/extensions/exectime/ExecTimeWidget.ts +++ b/packages/react/src/examples/extensions/exectime/ExecTimeWidget.ts @@ -27,10 +27,7 @@ export class ExecTimeWidget extends Widget { private _panel: NotebookPanel; private _cellSlotMap: { - [id: string]: ( - sender: ICellModel, - args: IMapChange - ) => void; + [id: string]: (sender: ICellModel, args: IMapChange) => void; } = {}; private _settings: IExecTimeSettings = { @@ -99,9 +96,7 @@ export class ExecTimeWidget extends Widget { private _removeExecuteNode(cell: CodeCell) { if (cell.inputArea) { const editorWidget = cell.inputArea.editorWidget; - const executionTimeNode = editorWidget.node.querySelector( - `.${EXECUTE_TIME_CLASS}` - ); + const executionTimeNode = editorWidget.node.querySelector(`.${EXECUTE_TIME_CLASS}`); if (executionTimeNode) { executionTimeNode.remove(); } @@ -128,9 +123,7 @@ export class ExecTimeWidget extends Widget { positioning = 'right'; break; default: - console.error( - `'${positioning}' is not a valid type for the setting 'positioning'` - ); + console.error(`'${positioning}' is not a valid type for the setting 'positioning'`); } const positioningClass = `${EXECUTE_TIME_CLASS}-positioning-${this._settings.positioning}`; executionTimeNode.className = `${EXECUTE_TIME_CLASS} ${positioningClass}`; From b90901b3f73e63f1a9e4f94929f9f1a2c12977ca Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sun, 27 Oct 2024 05:20:30 +0100 Subject: [PATCH 05/12] chore: notebook extensions --- packages/react/package.json | 2 +- .../src/components/notebook/Notebook.tsx | 15 +- .../components/notebook/NotebookAdapter.ts | 2 +- .../notebook/cell/sidebar/CellSidebar.tsx | 9 +- .../cell/sidebar/CellSidebarButton.tsx | 2 +- .../cell/sidebar/CellSidebarWidget.tsx | 28 +-- .../content/JupyterReactContentFactory.ts | 2 +- .../extensions/celltoolbar/CellToolbar.tsx | 203 ++---------------- .../extensions/celltoolbar/CellToolbar2.tsx | 21 -- .../celltoolbar/CellToolbarComponent.tsx | 170 +++++++++++++++ .../celltoolbar/CellToolbarExtension.tsx | 13 +- .../celltoolbar/CellToolbarWidget.tsx | 11 +- .../examples/extensions/celltoolbar/index.ts | 2 + .../extensions/exectime/ExecTimeExtension.tsx | 7 +- packages/react/webpack.config.js | 6 +- 15 files changed, 249 insertions(+), 244 deletions(-) delete mode 100644 packages/react/src/examples/extensions/celltoolbar/CellToolbar2.tsx create mode 100644 packages/react/src/examples/extensions/celltoolbar/CellToolbarComponent.tsx diff --git a/packages/react/package.json b/packages/react/package.json index 6cf0f4b4..ac86b399 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@datalayer/jupyter-react", - "version": "0.18.10", + "version": "0.18.11", "description": "Jupyter React - React.js components 100% compatible with Jupyter.", "license": "MIT", "main": "lib/index.js", diff --git a/packages/react/src/components/notebook/Notebook.tsx b/packages/react/src/components/notebook/Notebook.tsx index 8e714c79..409914c4 100644 --- a/packages/react/src/components/notebook/Notebook.tsx +++ b/packages/react/src/components/notebook/Notebook.tsx @@ -9,14 +9,15 @@ import { createPortal } from 'react-dom'; import { Box } from '@primer/react'; import { NotebookPanel, INotebookModel } from '@jupyterlab/notebook'; import { Cell, ICellModel } from '@jupyterlab/cells'; +import { CommandRegistry } from '@lumino/commands'; import { DocumentRegistry } from '@jupyterlab/docregistry'; import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; import { INotebookContent } from '@jupyterlab/nbformat'; import { ServiceManager } from '@jupyterlab/services'; import { useJupyter, Lite, Kernel } from './../../jupyter'; import { asObservable, Lumino } from '../lumino'; -import { CellMetadataEditor } from './cell/metadata/CellMetadataEditor'; -import { ICellSidebarProps } from './cell/sidebar/CellSidebarWidget'; +import { CellMetadataEditor } from './cell/metadata'; +import { ICellSidebarProps } from './cell/sidebar'; import { INotebookToolbarProps } from './toolbar/NotebookToolbar'; import { newUuid } from '../../utils'; import { OnKernelConnection } from '../../state'; @@ -34,7 +35,13 @@ export type BundledIPyWidgets = ExternalIPyWidgets & { module: any; } +export type IDatalayerNotebookExtensionProps = { + notebookId: string; + commands: CommandRegistry; +}; + export type DatalayerNotebookExtension = DocumentRegistry.IWidgetExtension & { + init(props: IDatalayerNotebookExtensionProps): void; get component(): JSX.Element | undefined; } @@ -114,6 +121,10 @@ export const Notebook = (props: INotebookProps) => { // Update the local state. setAdapter(adapter); extensions.forEach(extension => { + extension.init({ + notebookId: id, + commands: adapter.commands, + }) extension.createNew(adapter.notebookPanel!, adapter.context!); setExtensionComponents(extensionComponents.concat(extension.component ?? <>)); }); diff --git a/packages/react/src/components/notebook/NotebookAdapter.ts b/packages/react/src/components/notebook/NotebookAdapter.ts index 9834385a..635f2fdf 100755 --- a/packages/react/src/components/notebook/NotebookAdapter.ts +++ b/packages/react/src/components/notebook/NotebookAdapter.ts @@ -25,7 +25,7 @@ import { MathJaxTypesetter } from '@jupyterlab/mathjax-extension'; import { WIDGET_MIMETYPE } from '@jupyter-widgets/html-manager/lib/output_renderers'; import { Lite, Kernel, WidgetManager, WidgetLabRenderer } from '../../jupyter'; import { OnKernelConnection } from '../../state'; -import { ICellSidebarProps } from './cell/sidebar/CellSidebarWidget'; +import { ICellSidebarProps } from './cell/sidebar'; import { JupyterReactContentFactory } from './content/JupyterReactContentFactory'; import { JupyterReactNotebookModelFactory } from './model/JupyterReactNotebookModelFactory'; import { getMarked } from './marked/marked'; diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx index 8c55bbea..d1d2e3ac 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx @@ -14,12 +14,19 @@ import { ChevronDownIcon, SquareIcon, } from '@primer/octicons-react'; -import { ICellSidebarProps } from './CellSidebarWidget'; +import { CommandRegistry } from '@lumino/commands'; import CellMetadataEditor from '../metadata/CellMetadataEditor'; import useNotebookStore from '../../NotebookState'; import { DATALAYER_CELL_SIDEBAR_CLASS_NAME } from './CellSidebarWidget'; +export type ICellSidebarProps = { + notebookId: string; + cellId: string; + command: CommandRegistry; + nbgrader: boolean; +} + export const CellSidebar = (props: ICellSidebarProps) => { const { notebookId, cellId, nbgrader } = props; const [visible, setVisible] = useState(false); diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx index 274db05d..d213d93e 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx @@ -14,7 +14,7 @@ import { SquareIcon, XIcon, } from '@primer/octicons-react'; -import { ICellSidebarProps } from './CellSidebarWidget'; +import { ICellSidebarProps } from './CellSidebar'; import useNotebookStore from '../../NotebookState'; import { DATALAYER_CELL_SIDEBAR_CLASS_NAME } from './CellSidebarWidget'; diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebarWidget.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebarWidget.tsx index 6889fe99..858ecf04 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebarWidget.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebarWidget.tsx @@ -10,22 +10,13 @@ import { ICellHeader } from '@jupyterlab/cells'; import { CommandRegistry } from '@lumino/commands'; import { newUuid } from '../../../../utils/Utils'; import { ReactPortalWidget } from '../../../lumino/ReactPortalWidget'; +import { ICellSidebarProps } from './CellSidebar'; import { notebookStore } from '../../NotebookState'; export const DATALAYER_CELL_SIDEBAR_CLASS_NAME = 'dla-CellSidebar-Container'; -export type ICellSidebarProps = { - notebookId: string; - cellId: string; - command: CommandRegistry; - nbgrader: boolean; -}; - -export class CellSidebarWidget - extends ReactPortalWidget - implements ICellHeader -{ - private readonly commands: CommandRegistry; +export class CellSidebarWidget extends ReactPortalWidget implements ICellHeader { + private readonly _commands: CommandRegistry; constructor( CellSidebar: (props: ICellSidebarProps) => JSX.Element, notebookId: string, @@ -33,18 +24,21 @@ export class CellSidebarWidget commands: CommandRegistry, ) { super(); - this.commands = commands; + this._commands = commands; this.addClass('jp-CellHeader'); - this.id = newUuid(); + const cellId = newUuid(); + this.node.id = cellId; const props: ICellSidebarProps = { notebookId: notebookId, - cellId: this.id, - command: this.commands, + cellId, + command: this._commands, nbgrader, }; const sidebar = createElement(CellSidebar, props); const portalDiv = ( -
{sidebar}
+
+ {sidebar} +
); const portal = createPortal(portalDiv, this.node); notebookStore.getState().addPortals({ diff --git a/packages/react/src/components/notebook/content/JupyterReactContentFactory.ts b/packages/react/src/components/notebook/content/JupyterReactContentFactory.ts index fd7847e3..0f3c3d19 100644 --- a/packages/react/src/components/notebook/content/JupyterReactContentFactory.ts +++ b/packages/react/src/components/notebook/content/JupyterReactContentFactory.ts @@ -7,7 +7,7 @@ import { CommandRegistry } from '@lumino/commands'; import { Notebook, NotebookPanel } from '@jupyterlab/notebook'; import { ICellHeader, Cell } from '@jupyterlab/cells'; -import { CellSidebarWidget, ICellSidebarProps } from '../cell/sidebar/CellSidebarWidget'; +import { CellSidebarWidget, ICellSidebarProps } from '../cell/sidebar'; import { CodeCell } from '@jupyterlab/cells'; import { CodeEditor } from '@jupyterlab/codeeditor'; // import { CodeMirrorEditor } from '@jupyterlab/codemirror'; diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbar.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbar.tsx index 8e34762e..b1ab841b 100644 --- a/packages/react/src/examples/extensions/celltoolbar/CellToolbar.tsx +++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbar.tsx @@ -4,191 +4,28 @@ * MIT License */ -import { useState } from 'react'; -import { CommandRegistry } from '@lumino/commands'; -import { PanelLayout } from '@lumino/widgets'; -import { Box, IconButton } from '@primer/react'; -import { - PlayIcon, - ChevronUpIcon, - ChevronDownIcon, - SquareIcon, - XIcon, -} from '@primer/octicons-react'; -import { useNotebookStore } from '../../../components'; +import { ReactWidget } from '@jupyterlab/apputils'; +import { CodeCell } from '@jupyterlab/cells'; +import { IDatalayerNotebookExtensionProps } from '../../../components'; +import { CellToolbarComponent } from './CellToolbarComponent'; -export type ICellToolbarProps = { - notebookId: string; - cellId: string; - command: CommandRegistry; -}; +export class CellToolbar extends ReactWidget { + private _cell: CodeCell; + private _props: IDatalayerNotebookExtensionProps; -const DATALAYER_CELL_SIDEBAR_CLASS_NAME = 'dla-CellSidebar-Container'; - -export const CellToolbar = (props: ICellToolbarProps) => { - const { notebookId, cellId } = props; - const notebookStore = useNotebookStore(); - const [visible, setVisible] = useState(false); - const activeCell = notebookStore.selectActiveCell(notebookId); - const layout = activeCell?.layout; - if (layout) { - const cellWidget = (layout as PanelLayout).widgets[0]; - if (cellWidget?.node.id === cellId) { - if (!visible) { - setVisible(true); - } - } - if (cellWidget?.node.id !== cellId) { - if (visible) { - setVisible(false); - } - } + constructor(cell: CodeCell, props: IDatalayerNotebookExtensionProps) { + super(); + this._cell = cell; + this._props = props; + this.addClass('dla-Container'); } - if (!visible) { - return
; + + render(): JSX.Element { + return ( + <> + + + ) } - return activeCell ? ( - - - { - e.preventDefault(); - notebookStore.run(notebookId); - }} - icon={PlayIcon} - variant="invisible" - /> - - - { - e.preventDefault(); - notebookStore.insertAbove({ - id: notebookId, - cellType: 'code', - }); - }} - icon={ChevronUpIcon} - variant="invisible" - /> - - - { - e.preventDefault(); - notebookStore.insertAbove({ - id: notebookId, - cellType: 'markdown', - }); - }} - icon={ChevronUpIcon} - variant="invisible" - /> - - - {activeCell.model.type === 'code' ? ( - { - e.preventDefault(); - notebookStore.changeCellType({ - id: notebookId, - cellType: 'markdown', - }); - }} - /> - ) : ( - { - e.preventDefault(); - notebookStore.changeCellType({ - id: notebookId, - cellType: 'code', - }); - }} - /> - )} - - - { - e.preventDefault(); - notebookStore.insertBelow({ - id: notebookId, - cellType: 'markdown', - }); - }} - icon={ChevronDownIcon} - variant="invisible" - /> - - - { - e.preventDefault(); - notebookStore.insertBelow({ - id: notebookId, - cellType: 'code', - }); - }} - icon={ChevronDownIcon} - variant="invisible" - /> - - - { - e.preventDefault(); - notebookStore.delete(notebookId); - }} - icon={XIcon} - variant="invisible" - /> - - - ) : ( - <> - ); -}; -export default CellToolbar; +} diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbar2.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbar2.tsx deleted file mode 100644 index 4e9a4b36..00000000 --- a/packages/react/src/examples/extensions/celltoolbar/CellToolbar2.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2021-2023 Datalayer, Inc. - * - * MIT License - */ - -import { ReactWidget } from '@jupyterlab/apputils'; -import { CodeCell } from '@jupyterlab/cells'; - -export class CellToolbar extends ReactWidget { - private _cell: CodeCell; - constructor(cell: CodeCell) { - super(); - this._cell = cell; - this.addClass('dla-Container'); - } - - render(): JSX.Element { - return <>id: {this._cell.model.id} - } -} diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbarComponent.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbarComponent.tsx new file mode 100644 index 00000000..1cb237c9 --- /dev/null +++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbarComponent.tsx @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + +import { CodeCell } from '@jupyterlab/cells'; +import { Box, IconButton } from '@primer/react'; +import { + PlayIcon, + ChevronUpIcon, + ChevronDownIcon, + SquareIcon, + XIcon, +} from '@primer/octicons-react'; +import { useNotebookStore, IDatalayerNotebookExtensionProps } from '../../../components'; + +type ICellToolbarComponentProps = { + cell: CodeCell; + extensionProps: IDatalayerNotebookExtensionProps; +} + +export const CellToolbarComponent = (props: ICellToolbarComponentProps) => { + const { extensionProps } = props; + const notebookId = extensionProps.notebookId; + const notebookStore = useNotebookStore(); + const activeCell = notebookStore.selectActiveCell(notebookId); + return activeCell ? ( + + + { + e.preventDefault(); + notebookStore.run(notebookId); + }} + icon={PlayIcon} + variant="invisible" + /> + + + { + e.preventDefault(); + notebookStore.insertAbove({ + id: notebookId, + cellType: 'code', + }); + }} + icon={ChevronUpIcon} + variant="invisible" + /> + + + { + e.preventDefault(); + notebookStore.insertAbove({ + id: notebookId, + cellType: 'markdown', + }); + }} + icon={ChevronUpIcon} + variant="invisible" + /> + + + {activeCell.model.type === 'code' ? ( + { + e.preventDefault(); + notebookStore.changeCellType({ + id: notebookId, + cellType: 'markdown', + }); + }} + /> + ) : ( + { + e.preventDefault(); + notebookStore.changeCellType({ + id: notebookId, + cellType: 'code', + }); + }} + /> + )} + + + { + e.preventDefault(); + notebookStore.insertBelow({ + id: notebookId, + cellType: 'markdown', + }); + }} + icon={ChevronDownIcon} + variant="invisible" + /> + + + { + e.preventDefault(); + notebookStore.insertBelow({ + id: notebookId, + cellType: 'code', + }); + }} + icon={ChevronDownIcon} + variant="invisible" + /> + + + { + e.preventDefault(); + notebookStore.delete(notebookId); + }} + icon={XIcon} + variant="invisible" + /> + + + ) : ( + <> + ); +}; + +export default CellToolbarComponent; diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx index 8ee449a2..aec4afd9 100644 --- a/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx +++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx @@ -6,21 +6,22 @@ import { INotebookModel, NotebookPanel } from '@jupyterlab/notebook'; import { DocumentRegistry } from '@jupyterlab/docregistry'; -import { DatalayerNotebookExtension } from '../../../components'; +import { DatalayerNotebookExtension, IDatalayerNotebookExtensionProps } from '../../../components'; import { CellToolbarWidget } from './CellToolbarWidget'; -import { ICellToolbarProps } from './CellToolbar'; import './CellToolbarExtension.css'; export class CellToolbarExtension implements DatalayerNotebookExtension { - private _props: ICellToolbarProps; - constructor(props: ICellToolbarProps) { - this._props= props; + private _props: IDatalayerNotebookExtensionProps; + + /* @override */ + init(props: IDatalayerNotebookExtensionProps) { + this._props = props; } /* @override */ createNew(notebookPanel: NotebookPanel, context: DocumentRegistry.IContext) { - new CellToolbarWidget(notebookPanel); + new CellToolbarWidget(notebookPanel, this._props); } /* @override */ diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx index 77e81f4e..964f1828 100644 --- a/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx +++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx @@ -9,9 +9,8 @@ import { Widget, PanelLayout } from '@lumino/widgets'; import { NotebookPanel } from '@jupyterlab/notebook'; import { IObservableList } from '@jupyterlab/observables'; import { Cell, CodeCell, ICellModel } from '@jupyterlab/cells'; -import { CellToolbar } from './CellToolbar2'; - -const CELL_TOOLBAR_CLASS = 'dla-CellToolbar'; +import { IDatalayerNotebookExtensionProps } from '../../../components'; +import { CellToolbar } from './CellToolbar'; export interface ICellToolbarSettings { highlight: boolean; @@ -20,10 +19,12 @@ export interface ICellToolbarSettings { export class CellToolbarWidget extends Widget { private _panel: NotebookPanel; + private _props: IDatalayerNotebookExtensionProps; - constructor(panel: NotebookPanel) { + constructor(panel: NotebookPanel, props: IDatalayerNotebookExtensionProps) { super(); this._panel = panel; + this._props = props; this.updateConnectedCell = this.updateConnectedCell.bind(this); const cells = this._panel.context.model.cells; cells.changed.connect(this.updateConnectedCell); @@ -72,7 +73,7 @@ export class CellToolbarWidget extends Widget { const layout = cell.layout; if (layout) { const panelLayout = layout as PanelLayout; - panelLayout.insertWidget(0, new CellToolbar(cell)); + panelLayout.insertWidget(1, new CellToolbar(cell, this._props)); } } } diff --git a/packages/react/src/examples/extensions/celltoolbar/index.ts b/packages/react/src/examples/extensions/celltoolbar/index.ts index 91b139ee..13b46736 100644 --- a/packages/react/src/examples/extensions/celltoolbar/index.ts +++ b/packages/react/src/examples/extensions/celltoolbar/index.ts @@ -5,4 +5,6 @@ */ export * from './CellToolbar'; +export * from './CellToolbarComponent'; export * from './CellToolbarExtension'; +export * from './CellToolbarWidget'; diff --git a/packages/react/src/examples/extensions/exectime/ExecTimeExtension.tsx b/packages/react/src/examples/extensions/exectime/ExecTimeExtension.tsx index ebb36f23..91a3cca8 100644 --- a/packages/react/src/examples/extensions/exectime/ExecTimeExtension.tsx +++ b/packages/react/src/examples/extensions/exectime/ExecTimeExtension.tsx @@ -6,13 +6,16 @@ import { INotebookModel, NotebookPanel } from '@jupyterlab/notebook'; import { DocumentRegistry } from '@jupyterlab/docregistry'; -import { DatalayerNotebookExtension } from '../../../components'; +import { DatalayerNotebookExtension, IDatalayerNotebookExtensionProps } from '../../../components'; import { ExecTimeWidget } from './ExecTimeWidget'; import './ExecTimeExtension.css'; export class ExecTimeExtension implements DatalayerNotebookExtension { - constructor() {} + + /* @override */ + init(props: IDatalayerNotebookExtensionProps) { + } /* @override */ createNew(notebookPanel: NotebookPanel, context: DocumentRegistry.IContext) { diff --git a/packages/react/webpack.config.js b/packages/react/webpack.config.js index 50d0f430..933cf3d2 100644 --- a/packages/react/webpack.config.js +++ b/packages/react/webpack.config.js @@ -43,15 +43,14 @@ const ENTRY = // './src/examples/Lumino'; // './src/examples/Matplotlib'; // './src/examples/Notebook'; - // './src/examples/NotebookExtension'; - // './src/examples/NotebookURL'; // './src/examples/NotebookCellSidebar'; './src/examples/NotebookCellToolbar'; // './src/examples/NotebookColorMode'; + // './src/examples/NotebookExtension'; // './src/examples/NotebookKernelChange'; + // './src/examples/NotebookLess'; // './src/examples/NotebookLite'; // './src/examples/NotebookLiteContext'; - // './src/examples/NotebookLess'; // './src/examples/NotebookMutationsKernel'; // './src/examples/NotebookMutationsServiceManager'; // './src/examples/NotebookNbformat'; @@ -66,6 +65,7 @@ const ENTRY = // './src/examples/NotebookTheme'; // './src/examples/NotebookThemeColormode'; // './src/examples/NotebookURL'; + // './src/examples/NotebookURL'; // './src/examples/ObservableHQ'; // './src/examples/Output'; // './src/examples/OutputWithMonitoring'; From 92191fb3c40eeefbf8680bf517d0f34a16f0105a Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sun, 27 Oct 2024 05:24:54 +0100 Subject: [PATCH 06/12] fix: build --- .../src/components/notebook/cell/sidebar/CellSidebarRun.tsx | 3 +-- packages/react/src/examples/sidebars/CellSidebarSource.tsx | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebarRun.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebarRun.tsx index a450acb7..4f54eba1 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebarRun.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebarRun.tsx @@ -8,9 +8,8 @@ import { useState } from 'react'; import { PanelLayout } from '@lumino/widgets'; import { Box, Button } from '@primer/react'; import { PlayIcon } from '@primer/octicons-react'; -import { ICellSidebarProps } from './CellSidebarWidget'; import useNotebookStore from '../../NotebookState'; - +import { ICellSidebarProps } from './CellSidebar'; import { DATALAYER_CELL_SIDEBAR_CLASS_NAME } from './CellSidebarWidget'; export const CellSidebarRun = (props: ICellSidebarProps) => { diff --git a/packages/react/src/examples/sidebars/CellSidebarSource.tsx b/packages/react/src/examples/sidebars/CellSidebarSource.tsx index 4df576e7..5c655ece 100644 --- a/packages/react/src/examples/sidebars/CellSidebarSource.tsx +++ b/packages/react/src/examples/sidebars/CellSidebarSource.tsx @@ -14,9 +14,9 @@ import { ChevronDownIcon, SquareIcon, } from '@primer/octicons-react'; -import { ICellSidebarProps } from '../../components/notebook/cell/sidebar/CellSidebarWidget'; -import CellMetadataEditor from '../../components/notebook/cell/metadata/CellMetadataEditor'; -import useNotebookStore from '../../components/notebook/NotebookState'; +import { ICellSidebarProps } from '../../components/notebook/cell/sidebar'; +import { CellMetadataEditor } from '../../components/notebook/cell/metadata'; +import { useNotebookStore } from '../../components/notebook'; import { DATALAYER_CELL_SIDEBAR_CLASS_NAME } from '../../components/notebook/cell/sidebar/CellSidebarWidget'; From 144ff82ff6733d5341a83f2d0e6e839629709783 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sun, 27 Oct 2024 06:23:57 +0100 Subject: [PATCH 07/12] core: notebook extension --- packages/react/src/components/notebook/Notebook.tsx | 5 ----- .../src/examples/extensions/celltoolbar/CellToolbar.tsx | 4 +++- .../examples/extensions/celltoolbar/CellToolbarWidget.tsx | 7 ++++--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/react/src/components/notebook/Notebook.tsx b/packages/react/src/components/notebook/Notebook.tsx index 409914c4..4affe9ab 100644 --- a/packages/react/src/components/notebook/Notebook.tsx +++ b/packages/react/src/components/notebook/Notebook.tsx @@ -300,11 +300,6 @@ export const Notebook = (props: INotebookProps) => { width: `${props.cellSidebarMargin + 10}px`, marginLeft: 'auto', }, - '& .jp-Cell .dla-CellToolbar-Container': { - padding: '4px 8px', - width: `${props.cellSidebarMargin + 10}px`, - marginLeft: 'auto', - }, '& .jp-CodeMirrorEditor': { cursor: 'text !important', }, diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbar.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbar.tsx index b1ab841b..474b49ae 100644 --- a/packages/react/src/examples/extensions/celltoolbar/CellToolbar.tsx +++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbar.tsx @@ -9,6 +9,8 @@ import { CodeCell } from '@jupyterlab/cells'; import { IDatalayerNotebookExtensionProps } from '../../../components'; import { CellToolbarComponent } from './CellToolbarComponent'; +export const DATALAYER_CELL_TOOLBAR_CLASS = 'dla-CellToolbar-Container'; + export class CellToolbar extends ReactWidget { private _cell: CodeCell; private _props: IDatalayerNotebookExtensionProps; @@ -17,7 +19,7 @@ export class CellToolbar extends ReactWidget { super(); this._cell = cell; this._props = props; - this.addClass('dla-Container'); + this.addClass(DATALAYER_CELL_TOOLBAR_CLASS); } render(): JSX.Element { diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx index 964f1828..c5645d05 100644 --- a/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx +++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx @@ -4,13 +4,12 @@ * MIT License */ -// import { createPortal } from 'react-dom'; import { Widget, PanelLayout } from '@lumino/widgets'; import { NotebookPanel } from '@jupyterlab/notebook'; import { IObservableList } from '@jupyterlab/observables'; import { Cell, CodeCell, ICellModel } from '@jupyterlab/cells'; import { IDatalayerNotebookExtensionProps } from '../../../components'; -import { CellToolbar } from './CellToolbar'; +import { CellToolbar, DATALAYER_CELL_TOOLBAR_CLASS } from './CellToolbar'; export interface ICellToolbarSettings { highlight: boolean; @@ -73,7 +72,9 @@ export class CellToolbarWidget extends Widget { const layout = cell.layout; if (layout) { const panelLayout = layout as PanelLayout; - panelLayout.insertWidget(1, new CellToolbar(cell, this._props)); + if (!panelLayout.widgets[1].hasClass(DATALAYER_CELL_TOOLBAR_CLASS)) { + panelLayout.insertWidget(1, new CellToolbar(cell, this._props)); + } } } } From 6d7606a9051f3c64833943eff2da39b878a72494 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sun, 27 Oct 2024 09:07:06 +0100 Subject: [PATCH 08/12] chore: jlab extension --- packages/react/src/app/JupyterReact.tsx | 68 +++++++++---------- packages/react/src/app/tabs/AboutTab.tsx | 24 +++---- packages/react/src/app/tabs/ComponentsTab.tsx | 13 ++-- .../app/tabs/components/NotebookComponent.tsx | 8 +-- packages/react/src/components/cell/Cell.tsx | 13 +--- .../react/src/components/cell/CellAdapter.ts | 2 +- ...itor.tsx => CodeMirrorDatalayerEditor.tsx} | 12 ++-- .../react/src/components/codemirror/index.ts | 2 +- .../react/src/components/output/Output.tsx | 6 +- .../src/components/output/OutputExecutor.ts | 2 +- packages/react/src/jupyter/lab/plugin.ts | 7 ++ .../react/src/theme/JupyterReactTheme.tsx | 6 +- packages/react/src/theme/index.ts | 2 +- 13 files changed, 80 insertions(+), 85 deletions(-) rename packages/react/src/components/codemirror/{CodeMirrorEditor.tsx => CodeMirrorDatalayerEditor.tsx} (90%) diff --git a/packages/react/src/app/JupyterReact.tsx b/packages/react/src/app/JupyterReact.tsx index 03c10825..ac192456 100644 --- a/packages/react/src/app/JupyterReact.tsx +++ b/packages/react/src/app/JupyterReact.tsx @@ -6,13 +6,14 @@ import { useState, useEffect } from 'react'; import { JupyterFrontEnd } from '@jupyterlab/application'; -import { ThemeProvider, BaseStyles, Box } from '@primer/react'; +import { Box } from '@primer/react'; import { UnderlineNav } from '@primer/react'; import { ReactJsIcon, RingedPlanetIcon } from '@datalayer/icons-react'; import { ServerConnection } from '@jupyterlab/services'; +import { JupyterReactTheme } from '../theme'; +import { requestAPI } from '../jupyter/JupyterHandlers'; import AboutTab from './tabs/AboutTab'; import ComponentsTab from './tabs/ComponentsTab'; -import { requestAPI } from '../jupyter/JupyterHandlers'; export type JupyterFrontEndProps = { app?: JupyterFrontEnd; @@ -35,39 +36,36 @@ const JupyterReact = (props: JupyterFrontEndProps): JSX.Element => { }); return ( <> - - - - - - { - e.preventDefault(); - setTab(1); - }} - > - Components - - { - e.preventDefault(); - setTab(2); - }} - > - About - - - - - {tab === 1 && } - {tab === 2 && } - - - - + + + + { + e.preventDefault(); + setTab(1); + }} + > + Components + + { + e.preventDefault(); + setTab(2); + }} + > + About + + + + + {tab === 1 && } + {tab === 2 && } + + ); }; diff --git a/packages/react/src/app/tabs/AboutTab.tsx b/packages/react/src/app/tabs/AboutTab.tsx index eed2a1f6..639158ed 100644 --- a/packages/react/src/app/tabs/AboutTab.tsx +++ b/packages/react/src/app/tabs/AboutTab.tsx @@ -23,22 +23,12 @@ const AboutTab = (props: Props): JSX.Element => { React.js components 💯% compatible with 🪐 Jupyter. - - {!egg ? ( - setEgg(true)} - /> - ) : ( - setEgg(false)} /> - )} - - Datalayer 0.3.0 Black Snake Release + Datalayer 1.2.0 Black Snake Release @@ -49,6 +39,16 @@ const AboutTab = (props: Props): JSX.Element => { Source code + + {!egg ? ( + setEgg(true)} + /> + ) : ( + setEgg(false)} /> + )} + ); }; diff --git a/packages/react/src/app/tabs/ComponentsTab.tsx b/packages/react/src/app/tabs/ComponentsTab.tsx index f17c3c73..31c3d833 100644 --- a/packages/react/src/app/tabs/ComponentsTab.tsx +++ b/packages/react/src/app/tabs/ComponentsTab.tsx @@ -6,11 +6,10 @@ import { useState } from 'react'; import { Box, NavList } from '@primer/react'; -import { Jupyter } from '../../jupyter/Jupyter'; import { JupyterFrontEndProps } from '../JupyterReact'; import FileBrowserComponent from './components/FileBrowserComponent'; -import CellComponent from './components/CellComponent'; -import NotebookComponent from './components/NotebookComponent'; +// import CellComponent from './components/CellComponent'; +// import NotebookComponent from './components/NotebookComponent'; import IPyWidgetsComponent from './components/IPyWidgetsComponent'; import ViewerComponent from './components/ViewerComponent'; @@ -60,13 +59,13 @@ const MainTab = (props: JupyterFrontEndProps) => {
- {nav === 1 && } - {nav === 2 && } - {nav === 3 && } {nav === 4 && } {nav === 5 && } - + {/* + {nav === 2 && } + {nav === 3 && } + */} diff --git a/packages/react/src/app/tabs/components/NotebookComponent.tsx b/packages/react/src/app/tabs/components/NotebookComponent.tsx index ae7daffc..b2f39aaa 100644 --- a/packages/react/src/app/tabs/components/NotebookComponent.tsx +++ b/packages/react/src/app/tabs/components/NotebookComponent.tsx @@ -4,15 +4,14 @@ * MIT License */ -// import Notebook from '../../../components/notebook/Notebook'; -// import CellSidebarNew from './../../../components/notebook/cell/sidebar/CellSidebarButton'; +import Notebook from '../../../components/notebook/Notebook'; +import CellSidebarNew from './../../../components/notebook/cell/sidebar/CellSidebarButton'; -// import nbformat from './../../..//examples/notebooks/IPyWidgetsExample.ipynb.json'; +import nbformat from './../../..//examples/notebooks/IPyWidgetsExample.ipynb.json'; const NotebookComponent = () => { return ( <> - {/* { height="calc(100vh - 2.6rem)" // (Height - Toolbar Height). CellSidebar={CellSidebarNew} /> - */} ); }; diff --git a/packages/react/src/components/cell/Cell.tsx b/packages/react/src/components/cell/Cell.tsx index b9571d86..ff4c49a0 100644 --- a/packages/react/src/components/cell/Cell.tsx +++ b/packages/react/src/components/cell/Cell.tsx @@ -7,11 +7,11 @@ import { useState, useEffect } from 'react'; import { CodeCell, MarkdownCell } from '@jupyterlab/cells'; import { Box } from '@primer/react'; -import Lumino from '../lumino/Lumino'; import { useJupyter } from './../../jupyter/JupyterContext'; +import { newUuid } from '../../utils'; +import { Lumino } from '../lumino'; import CellAdapter from './CellAdapter'; import useCellsStore from './CellState'; -import { newUuid } from '../../utils'; export type ICellProps = { /** @@ -44,19 +44,15 @@ export const Cell = (props: ICellProps) => { type='code', } = props; const { defaultKernel, serverSettings } = useJupyter(); - const [id] = useState(props.id || newUuid()); const [adapter, setAdapter] = useState(); - const cellsStore = useCellsStore(); - const handleCellInitEvents = (adapter: CellAdapter) => { adapter.cell.model.contentChanged.connect( (cellModel, changedArgs) => { cellsStore.setSource(id, cellModel.sharedModel.getSource()); } ); - if (adapter.cell instanceof CodeCell) { adapter.cell.outputArea.outputLengthChanged?.connect( (outputArea, outputsCount) => { @@ -64,14 +60,12 @@ export const Cell = (props: ICellProps) => { } ); } - adapter.sessionContext.initialize().then(() => { 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 @@ -79,7 +73,6 @@ export const Cell = (props: ICellProps) => { }) }); } - useEffect(() => { if (id && defaultKernel && serverSettings) { defaultKernel.ready.then(() => { @@ -95,7 +88,6 @@ export const Cell = (props: ICellProps) => { cellsStore.setSource(id, source); handleCellInitEvents(adapter); setAdapter(adapter); - const handleDblClick = (event: Event) => { let target = event.target as HTMLElement; /** @@ -109,7 +101,6 @@ export const Cell = (props: ICellProps) => { (adapter.cell as MarkdownCell).rendered = false; } }; - // Adds the event for double click and the removal on component's destroy document.addEventListener('dblclick', handleDblClick); return () => { diff --git a/packages/react/src/components/cell/CellAdapter.ts b/packages/react/src/components/cell/CellAdapter.ts index b3f342b1..306a3398 100755 --- a/packages/react/src/components/cell/CellAdapter.ts +++ b/packages/react/src/components/cell/CellAdapter.ts @@ -36,7 +36,6 @@ import { RenderMimeRegistry, standardRendererFactories as initialFactories, } from '@jupyterlab/rendermime'; -import { execute as executeOutput } from './../output/OutputExecutor'; import { Session, ServerConnection, @@ -50,6 +49,7 @@ import { WIDGET_MIMETYPE, WidgetRenderer, } from '@jupyter-widgets/html-manager/lib/output_renderers'; +import { execute as executeOutput } from './../output/OutputExecutor'; import { requireLoader as loader } from '../../jupyter/ipywidgets/libembed-amd'; import ClassicWidgetManager from '../../jupyter/ipywidgets/classic/manager'; import Kernel from '../../jupyter/kernel/Kernel'; diff --git a/packages/react/src/components/codemirror/CodeMirrorEditor.tsx b/packages/react/src/components/codemirror/CodeMirrorDatalayerEditor.tsx similarity index 90% rename from packages/react/src/components/codemirror/CodeMirrorEditor.tsx rename to packages/react/src/components/codemirror/CodeMirrorDatalayerEditor.tsx index f226e516..17445c20 100644 --- a/packages/react/src/components/codemirror/CodeMirrorEditor.tsx +++ b/packages/react/src/components/codemirror/CodeMirrorDatalayerEditor.tsx @@ -7,16 +7,16 @@ import { useState, useRef, useEffect } from 'react'; import { basicSetup } from 'codemirror'; import { EditorState } from '@codemirror/state'; -// import { Compartment } from '@codemirror/state'; +import { Compartment } from '@codemirror/state'; import { keymap, EditorView, ViewUpdate } from '@codemirror/view'; -// import { python } from '@codemirror/lang-python'; +import { python } from '@codemirror/lang-python'; import Kernel from '../../jupyter/kernel/Kernel'; import codeMirrorTheme from './CodeMirrorTheme'; import OutputAdapter from '../output/OutputAdapter'; import CodeMirrorOutputToolbar from './CodeMirrorOutputToolbar'; import useOutputsStore from '../output/OutputState'; -export const CodeMirrorEditor = (props: { +export const CodeMirrorDatalayerEditor = (props: { code: string; codePre?: string; outputAdapter: OutputAdapter; @@ -80,7 +80,7 @@ export const CodeMirrorEditor = (props: { }; useEffect(() => { outputStore.setInput(sourceId, code); -// const language = new Compartment(); + const language = new Compartment(); const keyBinding = [ { key: 'Shift-Enter', @@ -92,7 +92,7 @@ export const CodeMirrorEditor = (props: { doc: code, extensions: [ basicSetup, -// language.of(python()), // TODO CodeMirrorEditor imported by Output breaks the JupyterLab extension loading. https://github.com/datalayer/jupyter-ui/issues/170 + language.of(python()), EditorView.lineWrapping, keymap.of([...keyBinding]), codeMirrorTheme, @@ -147,4 +147,4 @@ export const CodeMirrorEditor = (props: { ); }; -export default CodeMirrorEditor; +export default CodeMirrorDatalayerEditor; diff --git a/packages/react/src/components/codemirror/index.ts b/packages/react/src/components/codemirror/index.ts index 7f7e8c74..cac9c1e1 100644 --- a/packages/react/src/components/codemirror/index.ts +++ b/packages/react/src/components/codemirror/index.ts @@ -4,6 +4,6 @@ * MIT License */ -export * from './CodeMirrorEditor'; +export * from './CodeMirrorDatalayerEditor'; export * from './CodeMirrorOutputToolbar'; export * from './CodeMirrorTheme'; diff --git a/packages/react/src/components/output/Output.tsx b/packages/react/src/components/output/Output.tsx index e7fb4015..9f6a7057 100644 --- a/packages/react/src/components/output/Output.tsx +++ b/packages/react/src/components/output/Output.tsx @@ -13,14 +13,14 @@ import { useEffect, useState } from 'react'; import { useJupyter } from '../../jupyter/JupyterContext'; import { Kernel } from '../../jupyter/kernel/Kernel'; import { newUuid } from '../../utils'; -import { CodeMirrorEditor } from '../codemirror/CodeMirrorEditor'; +import { CodeMirrorDatalayerEditor } from '../codemirror'; import { Lumino } from '../lumino/Lumino'; +import { IExecutionPhaseOutput } from '../../jupyter/kernel'; import { KernelActionMenu, KernelProgressBar } from './../kernel'; import { OutputAdapter } from './OutputAdapter'; import { OutputRenderer } from './OutputRenderer'; import { useOutputsStore } from './OutputState'; -import { IExecutionPhaseOutput } from '../../jupyter/kernel'; import './Output.css'; export type IOutputProps = { @@ -170,7 +170,7 @@ export const Output = (props: IOutputProps) => { }, }} > - = { }); const category = 'Datalayer'; palette.addItem({ command, category, args: { origin: 'from palette' } }); + if (launcher) { + launcher.add({ + command, + category, + rank: 2.4, + }); + } const settingsUpdated = (settings: ISettingRegistry.ISettings) => { const showInLauncher = settings.get('showInLauncher') .composite as boolean; diff --git a/packages/react/src/theme/JupyterReactTheme.tsx b/packages/react/src/theme/JupyterReactTheme.tsx index b3c4b169..338a96df 100644 --- a/packages/react/src/theme/JupyterReactTheme.tsx +++ b/packages/react/src/theme/JupyterReactTheme.tsx @@ -9,13 +9,14 @@ import { Colormode, JupyterLabCss } from './../theme'; type IJupyterLabThemeProps = { colormode: Colormode; + loadJupyterLabCss: boolean; } export const JupyterReactTheme = (props: React.PropsWithChildren) => { - const { children, colormode } = props; + const { children, colormode, loadJupyterLabCss } = props; return ( <> - + { loadJupyterLabCss && } Date: Sun, 27 Oct 2024 10:42:16 +0100 Subject: [PATCH 09/12] lint --- .../notebook/cell/sidebar/CellSidebar.tsx | 17 ++--------------- .../notebook/cell/sidebar/CellSidebarButton.tsx | 10 ++-------- .../notebook/cell/sidebar/CellSidebarRun.tsx | 2 +- .../notebook/cell/sidebar/CellSidebarWidget.tsx | 8 +++++++- .../celltoolbar/CellToolbarExtension.tsx | 4 ++-- .../celltoolbar/CellToolbarWidget.tsx | 3 ++- 6 files changed, 16 insertions(+), 28 deletions(-) diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx index d1d2e3ac..6a0220a7 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx @@ -7,26 +7,13 @@ import { useState } from 'react'; import { PanelLayout } from '@lumino/widgets'; import { ActionMenu, Button, Box } from '@primer/react'; -import { - ChevronRightIcon, - XIcon, - ChevronUpIcon, - ChevronDownIcon, - SquareIcon, -} from '@primer/octicons-react'; -import { CommandRegistry } from '@lumino/commands'; +import { ChevronRightIcon, XIcon, ChevronUpIcon, ChevronDownIcon, SquareIcon } from '@primer/octicons-react'; +import { ICellSidebarProps } from './CellSidebarWidget'; import CellMetadataEditor from '../metadata/CellMetadataEditor'; import useNotebookStore from '../../NotebookState'; import { DATALAYER_CELL_SIDEBAR_CLASS_NAME } from './CellSidebarWidget'; -export type ICellSidebarProps = { - notebookId: string; - cellId: string; - command: CommandRegistry; - nbgrader: boolean; -} - export const CellSidebar = (props: ICellSidebarProps) => { const { notebookId, cellId, nbgrader } = props; const [visible, setVisible] = useState(false); diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx index d213d93e..5db637d6 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx @@ -7,15 +7,9 @@ import { useState } from 'react'; import { PanelLayout } from '@lumino/widgets'; import { Box, IconButton } from '@primer/react'; -import { - PlayIcon, - ChevronUpIcon, - ChevronDownIcon, - SquareIcon, - XIcon, -} from '@primer/octicons-react'; -import { ICellSidebarProps } from './CellSidebar'; +import { PlayIcon, ChevronUpIcon, ChevronDownIcon, SquareIcon, XIcon } from '@primer/octicons-react'; import useNotebookStore from '../../NotebookState'; +import { ICellSidebarProps } from './CellSidebarWidget'; import { DATALAYER_CELL_SIDEBAR_CLASS_NAME } from './CellSidebarWidget'; diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebarRun.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebarRun.tsx index 4f54eba1..4d42eafe 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebarRun.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebarRun.tsx @@ -9,7 +9,7 @@ import { PanelLayout } from '@lumino/widgets'; import { Box, Button } from '@primer/react'; import { PlayIcon } from '@primer/octicons-react'; import useNotebookStore from '../../NotebookState'; -import { ICellSidebarProps } from './CellSidebar'; +import { ICellSidebarProps } from './CellSidebarWidget'; import { DATALAYER_CELL_SIDEBAR_CLASS_NAME } from './CellSidebarWidget'; export const CellSidebarRun = (props: ICellSidebarProps) => { diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebarWidget.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebarWidget.tsx index 858ecf04..4bf30677 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebarWidget.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebarWidget.tsx @@ -10,11 +10,17 @@ import { ICellHeader } from '@jupyterlab/cells'; import { CommandRegistry } from '@lumino/commands'; import { newUuid } from '../../../../utils/Utils'; import { ReactPortalWidget } from '../../../lumino/ReactPortalWidget'; -import { ICellSidebarProps } from './CellSidebar'; import { notebookStore } from '../../NotebookState'; export const DATALAYER_CELL_SIDEBAR_CLASS_NAME = 'dla-CellSidebar-Container'; +export type ICellSidebarProps = { + notebookId: string; + cellId: string; + command: CommandRegistry; + nbgrader: boolean; +} + export class CellSidebarWidget extends ReactPortalWidget implements ICellHeader { private readonly _commands: CommandRegistry; constructor( diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx index aec4afd9..87e4bc19 100644 --- a/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx +++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx @@ -12,7 +12,7 @@ import { CellToolbarWidget } from './CellToolbarWidget'; import './CellToolbarExtension.css'; export class CellToolbarExtension implements DatalayerNotebookExtension { - private _props: IDatalayerNotebookExtensionProps; + private _props?: IDatalayerNotebookExtensionProps; /* @override */ init(props: IDatalayerNotebookExtensionProps) { @@ -21,7 +21,7 @@ export class CellToolbarExtension implements DatalayerNotebookExtension { /* @override */ createNew(notebookPanel: NotebookPanel, context: DocumentRegistry.IContext) { - new CellToolbarWidget(notebookPanel, this._props); + new CellToolbarWidget(notebookPanel, this._props!); } /* @override */ diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx index c5645d05..3559fbaf 100644 --- a/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx +++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx @@ -40,7 +40,8 @@ export class CellToolbarWidget extends Widget { private _deregisterCellChanges(cellModel: ICellModel) { } - private _registerCellChanges(cellModel: ICellModel, disableHighlight = false) { + private _registerCellChanges(cellModel: ICellModel) { + const disableHighlight = false; const codeCell = this._getCodeCell(cellModel); if (codeCell) { codeCell.displayChanged.connect(() => { From 0c88113356b42d4e51c3d6ee0430a3f24c1a8273 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sun, 27 Oct 2024 12:34:07 +0100 Subject: [PATCH 10/12] chore: cellNodeId --- .../notebook/cell/sidebar/CellSidebar.tsx | 34 +++++++++---------- .../cell/sidebar/CellSidebarButton.tsx | 34 +++++++++---------- .../notebook/cell/sidebar/CellSidebarRun.tsx | 8 ++--- .../cell/sidebar/CellSidebarWidget.tsx | 8 ++--- .../content/JupyterReactContentFactory.ts | 17 ++++++++-- .../examples/sidebars/CellSidebarSource.tsx | 34 +++++++++---------- 6 files changed, 73 insertions(+), 62 deletions(-) diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx index 6a0220a7..a4094643 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebar.tsx @@ -15,19 +15,19 @@ import useNotebookStore from '../../NotebookState'; import { DATALAYER_CELL_SIDEBAR_CLASS_NAME } from './CellSidebarWidget'; export const CellSidebar = (props: ICellSidebarProps) => { - const { notebookId, cellId, nbgrader } = props; + const { notebookId, cellNodeId, nbgrader } = props; const [visible, setVisible] = useState(false); const notebookStore = useNotebookStore(); const activeCell = notebookStore.selectActiveCell(notebookId); const layout = activeCell?.layout; if (layout) { const cellWidget = (layout as PanelLayout).widgets[0]; - if (cellWidget?.node.id === cellId) { + if (cellWidget?.node.id === cellNodeId) { if (!visible) { setVisible(true); } } - if (cellWidget?.node.id !== cellId) { + if (cellWidget?.node.id !== cellNodeId) { if (visible) { setVisible(false); } @@ -63,7 +63,7 @@ export const CellSidebar = (props: ICellSidebarProps) => { */} )} - + - - + + - - + + - - + + {activeCell.model.type === 'code' ? ( )} - - + + - - + + - - + + - + ) : ( <> diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx index 5db637d6..f2b531a3 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebarButton.tsx @@ -14,19 +14,19 @@ import { ICellSidebarProps } from './CellSidebarWidget'; import { DATALAYER_CELL_SIDEBAR_CLASS_NAME } from './CellSidebarWidget'; export const CellSidebarButton = (props: ICellSidebarProps) => { - const { notebookId, cellId } = props; + const { notebookId, cellNodeId } = props; const notebookStore = useNotebookStore(); const [visible, setVisible] = useState(false); const activeCell = notebookStore.selectActiveCell(notebookId); const layout = activeCell?.layout; if (layout) { const cellWidget = (layout as PanelLayout).widgets[0]; - if (cellWidget?.node.id === cellId) { + if (cellWidget?.node.id === cellNodeId) { if (!visible) { setVisible(true); } } - if (cellWidget?.node.id !== cellId) { + if (cellWidget?.node.id !== cellNodeId) { if (visible) { setVisible(false); } @@ -44,7 +44,7 @@ export const CellSidebarButton = (props: ICellSidebarProps) => { }, }} > - + { icon={PlayIcon} variant="invisible" /> - - + + { icon={ChevronUpIcon} variant="invisible" /> - - + + { icon={ChevronUpIcon} variant="invisible" /> - - + + {activeCell.model.type === 'code' ? ( { }} /> )} - - + + { icon={ChevronDownIcon} variant="invisible" /> - - + + { icon={ChevronDownIcon} variant="invisible" /> - - + + { icon={XIcon} variant="invisible" /> - + ) : ( <> diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebarRun.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebarRun.tsx index 4d42eafe..597db3a8 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebarRun.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebarRun.tsx @@ -20,10 +20,10 @@ export const CellSidebarRun = (props: ICellSidebarProps) => { const layout = activeCell?.layout; if (layout) { const cellWidget = (layout as PanelLayout).widgets[0]; - if (!visible && cellWidget?.node.id === props.cellId) { + if (!visible && cellWidget?.node.id === props.cellNodeId) { setVisible(true); } - if (visible && cellWidget?.node.id !== props.cellId) { + if (visible && cellWidget?.node.id !== props.cellNodeId) { setVisible(false); } } @@ -39,7 +39,7 @@ export const CellSidebarRun = (props: ICellSidebarProps) => { }, }} > - + - + ) : ( <> diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebarWidget.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebarWidget.tsx index 4bf30677..d51c801f 100644 --- a/packages/react/src/components/notebook/cell/sidebar/CellSidebarWidget.tsx +++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebarWidget.tsx @@ -16,7 +16,7 @@ export const DATALAYER_CELL_SIDEBAR_CLASS_NAME = 'dla-CellSidebar-Container'; export type ICellSidebarProps = { notebookId: string; - cellId: string; + cellNodeId: string; command: CommandRegistry; nbgrader: boolean; } @@ -32,11 +32,11 @@ export class CellSidebarWidget extends ReactPortalWidget implements ICellHeader super(); this._commands = commands; this.addClass('jp-CellHeader'); - const cellId = newUuid(); - this.node.id = cellId; + const cellNodeId = newUuid(); + this.node.id = cellNodeId; const props: ICellSidebarProps = { notebookId: notebookId, - cellId, + cellNodeId, command: this._commands, nbgrader, }; diff --git a/packages/react/src/components/notebook/content/JupyterReactContentFactory.ts b/packages/react/src/components/notebook/content/JupyterReactContentFactory.ts index 0f3c3d19..8f4e9520 100644 --- a/packages/react/src/components/notebook/content/JupyterReactContentFactory.ts +++ b/packages/react/src/components/notebook/content/JupyterReactContentFactory.ts @@ -14,6 +14,18 @@ import { CodeEditor } from '@jupyterlab/codeeditor'; // import { IInputPrompt } from '@jupyterlab/cells'; // import { NotebookInputPrompt } from './../cell/InputPrompt'; +export const PYTHON_CELL_MIMETYPE = 'text/x-ipython'; + +export const SQL_CELL_MIMETYPE = 'application/sql'; + +export const isSQLCell = (cell: Cell) => { + const datalayer = cell.model.getMetadata('datalayer'); + if (datalayer) { + return (datalayer['sql'] === true); + } + return false; +} + class DatalayerCodeCell extends CodeCell { constructor(options: CodeCell.IOptions) { super(options); @@ -44,9 +56,8 @@ export class JupyterReactContentFactory extends NotebookPanel.ContentFactory { } private _updateSQLEditor = (cell: CodeCell) => { - const datalayer = cell.model?.getMetadata('datalayer'); - if (datalayer && datalayer['sql']) { - (cell.editor!.model as CodeEditor.Model).mimeType = 'application/sql'; + if (isSQLCell(cell)) { + (cell.editor!.model as CodeEditor.Model).mimeType = SQL_CELL_MIMETYPE; } } diff --git a/packages/react/src/examples/sidebars/CellSidebarSource.tsx b/packages/react/src/examples/sidebars/CellSidebarSource.tsx index 5c655ece..89dd6e0a 100644 --- a/packages/react/src/examples/sidebars/CellSidebarSource.tsx +++ b/packages/react/src/examples/sidebars/CellSidebarSource.tsx @@ -21,19 +21,19 @@ import { useNotebookStore } from '../../components/notebook'; import { DATALAYER_CELL_SIDEBAR_CLASS_NAME } from '../../components/notebook/cell/sidebar/CellSidebarWidget'; export const CellSidebarSource = (props: ICellSidebarProps) => { - const { notebookId, cellId, nbgrader } = props; + const { notebookId, cellNodeId, nbgrader } = props; const notebookStore = useNotebookStore(); const [visible, setVisible] = useState(false); const activeCell = notebookStore.selectActiveCell(notebookId); const layout = activeCell?.layout; if (layout) { const cellWidget = (layout as PanelLayout).widgets[0]; - if (cellWidget?.node.id === cellId) { + if (cellWidget?.node.id === cellNodeId) { if (!visible) { setVisible(true); } } - if (cellWidget?.node.id !== cellId) { + if (cellWidget?.node.id !== cellNodeId) { if (visible) { setVisible(false); } @@ -51,7 +51,7 @@ export const CellSidebarSource = (props: ICellSidebarProps) => { }, }} > - + - - + + - - + + - - + + {activeCell.model.type === 'code' ? ( )} - - + + - - + + - - + + - + {nbgrader && ( {/* From ddd3f8782ccf9ece2b3bfb6685f0ad01ed04dd73 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 27 Oct 2024 15:35:48 +0000 Subject: [PATCH 11/12] Automatic application of license header --- .../extensions/celltoolbar/CellToolbarExtension.css | 6 ++++++ .../src/examples/extensions/exectime/ExecTimeExtension.css | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.css b/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.css index b2fd47b1..38f74ee9 100644 --- a/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.css +++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.css @@ -1,3 +1,9 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + /* Transition to highlight a cell change */ @keyframes executeHighlight { from { diff --git a/packages/react/src/examples/extensions/exectime/ExecTimeExtension.css b/packages/react/src/examples/extensions/exectime/ExecTimeExtension.css index 8ebfc264..c1e48477 100644 --- a/packages/react/src/examples/extensions/exectime/ExecTimeExtension.css +++ b/packages/react/src/examples/extensions/exectime/ExecTimeExtension.css @@ -1,3 +1,9 @@ +/* + * Copyright (c) 2021-2023 Datalayer, Inc. + * + * MIT License + */ + /* Transition to highlight a cell change */ @keyframes executeHighlight { from { From cc8bc4842489a75de3436f1b9a592762bfe36a92 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Mon, 28 Oct 2024 12:00:52 +0100 Subject: [PATCH 12/12] Update packages/react/src/components/notebook/Notebook.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Frédéric Collonval --- packages/react/src/components/notebook/Notebook.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/notebook/Notebook.tsx b/packages/react/src/components/notebook/Notebook.tsx index 4affe9ab..c92c8193 100644 --- a/packages/react/src/components/notebook/Notebook.tsx +++ b/packages/react/src/components/notebook/Notebook.tsx @@ -314,7 +314,7 @@ export const Notebook = (props: INotebookProps) => { {extensionComponents.map((extensionComponent, index) => { return ( - + {extensionComponent} )}