Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/react/src/components/notebook/Notebook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export type IDatalayerNotebookExtensionProps = {
notebookId: string;
commands: CommandRegistry;
panel: NotebookPanel;
adapter: NotebookAdapter;
};

export type DatalayerNotebookExtension = DocumentRegistry.IWidgetExtension<
Expand Down Expand Up @@ -168,6 +169,7 @@ export const Notebook = (props: INotebookProps) => {
notebookId: id,
commands: adapter.commands,
panel: adapter.notebookPanel!,
adapter,
});
extension.createNew(adapter.notebookPanel!, adapter.context!);
setExtensionComponents(
Expand Down
20 changes: 19 additions & 1 deletion packages/react/src/components/notebook/NotebookState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useStore } from 'zustand';
import { ReactPortal } from 'react';
import { INotebookModel } from '@jupyterlab/notebook';
import * as nbformat from '@jupyterlab/nbformat';
import { TableOfContents } from '@jupyterlab/toc';
import { Cell, ICellModel } from '@jupyterlab/cells';
import { NotebookChange } from '@jupyter/ydoc';
import { Kernel as JupyterKernel } from '@jupyterlab/services';
Expand All @@ -23,6 +24,7 @@ export type PortalDisplay = {

export type INotebookState = {
model?: INotebookModel;
tocModel?: TableOfContents.Model;
adapter?: NotebookAdapter;
saveRequest?: Date;
activeCell?: Cell<ICellModel>;
Expand All @@ -48,6 +50,10 @@ type NotebookModelId = {
id: string;
notebookModel: INotebookModel;
};
type TocModelId = {
id: string;
tocModel: TableOfContents.Model;
};
type CellModelId = {
id: string;
cellModel?: Cell<ICellModel>;
Expand Down Expand Up @@ -83,6 +89,7 @@ export type NotebookState = INotebooksState & {
selectNotebook: (id: string) => INotebookState | undefined;
selectNotebookAdapter: (id: string) => NotebookAdapter | undefined;
selectNotebookModel: (id: string) => { model: INotebookModel | undefined; changed: any } | undefined;
selectTocModel: (id: string) => TableOfContents.Model | undefined;
selectKernelStatus: (id: string) => string | undefined;
selectActiveCell: (id: string) => Cell<ICellModel> | undefined;
selectNotebookPortals: (id: string) => React.ReactPortal[] | undefined;
Expand All @@ -100,6 +107,7 @@ export type NotebookState = INotebooksState & {
update: (update: NotebookUpdate) => void;
activeCellChange: (cellModelId: CellModelId) => void;
changeModel: (notebookModelId: NotebookModelId) => void;
changeTocModel: (tocModelId: TocModelId) => void;
changeNotebook: (notebookChangeId: NotebookChangeId) => void;
changeKernelStatus: (kernelStatusId: KernelStatusMutation) => void;
changeKernel: (kernelChange: KernelChangeMutation) => void;
Expand Down Expand Up @@ -131,6 +139,9 @@ export const notebookStore = createStore<NotebookState>((set, get) => ({
}
return undefined;
},
selectTocModel: (id: string): TableOfContents.Model | undefined => {
return get().notebooks.get(id)?.tocModel;
},
selectKernelStatus: (id: string): string | undefined => {
return get().notebooks.get(id)?.kernelStatus;
},
Expand Down Expand Up @@ -207,9 +218,16 @@ export const notebookStore = createStore<NotebookState>((set, get) => ({
set((state: NotebookState) => ({ notebooks }));
}
},
changeTocModel: (tocModelId: TocModelId) => {
const notebooks = get().notebooks;
const notebook = notebooks.get(tocModelId.id);
if (notebook) {
notebook.tocModel = tocModelId.tocModel;
set((state: NotebookState) => ({ notebooks }));
}
},
changeNotebook: (notebookChangeId: NotebookChangeId) => {
const notebooks = get().notebooks;
const notebook = notebooks.get(notebookChangeId.id);
if (notebook) {
notebook.notebookChange = notebookChangeId.notebookChange;
set((state: NotebookState) => ({ notebooks }));
Expand Down
61 changes: 61 additions & 0 deletions packages/react/src/examples/NotebookToc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2021-2023 Datalayer, Inc.
*
* MIT License
*/

import { INotebookContent } from '@jupyterlab/nbformat';
import { useMemo, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { Notebook } from '../components/notebook/Notebook';
import { JupyterReactTheme } from '../theme/JupyterReactTheme';
import { NotebookToolbar } from '../components/notebook/toolbar/NotebookToolbar';
import { TocExtension } from './extensions/toc/TocExtension';
import { ReactLayoutFactory } from './extensions/toc/ReactLayoutFactory';
import nbformat from './notebooks/NotebookToCExample.ipynb.json';
import { Box, Button } from '@primer/react';
import { JupyterLayoutFactory } from './extensions/toc/JupyterLayoutFactory';

const NotebookToc = () => {
const [layout, setLayout] = useState<'react' | 'jupyter'>('jupyter');

const extensions = useMemo(
() => [
new TocExtension({
factory:
layout === 'react'
? new ReactLayoutFactory()
: new JupyterLayoutFactory(),
}),
],
[layout]
);
return (
<JupyterReactTheme>
<Box>
<Button
onClick={() => {
setLayout(layout === 'react' ? 'jupyter' : 'react');
}}
>
Use {layout === 'react' ? 'Jupyter' : 'React'} Layout
</Button>
</Box>
<Notebook
key={layout}
nbformat={nbformat as INotebookContent}
extensions={extensions}
id="notebook-toc-id"
height="calc(100vh - 2.6rem)" // (Height - Toolbar Height).
Toolbar={NotebookToolbar}
serverless
/>
</JupyterReactTheme>
);
};

const div = document.createElement('div');
document.body.appendChild(div);
const root = createRoot(div);

root.render(<NotebookToc />);
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2021-2023 Datalayer, Inc.
*
* MIT License
*/

import { NotebookPanel } from '@jupyterlab/notebook';
import { TocLayoutFactory } from './TocExtension';
import { TableOfContents, TableOfContentsPanel } from '@jupyterlab/toc';
import { BoxPanel } from '@lumino/widgets';

/**
* Jupyter ToC Layout Factory.
* Insert ToC Panel into (Lumino) notebook BoxPanel. (Default: left side and 50% stretch)
*/
export class JupyterLayoutFactory implements TocLayoutFactory {
private _tocPanel: TableOfContentsPanel;
private _config: Record<string, any>;

constructor(config?: Record<string, any>) {
this._config = config ?? {};
this._tocPanel = new TableOfContentsPanel();
}

layout(panel: BoxPanel, notebookPanel: NotebookPanel, notebookId: string) {
panel.direction = this._config?.direction ?? 'left-to-right';
panel.insertWidget(this._config?.index ?? 1, this._tocPanel);
BoxPanel.setStretch(this._tocPanel, this._config?.stretch ?? 0);
BoxPanel.setSizeBasis(this._tocPanel, this._config?.sizeBasis ?? 0);
return null;
}

setModel(model: TableOfContents.Model) {
this._tocPanel.model = model;
}

dispose() {
this._tocPanel.dispose();
}
}
45 changes: 45 additions & 0 deletions packages/react/src/examples/extensions/toc/ReactLayoutFactory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2021-2023 Datalayer, Inc.
*
* MIT License
*/

import { NotebookPanel } from '@jupyterlab/notebook';
import { TocLayoutFactory } from './TocExtension';
import { TableOfContents } from '@jupyterlab/toc';
import { BoxPanel } from '@lumino/widgets';
import TocComponent from './TocComponent';
import { Box } from '@primer/react';

/**
* React ToC Layout Factory.
*/
export class ReactLayoutFactory implements TocLayoutFactory {
constructor() {}

layout(panel: BoxPanel, notebookPanel: NotebookPanel, notebookId: string) {
return (
<Box
position="fixed"
top="2.6rem"
right={0}
width="200px"
height="100%"
style={{
float: 'right',
zIndex: 1000,
}}
>
<TocComponent notebookId={notebookId} />
</Box>
);
}

setModel(model: TableOfContents.Model) {
// React will get model from notebookStore
}

dispose() {
// React will dispose automatically
}
}
61 changes: 61 additions & 0 deletions packages/react/src/examples/extensions/toc/TocComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2021-2023 Datalayer, Inc.
*
* MIT License
*/

import { TableOfContents, TableOfContentsTree } from '@jupyterlab/toc';
import { useEffect, useState } from 'react';
import { useNotebookStore } from '../../../components';

export interface TocTreeProps {
notebookId: string;
}

/** Custom CSS Variables */
const CustomCssVarStyles = {
'--base-height-multiplier': '8', // Size scaling ratio
'--jp-inverse-layout-color3': '#a8a8a8', // Icon color
'--type-ramp-base-font-size': '14px', // Font size
} as React.CSSProperties;

/** Table of Contents Tree Component */
const TocTree = ({ notebookId }: TocTreeProps) => {
const model = useNotebookStore(state => state.selectTocModel(notebookId));
const [, setCount] = useState(0);
const update = () => setCount(c => c + 1);

useEffect(() => {
if (model) {
model.isActive = true;
// model change not trigger react update, so we need to manually trigger
model.stateChanged.connect(update);
}
return () => {
if (model) {
model.isActive = false;
model.stateChanged.disconnect(update);
}
};
}, [model, update]);

return model && model.headings.length > 0 ? (
<section style={CustomCssVarStyles}>
<TableOfContentsTree
activeHeading={model.activeHeading}
documentType={model.documentType}
headings={model.headings}
onCollapseChange={(heading: TableOfContents.IHeading) => {
model!.toggleCollapse({ heading });
}}
setActiveHeading={(heading: TableOfContents.IHeading) => {
model!.setActiveHeading(heading);
}}
/>
</section>
) : (
<>Empty</>
);
};

export default TocTree;
Loading