Skip to content

Commit

Permalink
passive thebe manager (#756)
Browse files Browse the repository at this point in the history
* wip

* latest

* controls are rendering

* revert notebookId change

* remove lumino

* use the rendermime

* 🎉 outputs

* fix markdown cell

* alert on comms change

* add factory method

* added on message handler

* 🧹 tidy up, upgrade demo

* 📗 changeset
  • Loading branch information
stevejpurves authored Jul 15, 2024
1 parent ea879c3 commit 30f569e
Show file tree
Hide file tree
Showing 19 changed files with 659 additions and 1,104 deletions.
7 changes: 7 additions & 0 deletions .changeset/clever-balloons-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'demo-react': patch
'thebe-react': patch
'thebe-core': patch
---

Added a passive widget manager and integrated into `thebe-react` and `demo-react`
1 change: 1 addition & 0 deletions apps/demo-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@types/node": "^16.18.14",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"classnames": "^2.5.1",
"localforage": "^1.10.0",
"match-sorter": "^6.3.1",
"react": "^18.2.0",
Expand Down
1,416 changes: 363 additions & 1,053 deletions apps/demo-react/public/widget-test.ipynb

Large diffs are not rendered by default.

27 changes: 19 additions & 8 deletions apps/demo-react/src/NotebookPage.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
import { ThebeSessionProvider, ThebeRenderMimeRegistryProvider, useThebeServer } from 'thebe-react';
import {
ThebeSessionProvider,
ThebeRenderMimeRegistryProvider,
useThebeServer,
useThebeLoader,
} from 'thebe-react';
import { ConnectionStatusTray } from './ConnectionStatusTray';
import { ConnectionErrorTray } from './ConnectionErrorTray';
import { NotebookStatusTray } from './NotebookStatusTray';
import { NotebookErrorTray } from './NotebookErrorTray';
import { AdminPanel } from './AdminPanel';

export function NotebookPage({ children }: React.PropsWithChildren) {
const { connecting, ready, config, error } = useThebeServer();
const { core } = useThebeLoader();
const { connecting, config, ready, error } = useThebeServer();

if (!core) return null;

if (!connecting && !ready && !error) return null;
return (
<ThebeRenderMimeRegistryProvider>
<ThebeSessionProvider start path={config?.kernels.path}>
<>
<ConnectionStatusTray />
<ConnectionErrorTray />
<NotebookStatusTray />
<NotebookErrorTray />
<AdminPanel />
{(connecting || ready || error) && (
<>
<ConnectionStatusTray />
<ConnectionErrorTray />
<NotebookStatusTray />
<NotebookErrorTray />
<AdminPanel />
</>
)}
{children}
</>
</ThebeSessionProvider>
Expand Down
17 changes: 14 additions & 3 deletions apps/demo-react/src/WidgetsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useNotebook } from 'thebe-react';
import JupyterOutputDecoration from './JupyterOutputDecoration';
import { useParams } from 'react-router-dom';
import classNames from 'classnames';

export function WidgetsPage() {
const { notebookName } = useParams<{ notebookName: string }>();
const { ready, executing, executeAll, cellRefs, cellIds } = useNotebook(
const { ready, executing, executeAll, cellRefs, cellIds, errors } = useNotebook(
notebookName ?? 'widget-test',
async (n) => {
const url = `/${n}.ipynb`;
Expand All @@ -27,12 +28,22 @@ export function WidgetsPage() {
<h4 className="text-sm">
notebook: <code>{notebookName}.ipynb</code>
</h4>
<div className="inline-block px-4 py-2 mt-3 text-sm font-bold text-white bg-green-500 rounded-full">
<div
className={classNames(
'inline-block px-4 py-2 mt-3 text-sm font-bold text-white rounded-full',
{ 'bg-gray-500': !ready, 'bg-green-500': ready },
)}
>
{ready ? 'ready' : 'not ready'}
</div>
<div className="mt-4">
{!executing && (
<button className="button" onClick={clickExecute}>
<button
className={classNames('button', {
'text-gray-400 bg-gray-100 border-gray-300 cursor-not-allowed': !ready || errors,
})}
onClick={clickExecute}
>
run all
</button>
)}
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions packages/core/src/cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,31 @@ import { ensureString, shortId } from './utils';

class ThebeCodeCell extends PassiveCellRenderer implements IThebeCell {
kind: CellKind;
notebookId: string;
source: string;
metadata: JsonObject;
session?: ThebeSession;
executionCount: number | null;
protected initialOutputs: IOutput[];
readonly notebookId: string;
protected busy: boolean;
protected events: EventEmitter;

constructor(
id: string,
notebookId: string,
source: string,
outputs: IOutput[],
config: Config,
metadata: JsonObject,
rendermime: IRenderMimeRegistry,
) {
super(id, rendermime);
super(id, outputs, rendermime);
this.kind = 'code';
this.events = new EventEmitter(id, config, EventSubject.cell, this);
this.notebookId = notebookId;
this.source = source;
this.metadata = metadata;
this.busy = false;
this.executionCount = null;
this.initialOutputs = [];
console.debug('thebe:cell constructor', this);
}

Expand All @@ -49,6 +48,7 @@ class ThebeCodeCell extends PassiveCellRenderer implements IThebeCell {
icc.id ?? shortId(),
notebookId,
ensureString(icc.source),
icc.outputs ?? [],
config,
icc.metadata,
rendermime,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from './thebe/api';
export * from './thebe/entrypoint';
export * from './utils';
export * from './manager';
export * from './passiveManager';
export * from './rendermime';
export * from './types';
export * from './config';
Expand Down
27 changes: 11 additions & 16 deletions packages/core/src/manager.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import type { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import type { Widget } from '@lumino/widgets';

import * as LuminoWidget from '@lumino/widgets';
import { MessageLoop } from '@lumino/messaging';

import { KernelWidgetManager, WidgetRenderer, output } from '@jupyter-widgets/jupyterlab-manager';

export const WIDGET_MIMETYPE = 'application/vnd.jupyter.widget-view+json';

import * as base from '@jupyter-widgets/base';
import * as controls from '@jupyter-widgets/controls';
import { shortId } from './utils';
import { RequireJsLoader } from './requireJsLoader';
import { requireLoader } from './loader';
import type { Kernel } from '@jupyterlab/services';

export const WIDGET_STATE_MIMETYPE = 'application/vnd.jupyter.widget-state+json';
export const WIDGET_VIEW_MIMETYPE = 'application/vnd.jupyter.widget-view+json';

/**
* @deprecated use WIDGET_VIEW_MIMETYPE
*/
export const WIDGET_MIMETYPE = WIDGET_VIEW_MIMETYPE;

/**
* A Widget Manager class for Thebe using the context-free KernelWidgetManager from
* the JupyterLab Manager and inspierd by the implementation in Voila here:
Expand All @@ -32,25 +35,17 @@ export class ThebeManager extends KernelWidgetManager {
/** ensure this registry always gets the widget renderer.
* This is essential for cases where widgets are rendered heirarchically
*/
this.addWidgetFactories();

this._registerWidgets();
this._loader = new RequireJsLoader();
}

addWidgetFactories() {
this.rendermime.addFactory(
{
safe: false,
mimeTypes: [WIDGET_MIMETYPE],
mimeTypes: [WIDGET_VIEW_MIMETYPE],
createRenderer: (options) => new WidgetRenderer(options, this as any),
},
1,
);
}

removeWidgetFactories() {
this.rendermime.removeMimeType(WIDGET_MIMETYPE);
this._registerWidgets();
this._loader = new RequireJsLoader();
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import PassiveCellRenderer from './passive';
import type ThebeSession from './session';
import type { CellKind, IThebeCell, IThebeCellExecuteReturn, JsonObject } from './types';
import { ensureString, shortId } from './utils';
import type ThebeNotebook from './notebook';

/**
* A Thebe cell that is exepected to contain markdown (or raw) source.
Expand All @@ -28,7 +29,7 @@ export default class ThebeMarkdownCell extends PassiveCellRenderer implements IT
metadata: JsonObject,
rendermime: IRenderMimeRegistry,
) {
super(id, rendermime);
super(id, [], rendermime);
this.kind = 'markdown';
this.id = id;
this.notebookId = notebookId;
Expand Down
30 changes: 27 additions & 3 deletions packages/core/src/notebook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,32 @@ export interface CodeBlock {
[x: string]: any;
}

function coerceToObject(maybe: any): Record<string, any> {
if (typeof maybe === 'object') return maybe;
if (Array.isArray(maybe)) return Object.fromEntries(maybe.map((v, k) => [k, v]));
return {};
}

class ThebeNotebook {
readonly id: string;
readonly rendermime: IRenderMimeRegistry;
cells: IThebeCell[];
metadata: INotebookMetadata;
widgetState: Record<string, any>;
session?: ThebeSession;
protected events: EventEmitter;

constructor(id: string, config: Config, rendermime: IRenderMimeRegistry) {
constructor(
id: string,
config: Config,
rendermime: IRenderMimeRegistry,
metadata?: INotebookMetadata,
) {
this.id = id;
this.events = new EventEmitter(id, config, EventSubject.notebook, this);
this.cells = [];
this.metadata = {};
this.metadata = metadata ?? {};
this.widgetState = coerceToObject(metadata?.widgets);
this.rendermime = rendermime;
console.debug('thebe:notebook constructor', this);
}
Expand All @@ -37,7 +50,15 @@ class ThebeNotebook {
const notebook = new ThebeNotebook(id, config, rendermime);
notebook.cells = blocks.map((c) => {
const metadata = {};
const cell = new ThebeCodeCell(c.id, id, c.source, config, metadata, notebook.rendermime);
const cell = new ThebeCodeCell(
c.id,
notebook.id,
c.source,
c.outputs ?? [],
config,
metadata,
notebook.rendermime,
);
console.debug(`thebe:notebook:fromCodeBlocks Initializing cell ${c.id}`);
return cell;
});
Expand Down Expand Up @@ -71,6 +92,9 @@ class ThebeNotebook {
return p;
}

/**
@deprecated
*/
get widgets() {
return this.findCells('widget') ?? [];
}
Expand Down
19 changes: 15 additions & 4 deletions packages/core/src/passive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,25 @@ import { MessageLoop } from '@lumino/messaging';
class PassiveCellRenderer implements IPassiveCell {
readonly id: string;
readonly rendermime: IRenderMimeRegistry;
initialOutputs: nbformat.IOutput[];

protected model: OutputAreaModel;
protected area: OutputArea;

constructor(id: string, rendermime?: IRenderMimeRegistry, mathjax?: MathjaxOptions) {
constructor(
id: string,
initialOutputs?: nbformat.IOutput[],
rendermime?: IRenderMimeRegistry,
mathjax?: MathjaxOptions,
) {
this.id = id;
this.rendermime = rendermime ?? makeRenderMimeRegistry(mathjax ?? makeMathjaxOptions());
this.model = new OutputAreaModel({ trusted: true });
this.area = new OutputArea({
model: this.model,
rendermime: this.rendermime,
});
this.initialOutputs = initialOutputs ?? [];
}

/**
Expand All @@ -34,7 +42,10 @@ class PassiveCellRenderer implements IPassiveCell {
return this.area.isAttached;
}

attachToDOM(el?: HTMLElement, strict = false) {
attachToDOM(
el?: HTMLElement,
opts: { strict?: boolean; appendExisting?: boolean } = { strict: false, appendExisting: true },
) {
if (!this.area || !el) {
console.error(
`thebe:renderer:attachToDOM - could not attach to DOM - area: ${this.area}, el: ${el}`,
Expand All @@ -44,11 +55,11 @@ class PassiveCellRenderer implements IPassiveCell {
if (this.area.isAttached) {
// TODO should we detach and reattach?
console.debug(`thebe:renderer:attachToDOM - already attached`);
if (strict) return;
if (opts.strict) return;
} else {
// if the target element has contents, preserve it but wrap it in our output area
console.debug(`thebe:renderer:attachToDOM ${this.id} - appending existing contents`);
if (el.innerHTML) {
if (opts.appendExisting && el.innerHTML) {
this.area.model.add({
output_type: 'display_data',
data: {
Expand Down
Loading

0 comments on commit 30f569e

Please sign in to comment.