Skip to content

Commit a035f88

Browse files
authored
feat: collaborative notebook (#346)
* chore: notebook collaborative * chore: collaborative notebook * chore: collaboration * deps: jupyter ydoc * fix: build * license
1 parent b229055 commit a035f88

27 files changed

+198
-66
lines changed

LICENSE

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ This repository contains source code from the following repositories under BSD-3
2525
- JupyterLab https://github.com/jupyterlab/jupyterlab
2626
- JupyterLite https://github.com/jupyterlite/jupyterlite
2727
- JupyterLab KernelSpy: https://github.com/jupyterlab-contrib/jupyterlab-kernelspy
28-
- JupyterLab Variables Inspecdtor: https://github.com/jupyterlab-contrib/jupyterlab-variableInspector
28+
- JupyterLab Variables Inspector: https://github.com/jupyterlab-contrib/jupyterlab-variableInspector
29+
- Jupyter Collaboration: https://github.com/jupyterlab/jupyter-collaboration

examples/lexical/src/App.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,10 @@ const Tabs = () => {
9090
<StyledNotebook>
9191
<Box mb={3}>
9292
<Notebook
93-
uid={NOTEBOOK_UID}
93+
id={NOTEBOOK_UID}
9494
nbformat={notebookContent}
9595
CellSidebar={CellSidebar}
96-
/>
96+
/>
9797
<Button
9898
onClick={(e: React.MouseEvent) => {
9999
e.preventDefault();
@@ -119,7 +119,7 @@ export default function App() {
119119
<div className="App">
120120
<h1>Jupyter UI ❤️ Lexical</h1>
121121
</div>
122-
<Jupyter>
122+
<Jupyter startDefaultKernel>
123123
<LexicalProvider>
124124
<Tabs/>
125125
</LexicalProvider>

examples/lexical/src/context/LexicalContext.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
import React, {useState, createContext, useContext} from 'react';
88
import { LexicalEditor } from "lexical";
99

10-
type LexicalCntextType = {
10+
type LexicalContextType = {
1111
editor?: LexicalEditor;
1212
setEditor: (editor?: LexicalEditor) => void;
1313
};
1414

15-
const context = createContext<LexicalCntextType | undefined>(undefined);
15+
const context = createContext<LexicalContextType | undefined>(undefined);
1616

1717
export function useLexical() {
1818
const lexicalContext = useContext(context);

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@
133133
"@jupyterlab/ui-components": "4.1.0",
134134
"@jupyterlite/server": "^0.4.0",
135135
"@jupyterlite/server-extension": "^0.4.0",
136-
"@jupyter/ydoc": "1.1.1",
136+
"@jupyter/ydoc": "3.0.2",
137137
"@lumino/algorithm": "2.0.1",
138138
"@lumino/application": "2.2.0",
139139
"@lumino/collections": "2.0.1",
@@ -244,7 +244,7 @@
244244
"@jupyterlab/ui-components": "4.1.0",
245245
"@jupyterlite/server": "^0.4.0",
246246
"@jupyterlite/server-extension": "^0.4.0",
247-
"@jupyter/ydoc": "1.1.1",
247+
"@jupyter/ydoc": "3.0.2",
248248
"@lumino/algorithm": "2.0.1",
249249
"@lumino/application": "2.2.0",
250250
"@lumino/collections": "2.0.1",

packages/lexical/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@datalayer/jupyter-lexical",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"description": "Jupyter UI for Lexical",
55
"license": "MIT",
66
"main": "lib/index.js",

packages/lexical/src/components/JupyterCellComponent.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import { Cell } from '@datalayer/jupyter-react';
99
export const JupyterCellComponent = (props: any) => {
1010
return (
1111
<Cell
12-
// startDefaultKernel={true}
12+
// startDefaultKernel
1313
source="print('Hello Jupyter React')"
14-
autoStart={true}
14+
autoStart
1515
/>
1616
)
1717
}

packages/lexical/src/examples/App1.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const App1 = () => {
4141
<div className="App">
4242
<h1>Jupyter UI ❤️ Lexical</h1>
4343
</div>
44-
<Jupyter startDefaultKernel={true}>
44+
<Jupyter startDefaultKernel>
4545
<LexicalProvider>
4646
<LexicalEditor/>
4747
</LexicalProvider>

packages/lexical/src/examples/App2.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export default function App() {
119119
<div className="App">
120120
<h1>Jupyter UI ❤️ Lexical</h1>
121121
</div>
122-
<Jupyter startDefaultKernel={true}>
122+
<Jupyter startDefaultKernel>
123123
<LexicalProvider>
124124
<Tabs/>
125125
</LexicalProvider>

packages/lexical/src/examples/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { createRoot } from "react-dom/client";
8-
import App from "./App1";
8+
import App from "./App2";
99

1010
const rootElement = document.getElementById("root");
1111

packages/lexical/src/examples/plugins/DraggableBlockPlugin/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ function useDraggableBlockMenu(
421421
<div
422422
className="icon draggable-block-menu"
423423
ref={menuRef}
424-
draggable={true}
424+
draggable
425425
onDragStart={onDragStart}
426426
onDragEnd={onDragEnd}>
427427
<div className={isEditable ? 'icon' : ''} />

packages/lexical/src/examples/plugins/ToolbarPlugin.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ function FloatingLinkEditor({ editor }: any) {
228228
function Select({ onChange, className, options, value }: any) {
229229
return (
230230
<select className={className} onChange={onChange} value={value}>
231-
<option hidden={true} value="" />
231+
<option hidden value="" />
232232
{options.map((option: string) => (
233233
<option key={option} value={option}>
234234
{option}

packages/lexical/src/nodes/ImageNode.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
547547
nodeKey={this.getKey()}
548548
showCaption={this.__showCaption}
549549
caption={this.__caption}
550-
resizable={true}
550+
resizable
551551
/>
552552
);
553553
}

packages/lexical/src/nodes/YouTubeNode.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function YouTubeComponent({
4949
src={`https://www.youtube.com/embed/${videoID}`}
5050
frameBorder="0"
5151
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
52-
allowFullScreen={true}
52+
allowFullScreen
5353
title="YouTube video"
5454
/>
5555
</BlockWithAlignableContents>

packages/lexical/src/ui/EquationEditor.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function InlineEquationEditor({
6969
className="EquationEditor_inlineEditor"
7070
value={equation}
7171
onChange={onChange}
72-
autoFocus={true}
72+
autoFocus
7373
ref={inputRef}
7474
/>
7575
<span className="EquationEditor_dollarSign">$</span>

packages/react/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@datalayer/jupyter-react",
3-
"version": "0.19.2",
3+
"version": "0.19.3",
44
"description": "Jupyter React - React.js components 100% compatible with Jupyter.",
55
"license": "MIT",
66
"main": "lib/index.js",
@@ -72,9 +72,8 @@
7272
"@jupyter-widgets/html-manager": "^1.0.0",
7373
"@jupyter-widgets/jupyterlab-manager": "^5.0.0",
7474
"@jupyter-widgets/output": "^6.0.0",
75-
"@jupyter/collaboration-extension": "^1.0.0",
7675
"@jupyter/web-components": "^0.15.3",
77-
"@jupyter/ydoc": "^2.0.1",
76+
"@jupyter/ydoc": "3.0.2",
7877
"@jupyterlab/application": "^4.0.0",
7978
"@jupyterlab/application-extension": "^4.0.0",
8079
"@jupyterlab/apputils": "^4.0.0",
@@ -151,6 +150,7 @@
151150
"usehooks-ts": "^2.9.1",
152151
"utf-8-validate": "^6.0.3",
153152
"wildcard-match": "^5.1.2",
153+
"y-websocket": "^2.1.0",
154154
"zustand": "^4.4.1"
155155
},
156156
"devDependencies": {

packages/react/src/components/jupyterlab/JupyterLabApp.tsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,21 @@ const JupyterLabAppComponent = (props: JupyterLabAppProps) => {
5959
theme,
6060
width,
6161
} = props;
62-
const { serviceManager, collaborative } = useJupyter({
62+
const { serviceManager } = useJupyter({
6363
serverless,
6464
serviceManager: propsServiceManager,
6565
startDefaultKernel,
6666
});
6767
const defaultMimeExtensionPromises = useMemo(
6868
() =>
6969
props.mimeRendererPromises ??
70-
JupyterLabAppCorePlugins(collaborative).mimeExtensionPromises,
70+
JupyterLabAppCorePlugins().mimeExtensionPromises,
7171
[]
7272
);
7373
const defaultExtensionPromises = useMemo(
7474
() =>
7575
props.pluginPromises ??
76-
JupyterLabAppCorePlugins(collaborative).extensionPromises,
76+
JupyterLabAppCorePlugins().extensionPromises,
7777
[]
7878
);
7979
const ref = useRef<HTMLDivElement>(null);
@@ -84,7 +84,6 @@ const JupyterLabAppComponent = (props: JupyterLabAppProps) => {
8484
...props,
8585
mimeRendererPromises: defaultMimeExtensionPromises,
8686
pluginPromises: defaultExtensionPromises,
87-
collaborative,
8887
serviceManager,
8988
});
9089
adapter.ready.then(() => {

packages/react/src/components/jupyterlab/JupyterLabAppPlugins.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const JupyterLabAppMinimumPlugins = {
3535
] as Array<Promise<IRenderMime.IExtensionModule>>,
3636
};
3737

38-
export const JupyterLabAppCorePlugins = (collaborative?: boolean) => {
38+
export const JupyterLabAppCorePlugins = () => {
3939
return {
4040
extensionPromises: [
4141
import('@jupyterlab/application-extension'),

packages/react/src/components/notebook/Notebook.tsx

+45-23
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,22 @@
77
import { useState, useEffect } from 'react';
88
import { createPortal } from 'react-dom';
99
import { Box } from '@primer/react';
10+
import { CommandRegistry } from '@lumino/commands';
11+
import { URLExt } from '@jupyterlab/coreutils';
1012
import { Cell, ICellModel } from '@jupyterlab/cells';
1113
import { NotebookPanel, INotebookModel } from '@jupyterlab/notebook';
12-
import { CommandRegistry } from '@lumino/commands';
1314
import { DocumentRegistry } from '@jupyterlab/docregistry';
1415
import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
1516
import { INotebookContent } from '@jupyterlab/nbformat';
1617
import { ServiceManager, Kernel as JupyterKernel } from '@jupyterlab/services';
17-
import { useJupyter, Lite, Kernel } from './../../jupyter';
18+
import { WebsocketProvider as YWebsocketProvider } from 'y-websocket';
19+
import { useJupyter, Lite, Kernel, requestDocSession, COLLABORATION_ROOM_URL_PATH } from './../../jupyter';
1820
import { asObservable, Lumino } from '../lumino';
21+
import { newUuid } from '../../utils';
22+
import { OnSessionConnection, KernelTransfer } from '../../state';
1923
import { CellMetadataEditor } from './cell/metadata';
2024
import { ICellSidebarProps } from './cell/sidebar';
2125
import { INotebookToolbarProps } from './toolbar/NotebookToolbar';
22-
import { newUuid } from '../../utils';
23-
import { OnSessionConnection, KernelTransfer } from '../../state';
2426
import { useNotebookStore } from './NotebookState';
2527
import { NotebookAdapter } from './NotebookAdapter';
2628

@@ -50,6 +52,7 @@ export type INotebookProps = {
5052
Toolbar?: (props: INotebookToolbarProps) => JSX.Element;
5153
cellMetadataPanel: boolean;
5254
cellSidebarMargin: number;
55+
collaborative?: boolean;
5356
extensions: DatalayerNotebookExtension[]
5457
height?: string;
5558
id: string;
@@ -97,6 +100,7 @@ export const Notebook = (props: INotebookProps) => {
97100
});
98101
const {
99102
Toolbar,
103+
collaborative,
100104
extensions,
101105
height,
102106
maxHeight,
@@ -114,7 +118,7 @@ export const Notebook = (props: INotebookProps) => {
114118
const notebookStore = useNotebookStore();
115119
const portals = notebookStore.selectNotebookPortals(id);
116120
// Bootstrap the Notebook Adapter.
117-
const bootstrapAdapter = (serviceManager?: ServiceManager.IManager, kernel?: Kernel) => {
121+
const bootstrapAdapter = async (collaborative: boolean, serviceManager?: ServiceManager.IManager, kernel?: Kernel) => {
118122
const adapter = new NotebookAdapter({
119123
...props,
120124
id,
@@ -132,18 +136,35 @@ export const Notebook = (props: INotebookProps) => {
132136
extension.createNew(adapter.notebookPanel!, adapter.context!);
133137
setExtensionComponents(extensionComponents.concat(extension.component ?? <></>));
134138
});
135-
// Update the global state.
136-
notebookStore.update({ id, state: { adapter } });
137-
// Update the global state based on events.
138-
adapter.notebookPanel?.model?.contentChanged.connect((notebookModel, _) => {
139-
notebookStore.changeModel({ id, notebookModel })
140-
});
139+
if (collaborative) {
140+
// Setup Collaboration.
141+
const ydoc = (adapter.notebookPanel?.model?.sharedModel as any).ydoc;
142+
const session = await requestDocSession("json", "notebook", path!);
143+
const yWebsocketProvider = new YWebsocketProvider(
144+
URLExt.join(serviceManager?.serverSettings.wsUrl!, COLLABORATION_ROOM_URL_PATH),
145+
`${session.format}:${session.type}:${session.fileId}`,
146+
ydoc,
147+
{
148+
disableBc: true,
149+
params: { sessionId: session.sessionId },
150+
// awareness: this._awareness
151+
}
152+
);
153+
console.log('Collaboration is setup with websocket provider.', yWebsocketProvider);
154+
// Update the notebook state with the adapter.
155+
notebookStore.update({ id, state: { adapter } });
156+
// Update the notebook state further to events.
157+
adapter.notebookPanel?.model?.contentChanged.connect((notebookModel, _) => {
158+
notebookStore.changeModel({ id, notebookModel });
159+
});
160+
}
141161
/*
142-
adapter.notebookPanel?.model!.sharedModel.changed.connect((_, notebookChange) => {
143-
notebookStore.notebookChange({ id, notebookChange });
162+
adapter.notebookPanel?.model?.sharedModel.changed.connect((_, notebookChange) => {
163+
notebookStore.changeNotebook({ id, notebookChange });
144164
});
165+
/*
145166
adapter.notebookPanel?.content.modelChanged.connect((notebook, _) => {
146-
dispatÅch(notebookStore.notebookChange({ id, notebook }));
167+
notebookStore.changeModel({ id, notebookModel: notebook.model! });
147168
});
148169
*/
149170
adapter.notebookPanel?.content.activeCellChanged.connect((_, cellModel) => {
@@ -185,12 +206,12 @@ export const Notebook = (props: INotebookProps) => {
185206
});
186207
}
187208
//
188-
const createAdapter = (serviceManager?: ServiceManager.IManager, kernel?: Kernel) => {
209+
const createAdapter = (collaborative: boolean, serviceManager?: ServiceManager.IManager, kernel?: Kernel) => {
189210
if (!kernel) {
190-
bootstrapAdapter(serviceManager, kernel);
211+
bootstrapAdapter(collaborative, serviceManager, kernel);
191212
} else {
192213
kernel.ready.then(() => {
193-
bootstrapAdapter(serviceManager, kernel);
214+
bootstrapAdapter(collaborative, serviceManager, kernel);
194215
});
195216
}
196217
}
@@ -206,12 +227,12 @@ export const Notebook = (props: INotebookProps) => {
206227
// Mutation Effects.
207228
useEffect(() => {
208229
if (serviceManager && serverless) {
209-
createAdapter(serviceManager, kernel);
230+
createAdapter(collaborative ?? false, serviceManager, kernel);
210231
}
211232
else if (serviceManager && kernel) {
212-
createAdapter(serviceManager, kernel);
233+
createAdapter(collaborative ?? false, serviceManager, kernel);
213234
}
214-
}, [serviceManager, kernel]);
235+
}, [collaborative, serviceManager, kernel]);
215236
useEffect(() => {
216237
if (adapter && adapter.kernel !== kernel) {
217238
adapter.setKernel(kernel);
@@ -235,15 +256,15 @@ export const Notebook = (props: INotebookProps) => {
235256
useEffect(() => {
236257
if (adapter && path && adapter.path !== path) {
237258
disposeAdapter();
238-
createAdapter(serviceManager);
259+
createAdapter(collaborative ?? false, serviceManager);
239260
}
240261
}, [path]);
241262
useEffect(() => {
242263
if (adapter && url && adapter.url !== url) {
243264
disposeAdapter();
244-
createAdapter(serviceManager);
265+
createAdapter(collaborative ?? false, serviceManager);
245266
}
246-
}, [url]);
267+
}, [collaborative, url]);
247268
// Dispose Effects.
248269
useEffect(() => {
249270
return () => {
@@ -339,6 +360,7 @@ export const Notebook = (props: INotebookProps) => {
339360
Notebook.defaultProps = {
340361
cellMetadataPanel: false,
341362
cellSidebarMargin: 120,
363+
collaborative: false,
342364
extensions: [],
343365
height: '100vh',
344366
kernelClients: [],

0 commit comments

Comments
 (0)