diff --git a/CLAUDE.md b/CLAUDE.md
index 67c72afb0..959f83579 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,66 +1,66 @@
-# Jupyter UI - AI Assistant Guide
+# Jupyter UI - Quick Reference
-## Quick Overview
+## Overview
-React component library for Jupyter notebooks. Monorepo with 4 packages managed by Lerna.
+React component library for Jupyter notebooks. Monorepo with 4 main packages.
-## Core Packages
-
-- `@datalayer/jupyter-react` - React components for notebooks, cells, terminals
-- `@datalayer/jupyter-lexical` - Rich text editor integration
-- `@datalayer/jupyter-docusaurus-plugin` - Docusaurus plugin
-- `datalayer-jupyter-vscode` - VS Code extension
-
-## Essential Commands
+## Commands
```bash
-npm install # Install dependencies
-npm run build # Build all packages
-npm run jupyter:server # Start Jupyter server (port 8686)
-npm run storybook # Start Storybook (port 6006)
-npm run lint # Check errors only (--quiet)
-npm run lint:fix # Auto-fix issues
-npm run format # Format code
-npm run type-check # TypeScript checking
-npm run check # Run all checks (format, lint, type)
-npm run check:fix # Auto-fix and check all
-npm test # Run tests
+# Setup
+npm install
+npm run build
+
+# Development
+npm run jupyter:server # Start Jupyter (port 8686)
+npm run storybook # Start Storybook (port 6006)
+
+# React package dev (cd packages/react)
+npm run start # Remote server config
+npm run start-local # Local server (webpack + jupyter)
+npm run start-local:webpack # Only webpack
+
+# Code quality
+npm run check # Format, lint, type-check
+npm run check:fix # Auto-fix and check
```
-## Requirements
+## Key Info
-- Node.js >= 20.0.0 (use .nvmrc)
-- npm (not yarn)
-- Server token: `60c1661cc408f978c309d04157af55c9588ff9557c9380e4fb50785750703da6`
+- **Node.js**: >= 20.0.0 (use .nvmrc)
+- **Server token**: `60c1661cc408f978c309d04157af55c9588ff9557c9380e4fb50785750703da6`
+- **Webpack entry**: Edit `packages/react/webpack.config.js` → `ENTRY` variable
+- **Jupyter config**: `dev/config/jupyter_server_config.py`
-## Key Files
+## Collaboration Setup
-- `eslint.config.js` - ESLint v9 flat config
-- `.prettierrc.json` - Formatter config
-- `.prettierignore` - Excludes MDX files
-- `patches/` - Third-party fixes (auto-applied)
-- `packages/react/webpack.config.js` - Build config
+1. Install: `pip install jupyter-collaboration jupyterlab`
+2. Enable: Set `c.LabApp.collaborative = True` in jupyter config
+3. Test: Open http://localhost:3208/ in multiple windows
-## Recent Fixes (2024)
+## Collaboration Providers
-- MDX comments: `{/_` → `{/** **/}` in 13 files
-- Node requirement: 18 → 20+
-- Webpack warnings: 7 → 2 (source-map exclusions)
-- @jupyterlite patch for missing logos
-- ESLint v9 flat config migration
-- React 18 deprecations fixed
-- Storybook CI: Added wait-on and --url for test reliability
-- Terminal component: Fixed BoxPanel initialization issue
+```tsx
+// Basic usage
+const provider = new JupyterCollaborationProvider();
+;
+
+// With config
+const provider = new JupyterCollaborationProvider({
+ path: 'notebook.ipynb',
+ serverSettings: mySettings,
+});
+```
-## Common Issues
+## Troubleshooting
-1. **Storybook errors**: Check MDX syntax, run `npx patch-package`
-2. **Node version**: Use Node 20+ (`nvm use`)
-3. **Lint errors**: Run `npm run lint:fix`
-4. **Build fails**: Run `npm run type-check`
+- **Build fails**: Run `npm run type-check`
+- **Lint errors**: Run `npm run lint:fix`
+- **Node version**: Use Node 20+ (`nvm use`)
+- **Collaboration issues**: Check WebSocket connections and jupyter-collaboration installation
-## AI Assistant Notes
+## Development Tips
-- Always use npm, not yarn
+- Use npm, not yarn
- Prefer editing over creating files
-- Run lint/type checks before committing
+- Run checks after changes: `npm run check:fix`
diff --git a/README.md b/README.md
index 61df031ec..67aef516b 100644
--- a/README.md
+++ b/README.md
@@ -20,12 +20,12 @@ Jupyter UI is a set of [React.js](https://react.dev) components that allow a fro
## 📦 Packages
-| Package | Version | Description |
-| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
-| [@datalayer/jupyter-react](./packages/react) | [](https://www.npmjs.com/package/@datalayer/jupyter-react) | Core React components for Jupyter integration |
-| [@datalayer/jupyter-lexical](./packages/lexical) | [](https://www.npmjs.com/package/@datalayer/jupyter-lexical) | Rich text editor with Lexical framework |
-| [@datalayer/jupyter-docusaurus-plugin](./packages/docusaurus-plugin) | [](https://www.npmjs.com/package/@datalayer/jupyter-docusaurus-plugin) | Docusaurus plugin for Jupyter notebooks |
-| [datalayer-jupyter-vscode](./packages/vscode) | [](https://marketplace.visualstudio.com/items?itemName=datalayer.datalayer-jupyter-vscode) | VS Code extension |
+| Package | Version | Description |
+| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
+| [@datalayer/jupyter-react](./packages/react) | [](https://www.npmjs.com/package/@datalayer/jupyter-react) | Generic React components for Jupyter |
+| [@datalayer/jupyter-lexical](./packages/lexical) | [](https://www.npmjs.com/package/@datalayer/jupyter-lexical) | Rich text editor with Lexical framework |
+| [@datalayer/jupyter-docusaurus-plugin](./packages/docusaurus-plugin) | [](https://www.npmjs.com/package/@datalayer/jupyter-docusaurus-plugin) | Docusaurus plugin for Jupyter notebooks |
+| [datalayer-jupyter-vscode](./packages/vscode) | [](https://marketplace.visualstudio.com/items?itemName=datalayer.datalayer-jupyter-vscode) | VS Code extension |
## 🚀 Quick Start
@@ -38,21 +38,65 @@ npm install @datalayer/jupyter-react
### Basic Usage
```tsx
-import { Jupyter, Notebook } from '@datalayer/jupyter-react';
+import { JupyterReactTheme, Notebook } from '@datalayer/jupyter-react';
function App() {
return (
-
-
-
+
+
+
);
}
```
+### Collaborative Editing
+
+Jupyter UI supports real-time collaboration through a pluggable provider system:
+
+```tsx
+import {
+ Notebook,
+ JupyterCollaborationProvider,
+} from '@datalayer/jupyter-react';
+
+function CollaborativeNotebook() {
+ const collaborationProvider = new JupyterCollaborationProvider();
+
+ return (
+
+ );
+}
+```
+
+#### Creating Custom Collaboration Providers
+
+You can create your own collaboration provider by extending `CollaborationProviderBase`:
+
+```tsx
+import { CollaborationProviderBase } from '@datalayer/jupyter-react';
+
+class MyCustomProvider extends CollaborationProviderBase {
+ constructor(config) {
+ super('my-provider-type');
+ // Initialize your provider
+ }
+
+ async connect(sharedModel, documentId, options) {
+ // Implement your connection logic
+ // Set up WebSocket, authenticate, etc.
+ }
+}
+
+// Use it with any Notebook component
+const provider = new MyCustomProvider({
+ /* config */
+});
+;
+```
+
### Development Setup
As a developer start with the [setup of your environment](https://jupyter-ui.datalayer.tech/docs/develop/setup) and try [one of the examples](https://jupyter-ui.datalayer.tech/docs/category/examples). We have [documentation](https://jupyter-ui.datalayer.tech) for more details.
@@ -100,12 +144,21 @@ We host a Storybook on ✨ https://jupyter-ui-storybook.datalayer.tech that show
### Advanced Features
- **🔌 IPyWidgets Support** - Full support for interactive widgets
-- **👥 Collaborative Editing** - Real-time collaboration using Y.js
+- **👥 Collaborative Editing** - Pluggable provider system supporting:
+ - Jupyter collaboration (WebSocket-based with Y.js)
+ - Custom providers via `ICollaborationProvider` interface
- **🎨 Theming** - JupyterLab theme support with dark/light modes
- **🔧 Extensible** - Plugin system for custom functionality
- **🚀 Performance** - Virtual scrolling, lazy loading, and optimizations
- **🔒 Security** - Token authentication, CORS, XSS protection
+### Architecture Highlights
+
+- **🏗️ Clean Architecture** - Modular, composable components with clear interfaces
+- **🔄 Composition Pattern** - Components compose rather than inherit for maximum flexibility
+- **🔌 Provider System** - Pluggable collaboration providers for different backends
+- **📦 One-way Dependencies** - Core depends on jupyter-react, not vice versa
+
@@ -151,7 +204,7 @@ We maintain a plugin for [Docusaurus](https://docusaurus.io) in the [docusaurus-
## 📋 Requirements
-- **Node.js** >= 18.0.0
+- **Node.js** >= 20.0.0
- **npm** >= 8.0.0
- **Python** >= 3.8 (for Jupyter server)
- **JupyterLab** >= 4.0.0
diff --git a/SUMMARY.md b/SUMMARY.md
index 1a806ae3f..d0a0a0222 100644
--- a/SUMMARY.md
+++ b/SUMMARY.md
@@ -4,6 +4,15 @@
Jupyter UI is an open-source React.js component library that bridges the gap between the Jupyter ecosystem and modern web development frameworks. It provides React components that are 100% compatible with Jupyter, allowing developers to build custom data products without being constrained by the traditional JupyterLab interface.
+## Recent Updates (January 2025)
+
+### Collaboration Provider System
+
+- Implemented plugin-based architecture for extensible collaboration
+- Built-in providers: `JupyterCollaborationProvider`, `NoOpCollaborationProvider`
+- Direct instantiation pattern for simplicity
+- Added collaboration support to `Notebook2` component
+
### Core Problem Solved
Traditional JupyterLab uses the Lumino widget toolkit, an imperative UI framework that isn't compatible with modern declarative frameworks like React. This forces developers to either:
@@ -23,7 +32,7 @@ The project uses Lerna to manage a monorepo structure with the following organiz
```
jupyter-ui/
├── packages/ # Core library packages
-│ ├── react/ # Main React component library
+│ ├── react/ # Main React component library (generic)
│ ├── lexical/ # Rich text editor integration
│ ├── docusaurus-plugin/ # Docusaurus integration
│ └── vscode/ # VS Code extension
@@ -60,12 +69,15 @@ The main package providing React components for Jupyter functionality.
- Provides React context providers for state management
- Supports both local and remote Jupyter servers
- Implements WebSocket communication for real-time updates
+- Plugin-based collaboration provider system
+- Extensible without platform-specific code
**Key Files:**
- `src/jupyter/JupyterContext.tsx` - Core context provider
-- `src/components/notebook/Notebook.tsx` - Main notebook component
+- `src/components/notebook/Notebook.tsx` - Main notebook component (accepts collaborationProvider)
- `src/providers/ServiceManagerProvider.tsx` - Service management
+- `src/jupyter/collaboration/ICollaborationProvider.ts` - Provider interface
### 2. @datalayer/jupyter-lexical (v1.0.3)
@@ -454,11 +466,11 @@ The repository includes several example implementations:
## Community & Ecosystem
-- Active development by Datalayer, Inc.
- MIT licensed
- Integration with major React frameworks
- Storybook for component documentation
- Comprehensive documentation site
+- Active development by Datalayer, Inc.
## Future Roadmap (Based on Code Structure)
diff --git a/dev/config/jupyter_server_config.py b/dev/config/jupyter_server_config.py
index e09dbf647..a26383b94 100755
--- a/dev/config/jupyter_server_config.py
+++ b/dev/config/jupyter_server_config.py
@@ -106,4 +106,4 @@
# JupyterLab
#################
-c.LabApp.collaborative = False
+c.LabApp.collaborative = True
diff --git a/dev/notebooks/collaboration.ipynb b/dev/notebooks/collaboration.ipynb
new file mode 100644
index 000000000..21405e3d3
--- /dev/null
+++ b/dev/notebooks/collaboration.ipynb
@@ -0,0 +1,93 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Collaboration Example\n",
+ "This notebook is for testing real-time collaboration."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-08-18T06:03:49.869760Z",
+ "iopub.status.busy": "2025-08-18T06:03:49.869446Z",
+ "iopub.status.idle": "2025-08-18T06:03:49.875095Z",
+ "shell.execute_reply": "2025-08-18T06:03:49.874611Z",
+ "shell.execute_reply.started": "2025-08-18T06:03:49.869729Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Hello from collaboration notebook!\n"
+ ]
+ }
+ ],
+ "source": [
+ "print('Hello from collaboration notebook!')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-08-18T06:03:51.258535Z",
+ "iopub.status.busy": "2025-08-18T06:03:51.258136Z",
+ "iopub.status.idle": "2025-08-18T06:03:51.262464Z",
+ "shell.execute_reply": "2025-08-18T06:03:51.261483Z",
+ "shell.execute_reply.started": "2025-08-18T06:03:51.258506Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "x + y = 30\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Test collaboration by editing this cell\n",
+ "x = 10\n",
+ "y = 20\n",
+ "print(f'x + y = {x + y}')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.12.7"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/packages/react/package.json b/packages/react/package.json
index 55db5d7e4..8f7a0bb44 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -54,7 +54,7 @@
"start-noconfig": "cross-env NO_CONFIG=true webpack serve",
"start-local": "run-p -c 'start-local:*'",
"start-local:webpack": "cross-env LOCAL_JUPYTER_SERVER=true webpack serve",
- "start-local:jupyter-server": "cd ./../.. && make start-jupyter-server",
+ "start-local:jupyter-server": "cd ./../.. && npm run jupyter:server",
"stylelint": "npm stylelint:check --fix",
"stylelint:check": "stylelint --cache \"style/**/*.css\"",
"test": "jest --coverage",
diff --git a/packages/react/public/index-local.html b/packages/react/public/index-local.html
index 7388c2d8d..17dd3d02c 100755
--- a/packages/react/public/index-local.html
+++ b/packages/react/public/index-local.html
@@ -28,7 +28,7 @@
"appUrl": "/lab",
"themesUrl": "/lab/api/themes",
"disableRTC": false,
- "terminalsAvailable": "false",
+ "terminalsAvailable": "true",
"mathjaxUrl": "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js",
"mathjaxConfig": "TeX-AMS_CHTML-full,Safe"
}
diff --git a/packages/react/src/components/codemirror/CodeMirrorDatalayerEditor.tsx b/packages/react/src/components/codemirror/CodeMirrorEditor.tsx
similarity index 94%
rename from packages/react/src/components/codemirror/CodeMirrorDatalayerEditor.tsx
rename to packages/react/src/components/codemirror/CodeMirrorEditor.tsx
index 0b4cd9ee4..4bc6b2220 100644
--- a/packages/react/src/components/codemirror/CodeMirrorDatalayerEditor.tsx
+++ b/packages/react/src/components/codemirror/CodeMirrorEditor.tsx
@@ -16,7 +16,7 @@ import useOutputsStore from '../output/OutputState';
import codeMirrorTheme from './CodeMirrorTheme';
import CodeMirrorOutputToolbar from './CodeMirrorOutputToolbar';
-export const CodeMirrorDatalayerEditor = (props: {
+export const CodeMirrorEditor = (props: {
code: string;
codePre?: string;
outputAdapter: OutputAdapter;
@@ -147,4 +147,7 @@ export const CodeMirrorDatalayerEditor = (props: {
);
};
-export default CodeMirrorDatalayerEditor;
+// Deprecated: Use CodeMirrorEditor instead of CodeMirrorDatalayerEditor
+export const CodeMirrorDatalayerEditor = CodeMirrorEditor;
+
+export default CodeMirrorEditor;
diff --git a/packages/react/src/components/codemirror/index.ts b/packages/react/src/components/codemirror/index.ts
index cac9c1e19..cffda6317 100644
--- a/packages/react/src/components/codemirror/index.ts
+++ b/packages/react/src/components/codemirror/index.ts
@@ -4,6 +4,7 @@
* MIT License
*/
-export * from './CodeMirrorDatalayerEditor';
+export * from './CodeMirrorEditor';
+export { CodeMirrorDatalayerEditor } from './CodeMirrorEditor';
export * from './CodeMirrorOutputToolbar';
export * from './CodeMirrorTheme';
diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts
index a2668aea5..e33347c41 100644
--- a/packages/react/src/components/index.ts
+++ b/packages/react/src/components/index.ts
@@ -6,7 +6,7 @@
export * from './button';
export * from './cell';
-// export * from './codemirror';
+export * from './codemirror';
export * from './commands';
export * from './console';
export * from './dialog';
diff --git a/packages/react/src/components/lumino/Lumino.tsx b/packages/react/src/components/lumino/Lumino.tsx
index 2421cdfb6..4ef3f8828 100644
--- a/packages/react/src/components/lumino/Lumino.tsx
+++ b/packages/react/src/components/lumino/Lumino.tsx
@@ -17,21 +17,56 @@ export const Lumino = (props: LuminoProps) => {
const ref = useRef(null);
const { children, id, height } = props;
useEffect(() => {
- if (ref && ref.current) {
+ console.log(
+ 'Lumino useEffect - ref.current:',
+ ref.current,
+ 'children:',
+ children,
+ 'children.isAttached:',
+ children?.isAttached
+ );
+ if (ref && ref.current && children) {
try {
- Widget.attach(children, ref.current);
+ // Only attach if not already attached
+ if (!children.isAttached) {
+ console.log('Attaching widget to DOM');
+ Widget.attach(children, ref.current);
+ console.log('Widget attached successfully');
+ console.log('Widget node:', children.node);
+ console.log('Widget node parent:', children.node.parentElement);
+ console.log(
+ 'Container children after attach:',
+ ref.current.children.length
+ );
+ console.log('Widget is visible:', children.isVisible);
+ if (!children.isVisible) {
+ console.log('Making widget visible');
+ children.show();
+ }
+ // Force the widget to update
+ children.update();
+ } else {
+ console.log('Widget already attached');
+ console.log('Widget node:', children.node);
+ console.log('Widget node parent:', children.node.parentElement);
+ // Ensure widget is in the DOM
+ if (!ref.current.contains(children.node)) {
+ console.log('Widget node not in container, re-attaching');
+ Widget.attach(children, ref.current);
+ }
+ }
} catch (e) {
console.warn('Exception while attaching Lumino widget.', e);
}
return () => {
+ console.log('Lumino cleanup - detaching widget');
try {
- if (children.isAttached || children.node.isConnected) {
- children.dispose();
+ if (children && (children.isAttached || children.node.isConnected)) {
+ console.log('Detaching widget from DOM');
Widget.detach(children);
}
} catch (e) {
- // no-op.
- // console.debug('Exception while detaching Lumino widget.', e);
+ console.warn('Exception while detaching Lumino widget.', e);
}
};
}
diff --git a/packages/react/src/components/notebook/Notebook.tsx b/packages/react/src/components/notebook/Notebook.tsx
index c432a7243..86d114b41 100644
--- a/packages/react/src/components/notebook/Notebook.tsx
+++ b/packages/react/src/components/notebook/Notebook.tsx
@@ -6,31 +6,22 @@
import { YNotebook } from '@jupyter/ydoc';
import { Cell, ICellModel } from '@jupyterlab/cells';
-import { URLExt } from '@jupyterlab/coreutils';
import { createGlobalStyle } from 'styled-components';
import { INotebookContent } from '@jupyterlab/nbformat';
import { NotebookModel } from '@jupyterlab/notebook';
import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
import { Kernel as JupyterKernel, ServiceManager } from '@jupyterlab/services';
-import { PromiseDelegate } from '@lumino/coreutils';
import { Box } from '@primer/react';
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
-import { WebsocketProvider as YWebsocketProvider } from 'y-websocket';
-import {
- jupyterReactStore,
- KernelTransfer,
- OnSessionConnection,
-} from '../../state';
-import { newUuid, sleep } from '../../utils';
+import { KernelTransfer, OnSessionConnection } from '../../state';
+import { newUuid } from '../../utils';
import { asObservable, Lumino } from '../lumino';
import {
- COLLABORATION_ROOM_URL_PATH,
- requestDatalayerollaborationSessionId,
ICollaborationProvider,
+ CollaborationStatus,
Kernel,
Lite,
- requestJupyterCollaborationSession,
useJupyter,
} from './../../jupyter';
import { CellMetadataEditor } from './cell/metadata';
@@ -40,7 +31,7 @@ import { INotebookToolbarProps } from './toolbar';
import { Loader } from '../utils';
import './Notebook.css';
-import { DatalayerNotebookExtension } from './NotebookExtensions';
+import { NotebookExtension } from './NotebookExtensions';
export type ExternalIPyWidgets = {
name: string;
@@ -64,8 +55,8 @@ export type INotebookProps = {
Toolbar?: (props: INotebookToolbarProps) => JSX.Element;
cellMetadataPanel?: boolean;
cellSidebarMargin?: number;
- collaborative?: ICollaborationProvider;
- extensions?: DatalayerNotebookExtension[];
+ collaborationProvider?: ICollaborationProvider;
+ extensions?: NotebookExtension[];
height?: string;
id: string;
kernel?: Kernel;
@@ -111,7 +102,7 @@ export const Notebook = (props: INotebookProps) => {
});
const {
Toolbar,
- collaborative,
+ collaborationProvider: collaborationProviderProp,
extensions,
height,
maxHeight,
@@ -123,6 +114,7 @@ export const Notebook = (props: INotebookProps) => {
} = props;
const [id, _] = useState(props.id || newUuid());
const [adapter, setAdapter] = useState();
+ const [isLoading, setIsLoading] = useState(false);
const [extensionComponents, setExtensionComponents] = useState(
new Array()
);
@@ -130,7 +122,14 @@ export const Notebook = (props: INotebookProps) => {
const notebookStore = useNotebookStore();
const portals = notebookStore.selectNotebookPortals(id);
- const [isLoading, setIsLoading] = useState(false);
+ console.log(
+ 'Notebook render - adapter:',
+ adapter,
+ 'isLoading:',
+ isLoading,
+ 'adapter.panel:',
+ adapter?.panel
+ );
// Bootstrap the Notebook Adapter.
const bootstrapAdapter = async (
@@ -138,6 +137,7 @@ export const Notebook = (props: INotebookProps) => {
serviceManager?: ServiceManager.IManager,
kernel?: Kernel
) => {
+ console.log('bootstrapAdapter called with kernel:', kernel);
const adapter = new NotebookAdapter({
...props,
id,
@@ -145,6 +145,7 @@ export const Notebook = (props: INotebookProps) => {
kernel,
serviceManager,
});
+ console.log('Created adapter:', adapter);
// Update the local state.
setAdapter(adapter);
extensions!.forEach(extension => {
@@ -242,151 +243,129 @@ export const Notebook = (props: INotebookProps) => {
}, [serviceManager, kernel]);
useEffect(() => {
- // As the server has the content source of truth, we
- // must ensure that the shared model is pristine before
- // to connect to the server. More over we should ensure,
- // the connection is disposed in case the server document is
- // reset for any reason while the client is still alive.
- let provider: YWebsocketProvider | null = null;
- let ready = new PromiseDelegate();
+ // Set up collaboration using the new provider system
+ let collaborationProvider: ICollaborationProvider | null = null;
let isMounted = true;
let sharedModel: YNotebook | null = null;
- const onConnectionClose = (event: any) => {
- if (event.code > 1000) {
- console.error(
- 'Connection with the document has been closed unexpectedly.',
- event
- );
-
- provider?.disconnect();
+ const connect = async () => {
+ if (!adapter?.notebookPanel || !isMounted) {
+ return;
+ }
- // If sessionId has expired - reset the client model
- if (event.code === 4002) {
- provider?.destroy();
- ready.reject('Connection closed.');
- ready = new PromiseDelegate();
- if (isMounted) {
- setIsLoading(true);
- Promise.all([connect(), ready.promise, sleep(500)])
- .catch(error => {
- console.error(
- 'Failed to setup collaboration connection.',
- error
- );
- })
- .finally(() => {
- if (isMounted) {
- setIsLoading(false);
- }
- });
- }
- }
- // FIXME inform the user.
+ // Use the provided collaboration provider
+ if (collaborationProviderProp) {
+ collaborationProvider = collaborationProviderProp;
}
- };
- const onSync = (isSynced: boolean) => {
- if (isSynced) {
- provider?.off('sync', onSync);
- ready.resolve(void 0);
+ if (!collaborationProvider) {
+ return;
}
- };
- const connect = async () => {
- if (adapter?.notebookPanel && isMounted) {
+ try {
sharedModel = new YNotebook();
- const { ydoc, awareness } = sharedModel;
- // Setup Collaboration.
- if (collaborative == 'jupyter') {
- const token =
- jupyterReactStore.getState().jupyterConfig?.jupyterServerToken;
- const session = await requestJupyterCollaborationSession(
- 'json',
- 'notebook',
- path!
- );
- const wsUrl = serviceManager?.serverSettings.wsUrl;
- if (!wsUrl) {
- throw new Error('WebSocket URL is not available');
+
+ // Set up event handlers
+ const handleStatusChange = (
+ _: ICollaborationProvider,
+ status: CollaborationStatus
+ ) => {
+ if (
+ status === CollaborationStatus.Connected &&
+ adapter?.notebookPanel
+ ) {
+ // Create a new model using the synchronized shared model
+ const model = new NotebookModel({
+ collaborationEnabled: true,
+ disableDocumentWideUndoRedo: true,
+ sharedModel: sharedModel!,
+ });
+
+ // Store the old model for disposal
+ const oldModel = adapter.notebookPanel.content.model;
+
+ // Safely update the model
+ try {
+ // Update the model without triggering widget reattachment
+ adapter.notebookPanel.content.model = model;
+
+ // Update the notebook store with the new model
+ notebookStore.changeModel({ id, notebookModel: model });
+
+ // Force the notebook panel to update its content
+ adapter.notebookPanel.update();
+
+ // Dispose the old model after successful update
+ if (oldModel && oldModel !== model) {
+ oldModel.dispose();
+ }
+
+ console.log(
+ 'Notebook model updated with collaboration. Cell count:',
+ model.cells?.length
+ );
+ } catch (error) {
+ console.error('Error updating notebook model:', error);
+ // Restore the old model if update fails
+ if (oldModel && !oldModel.isDisposed) {
+ adapter.notebookPanel.content.model = oldModel;
+ }
+ }
}
- const documentURL = URLExt.join(wsUrl, COLLABORATION_ROOM_URL_PATH);
- const documentName = `${session.format}:${session.type}:${session.fileId}`;
- provider = new YWebsocketProvider(documentURL, documentName, ydoc, {
- disableBc: true,
- params: {
- sessionId: session.sessionId,
- token: token!,
- },
- awareness,
- });
- } else if (collaborative == 'datalayer') {
- const { runUrl, token } =
- jupyterReactStore.getState().datalayerConfig ?? {};
- const documentName = id;
- const documentURL = URLExt.join(runUrl!, `/api/spacer/v1/documents`);
- const sessionId = await requestDatalayerollaborationSessionId({
- url: URLExt.join(documentURL, documentName),
- token,
- });
- provider = new YWebsocketProvider(
- documentURL.replace(/^http/, 'ws'),
- documentName,
- ydoc,
- {
- disableBc: true,
- params: {
- sessionId,
- token: token!,
- },
- awareness,
+ };
+
+ const handleError = (_: ICollaborationProvider, error: Error) => {
+ console.error('Collaboration error:', error);
+ // Handle collaboration errors
+ if (error.message.includes('session expired')) {
+ // Attempt to reconnect
+ if (isMounted && collaborationProvider && sharedModel) {
+ collaborationProvider
+ .connect(sharedModel, id)
+ .catch(console.error);
}
- );
- }
- if (provider) {
- provider.on('sync', onSync);
- provider.on('connection-close', onConnectionClose);
- console.log('Collaboration is setup with websocket provider.');
- // Create a new model using the one synchronize with the collaboration document
- const model = new NotebookModel({
- collaborationEnabled: true,
- disableDocumentWideUndoRedo: true,
- sharedModel,
- });
- const oldModel = adapter.notebookPanel.content.model;
- adapter.notebookPanel.content.model = model;
- // We must dispose the old model after setting the new one.
- oldModel?.dispose();
- }
+ }
+ };
+
+ collaborationProvider.events.statusChanged.connect(handleStatusChange);
+ collaborationProvider.events.errorOccurred.connect(handleError);
+
+ // Connect to collaboration service
+ await collaborationProvider.connect(sharedModel, id, {
+ serviceManager,
+ path: props.path, // Pass the notebook's path to the collaboration provider
+ });
+
+ console.log(
+ 'Collaboration is setup with provider:',
+ collaborationProvider.type
+ );
+ } catch (error) {
+ console.error('Failed to setup collaboration:', error);
+ setIsLoading(false);
}
};
- if (collaborative) {
- setIsLoading(true);
- Promise.all([connect(), ready.promise, sleep(500)])
+ if (collaborationProviderProp) {
+ // Don't set isLoading to true here as it causes the Lumino widget to unmount
+ // setIsLoading(true);
+ connect()
.catch(error => {
console.error('Failed to setup collaboration connection.', error);
})
.finally(() => {
if (isMounted) {
- setIsLoading(false);
+ // setIsLoading(false);
}
});
}
return () => {
isMounted = false;
- if (provider) {
- (provider.synced ? Promise.resolve() : ready.promise).finally(() => {
- provider?.off('sync', onSync);
- provider?.off('connection-close', onConnectionClose);
- provider?.disconnect();
- provider?.destroy();
- });
- }
+ collaborationProvider?.dispose();
sharedModel?.dispose();
};
- }, [adapter?.notebookPanel, collaborative]);
+ }, [adapter?.notebookPanel, collaborationProviderProp]);
useEffect(() => {
if (adapter && adapter.kernel !== kernel) {
@@ -495,7 +474,13 @@ export const Notebook = (props: INotebookProps) => {
{isLoading ? (
) : (
- {adapter && {adapter.panel}}
+
+ {adapter ? (
+ {adapter.panel}
+ ) : (
+ No adapter available
+ )}
+
)}
diff --git a/packages/react/src/components/notebook/Notebook2.tsx b/packages/react/src/components/notebook/Notebook2.tsx
index 49807daef..0b8402027 100644
--- a/packages/react/src/components/notebook/Notebook2.tsx
+++ b/packages/react/src/components/notebook/Notebook2.tsx
@@ -15,11 +15,11 @@ import { Box } from '@primer/react';
import type { OnSessionConnection } from '../../state';
import { Loader } from '../utils';
import { useKernelId, useNotebookModel, Notebook2Base } from './Notebook2Base';
-import type { DatalayerNotebookExtension } from './NotebookExtensions';
+import type { NotebookExtension } from './NotebookExtensions';
import type { INotebookToolbarProps } from './toolbar';
import './Notebook.css';
-import { ICollaborationServer } from '../../jupyter';
+import { ICollaborationProvider } from '../../jupyter';
const GlobalStyle = createGlobalStyle`
.dla-Box-Notebook .jp-Cell .dla-CellSidebar-Container {
@@ -35,9 +35,9 @@ const GlobalStyle = createGlobalStyle`
*/
export interface INotebook2Props {
/**
- * Collaboration server providing the document documents.
+ * Collaboration provider instance.
*/
- collaborationServer?: ICollaborationServer;
+ collaborationProvider?: ICollaborationProvider;
/**
* Custom command registry.
*
@@ -48,7 +48,7 @@ export interface INotebook2Props {
/**
* Notebook extensions.
*/
- extensions?: DatalayerNotebookExtension[];
+ extensions?: NotebookExtension[];
/**
* Notebook ID.
*/
@@ -127,7 +127,7 @@ export function Notebook2(
Toolbar,
children,
cellSidebarMargin = 120,
- collaborationServer,
+ collaborationProvider,
commands,
extensions,
height = '100vh',
@@ -153,10 +153,13 @@ export function Notebook2(
});
const model = useNotebookModel({
- collaborationServer,
+ collaborationProvider,
nbformat,
readonly,
url,
+ path,
+ serviceManager,
+ id,
});
useEffect(() => {
@@ -169,7 +172,7 @@ export function Notebook2(
useEffect(() => {
// Set user identity if collaborating using Jupyter collaboration
const setUserIdentity = () => {
- if (collaborationServer?.type === 'jupyter' && model) {
+ if (collaborationProvider && model) {
// Yjs details are hidden from the interface
(model.sharedModel as any).awareness.setLocalStateField(
'user',
@@ -182,7 +185,7 @@ export function Notebook2(
return () => {
serviceManager.user.userChanged.disconnect(setUserIdentity);
};
- }, [collaborationServer, model, serviceManager]);
+ }, [collaborationProvider, model, serviceManager]);
return isLoading ? (
diff --git a/packages/react/src/components/notebook/Notebook2Base.tsx b/packages/react/src/components/notebook/Notebook2Base.tsx
index 6c0f1937e..1521401ea 100644
--- a/packages/react/src/components/notebook/Notebook2Base.tsx
+++ b/packages/react/src/components/notebook/Notebook2Base.tsx
@@ -23,7 +23,7 @@ import {
KernelCompleterProvider,
ProviderReconciliator,
} from '@jupyterlab/completer';
-import { PathExt, URLExt, type IChangedArgs } from '@jupyterlab/coreutils';
+import { PathExt, type IChangedArgs } from '@jupyterlab/coreutils';
import { Context, type DocumentRegistry } from '@jupyterlab/docregistry';
import { rendererFactory as javascriptRendererFactory } from '@jupyterlab/javascript-extension';
import { rendererFactory as jsonRendererFactory } from '@jupyterlab/json-extension';
@@ -56,19 +56,14 @@ import type { ISessionConnection } from '@jupyterlab/services/lib/session/sessio
import { YNotebook, type ISharedNotebook, type IYText } from '@jupyter/ydoc';
import { find } from '@lumino/algorithm';
import { CommandRegistry } from '@lumino/commands';
-import { PromiseDelegate } from '@lumino/coreutils';
import { DisposableSet } from '@lumino/disposable';
import { Signal } from '@lumino/signaling';
import { Widget } from '@lumino/widgets';
import { Box } from '@primer/react';
import { Banner } from '@primer/react/experimental';
import { EditorView } from 'codemirror';
-import { WebsocketProvider } from 'y-websocket';
import {
- COLLABORATION_ROOM_URL_PATH,
- ICollaborationServer,
- requestDatalayerollaborationSessionId,
- requestJupyterCollaborationSession,
+ ICollaborationProvider,
WIDGET_MIMETYPE,
WidgetLabRenderer,
WidgetManager,
@@ -77,12 +72,12 @@ import type { OnSessionConnection } from '../../state';
import { newUuid, remoteUserCursors } from '../../utils';
import { Lumino } from '../lumino';
import { Loader } from '../utils';
-import type { DatalayerNotebookExtension } from './NotebookExtensions';
+import type { NotebookExtension } from './NotebookExtensions';
import { addNotebookCommands } from './NotebookCommands';
const COMPLETER_TIMEOUT_MILLISECONDS = 1000;
-const DEFAULT_EXTENSIONS = new Array();
+const DEFAULT_EXTENSIONS = new Array();
const FALLBACK_NOTEBOOK_PATH = '.datalayer/ping.ipynb';
@@ -104,7 +99,7 @@ export interface INotebook2BaseProps {
/**
* Notebook extensions
*/
- extensions?: DatalayerNotebookExtension[];
+ extensions?: NotebookExtension[];
/**
* Kernel ID to connect to
*/
@@ -596,9 +591,9 @@ export function useKernelId(
type IOptions = {
/**
- * Collaboration server providing the documents
+ * Collaboration provider for the notebook.
*/
- collaborationServer?: ICollaborationServer;
+ collaborationProvider?: ICollaborationProvider;
/**
* Notebook content.
*/
@@ -613,6 +608,18 @@ type IOptions = {
* URL to fetch the notebook content from.
*/
url?: string;
+ /**
+ * Path to the notebook file.
+ */
+ path?: string;
+ /**
+ * Service manager.
+ */
+ serviceManager?: ServiceManager.IManager;
+ /**
+ * Notebook ID.
+ */
+ id?: string;
};
/**
@@ -621,10 +628,18 @@ type IOptions = {
* The notebook content may come from 3 sources:
* - {@link nbformat}: The notebook content
* - {@link url}: A URL to fetch the notebook content from
- * - {@link collaborationServer}: Parameters to connect to a collaboration server
+ * - {@link collaborationProvider}: A collaboration provider for real-time editing
*/
export function useNotebookModel(options: IOptions): NotebookModel | null {
- const { collaborationServer, nbformat, readonly = false, url } = options;
+ const {
+ collaborationProvider,
+ nbformat,
+ readonly = false,
+ url,
+ path,
+ serviceManager,
+ id,
+ } = options;
// Generate the notebook model
// There are three posibilities (by priority order):
@@ -637,114 +652,19 @@ export function useNotebookModel(options: IOptions): NotebookModel | null {
let isMounted = true;
const disposable = new DisposableSet();
- if (collaborationServer) {
- // As the server has the content source of thruth, we
- // must ensure that the shared model is pristine before
- // to connect to the server. More over we should ensure,
- // the connection is disposed in case the server document is
- // reset for any reason while the client is still alive.
- let provider: WebsocketProvider | null = null;
- let ready = new PromiseDelegate();
- const isMounted = true;
- let sharedModel: YNotebook | null = null;
-
- const onConnectionClose = (event: any) => {
- if (event.code > 1000) {
- console.error(
- 'Connection with the document has been closed unexpectedly.',
- event
- );
-
- provider?.disconnect();
-
- // If sessionId has expired - reset the client model
- if (event.code === 4002) {
- disposable.clear();
- provider?.destroy();
- if (isMounted) {
- connect().catch(error => {
- console.error(
- 'Failed to setup collaboration connection.',
- error
- );
- });
- }
- }
- // FIXME inform the user.
- }
- };
-
- const onSync = (isSynced: boolean) => {
- if (isSynced) {
- provider?.off('sync', onSync);
- ready.resolve(void 0);
- }
- };
+ // Handle new collaboration provider
+ if (collaborationProvider && serviceManager && id) {
+ const setupCollaboration = async () => {
+ try {
+ const sharedModel = new YNotebook();
- const connect = async () => {
- ready.reject('Connection closed.');
- ready = new PromiseDelegate();
-
- sharedModel = new YNotebook();
- const { ydoc, awareness } = sharedModel;
- let documentURL = '';
- let documentName = '';
- const params: Record = {};
-
- // Setup Collaboration.
- if (collaborationServer.type === 'jupyter') {
- const { path, serverSettings } = collaborationServer;
- const session = await requestJupyterCollaborationSession(
- 'json',
- 'notebook',
+ // Connect to the collaboration provider
+ await collaborationProvider.connect(sharedModel, id, {
+ serviceManager,
path,
- serverSettings
- );
- documentURL = URLExt.join(
- serverSettings.wsUrl,
- COLLABORATION_ROOM_URL_PATH
- );
- documentName = `${session.format}:${session.type}:${session.fileId}`;
- params.sessionId = session.sessionId;
- if (serverSettings.token) {
- params.token = serverSettings.token;
- }
- } else if (collaborationServer.type === 'datalayer') {
- const {
- baseURL,
- documentName: documentName_,
- token,
- } = collaborationServer;
- documentName = documentName_; // Set non local variable.
- const serverURL = URLExt.join(baseURL, '/api/spacer/v1/documents');
- documentURL = serverURL.replace(/^http/, 'ws');
- params.sessionId = await requestDatalayerollaborationSessionId({
- url: URLExt.join(serverURL, documentName),
- token,
});
- params.token = token;
- }
-
- if (params.sessionId) {
- provider = new WebsocketProvider(documentURL, documentName, ydoc, {
- disableBc: true,
- params,
- awareness,
- });
- provider.on('sync', onSync);
- provider.on('connection-close', onConnectionClose);
- console.log('Collaboration is setup with websocket provider.');
-
- await ready.promise;
- const dispose = () => {
- provider?.off('sync', onSync);
- provider?.off('connection-close', onConnectionClose);
- provider?.disconnect();
- provider?.destroy();
- };
if (isMounted) {
- // Create a new model using the one synchronized with the collaboration document.
const model = new NotebookModel({
collaborationEnabled: true,
disableDocumentWideUndoRedo: true,
@@ -753,16 +673,21 @@ export function useNotebookModel(options: IOptions): NotebookModel | null {
model.readOnly = readonly;
setModel(model);
- disposable.add(Object.freeze({ dispose, isDisposed: false }));
- } else {
- dispose();
+ disposable.add({
+ dispose: () => {
+ collaborationProvider.disconnect();
+ },
+ get isDisposed() {
+ return false;
+ },
+ });
}
+ } catch (error) {
+ console.error('Failed to setup collaboration:', error);
}
};
- connect().catch(error => {
- console.error('Failed to setup collaboration connection.', error);
- });
+ setupCollaboration();
} else {
const createModel = (nbformat: INotebookContent | undefined) => {
const model = new NotebookModel();
@@ -791,7 +716,15 @@ export function useNotebookModel(options: IOptions): NotebookModel | null {
isMounted = false;
disposable.dispose();
};
- }, [collaborationServer, nbformat, readonly, url]);
+ }, [
+ collaborationProvider,
+ nbformat,
+ readonly,
+ url,
+ path,
+ serviceManager,
+ id,
+ ]);
return model;
}
diff --git a/packages/react/src/components/notebook/NotebookExtensions.ts b/packages/react/src/components/notebook/NotebookExtensions.ts
index b4ec37b3a..7dc918fa8 100644
--- a/packages/react/src/components/notebook/NotebookExtensions.ts
+++ b/packages/react/src/components/notebook/NotebookExtensions.ts
@@ -9,17 +9,17 @@ import { INotebookModel, NotebookPanel } from '@jupyterlab/notebook';
import { CommandRegistry } from '@lumino/commands';
import { NotebookAdapter } from './NotebookAdapter';
-export type IDatalayerNotebookExtensionProps = {
+export type INotebookExtensionProps = {
notebookId: string;
commands: CommandRegistry;
panel: NotebookPanel;
adapter?: NotebookAdapter;
};
-export type DatalayerNotebookExtension = DocumentRegistry.IWidgetExtension<
+export type NotebookExtension = DocumentRegistry.IWidgetExtension<
NotebookPanel,
INotebookModel
> & {
- init(props: IDatalayerNotebookExtensionProps): void;
+ init(props: INotebookExtensionProps): void;
get component(): JSX.Element | null;
};
diff --git a/packages/react/src/components/notebook/cell/sidebar/CellSidebarExtension.tsx b/packages/react/src/components/notebook/cell/sidebar/CellSidebarExtension.tsx
index ee49b8b94..7e1c8e620 100644
--- a/packages/react/src/components/notebook/cell/sidebar/CellSidebarExtension.tsx
+++ b/packages/react/src/components/notebook/cell/sidebar/CellSidebarExtension.tsx
@@ -16,8 +16,8 @@ import { Signal } from '@lumino/signaling';
import { JupyterReactTheme } from '../../../../theme';
import { CellSidebar, type ICellSidebarProps } from './CellSidebar';
import {
- DatalayerNotebookExtension,
- IDatalayerNotebookExtensionProps,
+ NotebookExtension,
+ INotebookExtensionProps,
} from '../../NotebookExtensions';
class CellSidebarFactory implements IDisposable {
@@ -130,7 +130,7 @@ type ICellSidebarExtensionOptions = {
/**
* Cell sidebar extension for notebook panels.
*/
-export class CellSidebarExtension implements DatalayerNotebookExtension {
+export class CellSidebarExtension implements NotebookExtension {
protected factory: React.JSXElementConstructor;
protected commands?: CommandRegistry;
protected nbgrader?: boolean;
@@ -165,7 +165,7 @@ export class CellSidebarExtension implements DatalayerNotebookExtension {
return sidebar;
}
- init(props: IDatalayerNotebookExtensionProps): void {
+ init(props: INotebookExtensionProps): void {
this.commands = props.commands;
}
}
diff --git a/packages/react/src/components/output/Output.tsx b/packages/react/src/components/output/Output.tsx
index 2fa08dd5b..c25696bc6 100644
--- a/packages/react/src/components/output/Output.tsx
+++ b/packages/react/src/components/output/Output.tsx
@@ -14,13 +14,12 @@ import { useJupyter } from '../../jupyter/JupyterContext';
import { IExecutionPhaseOutput, Kernel } from '../../jupyter/kernel';
import { newUuid } from '../../utils';
import { KernelActionMenu, KernelProgressBar } from '../kernel';
-// import { CodeMirrorDatalayerEditor } from '../codemirror';
+import { CodeMirrorEditor } from '../codemirror';
import { OutputAdapter } from './OutputAdapter';
import { OutputRenderer } from './OutputRenderer';
import { useOutputsStore } from './OutputState';
import './Output.css';
-import { CodeMirrorDatalayerEditor } from '../codemirror';
export type IOutputProps = {
adapter?: OutputAdapter;
@@ -184,7 +183,7 @@ export const Output = (props: IOutputProps) => {
},
}}
>
- {
serviceManager={serviceManager}
height="calc(100vh - 2.6rem)" // (Height - Toolbar Height).
extensions={extensions}
- /*
- collaborationServer={{
- baseURL: 'https://prod1.datalayer.run',
- token: '',
- documentName: '',
- type: 'datalayer'
- }}
- */
/>
) : (
diff --git a/packages/react/src/examples/Notebook2Collaborative.tsx b/packages/react/src/examples/Notebook2Collaborative.tsx
new file mode 100644
index 000000000..7d68899ee
--- /dev/null
+++ b/packages/react/src/examples/Notebook2Collaborative.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2021-2023 Datalayer, Inc.
+ *
+ * MIT License
+ */
+
+import { useMemo } from 'react';
+import { createRoot } from 'react-dom/client';
+import { JupyterReactTheme } from '../theme/JupyterReactTheme';
+import { useJupyter, JupyterCollaborationProvider } from '../jupyter';
+import {
+ Notebook2,
+ CellSidebarExtension,
+ CellSidebarButton,
+} from '../components';
+
+const Notebook2Collaborative = () => {
+ const { serviceManager } = useJupyter();
+ const extensions = useMemo(
+ () => [new CellSidebarExtension({ factory: CellSidebarButton })],
+ []
+ );
+
+ const collaborationProvider = useMemo(
+ () => new JupyterCollaborationProvider(),
+ []
+ );
+
+ return serviceManager ? (
+
+
+
+ ) : (
+ <>>
+ );
+};
+
+const div = document.createElement('div');
+document.body.appendChild(div);
+const root = createRoot(div);
+
+root.render();
diff --git a/packages/react/src/examples/NotebookCollaborative.tsx b/packages/react/src/examples/NotebookCollaborative.tsx
index ac87c0f34..857efdd0d 100644
--- a/packages/react/src/examples/NotebookCollaborative.tsx
+++ b/packages/react/src/examples/NotebookCollaborative.tsx
@@ -11,16 +11,24 @@ import { CellSidebarButton } from '../components/notebook/cell/sidebar/CellSideb
import { Notebook } from '../components/notebook/Notebook';
import { JupyterReactTheme } from '../theme/JupyterReactTheme';
import { NotebookToolbar } from './../components/notebook/toolbar/NotebookToolbar';
+import { JupyterCollaborationProvider } from '../jupyter/collaboration/providers/JupyterCollaborationProvider';
const NotebookCollaborative = () => {
const extensions = useMemo(
() => [new CellSidebarExtension({ factory: CellSidebarButton })],
[]
);
+
+ // Create a Jupyter collaboration provider
+ const collaborationProvider = useMemo(
+ () => new JupyterCollaborationProvider(),
+ []
+ );
+
return (
{
- const [index, setIndex] = useState(0);
- const [nbformat, setNbformat] = useState(nbformatExample as INotebookContent);
- const [readonly, setReadonly] = useState(true);
- const [serverless, setServerless] = useState(true);
- const [kernelIndex, setKernelIndex] = useState(-1);
- const [waiting, setWaiting] = useState(false);
- const [lite, setLite] = useState(false);
- const [serviceManager, setServiceManager] =
- useState(SERVICE_MANAGER_LESS);
- const [sessions, setSessions] = useState>(
- []
- );
- const { datalayerConfig } = useJupyterReactStore();
- const notebookStore = useNotebookStore();
- const notebook = notebookStore.selectNotebook(NOTEBOOK_ID);
- const onSessionConnection: OnSessionConnection = (
- session: Session.ISessionConnection | undefined
- ) => {
- console.log('Received a Kernel Sessoin.', session);
- if (session) {
- setSessions(sessions.concat(session));
- }
- };
- const changeIndex = (index: number) => {
- setIndex(index);
- switch (index) {
- case 0: {
- setKernelIndex(-1);
- setNbformat(
- notebook?.adapter?.notebookPanel?.content.model?.toJSON() as INotebookContent
- );
- setServerless(true);
- setReadonly(true);
- setLite(false);
- setServiceManager(SERVICE_MANAGER_LESS);
- break;
- }
- case 1: {
- setJupyterServerUrl(location.protocol + '//' + location.host);
- createLiteServiceManager().then(liteServiceManager => {
- setKernelIndex(-1);
- console.log('Lite Service Manager is available', liteServiceManager);
- setServiceManager(liteServiceManager);
- setNbformat(
- notebook?.adapter?.notebookPanel?.content.model?.toJSON() as INotebookContent
- );
- setServerless(false);
- setReadonly(false);
- setLite(true);
- });
- break;
- }
- case 2: {
- setJupyterServerUrl(DEFAULT_JUPYTER_SERVER_URL);
- setKernelIndex(-1);
- setNbformat(
- notebook?.adapter?.notebookPanel?.content.model?.toJSON() as INotebookContent
- );
- setServerless(false);
- setReadonly(false);
- setLite(false);
- const serverSettings = createServerSettings(
- getJupyterServerUrl(),
- getJupyterServerToken()
- );
- const serviceManager = new ServiceManager({ serverSettings });
- (serviceManager as any)['__NAME__'] = 'MutatingServiceManager';
- setServiceManager(serviceManager);
- break;
- }
- case 3: {
- // setWaiting(true);
- setLite(false);
- createDatalayerServiceManager(
- datalayerConfig?.cpuEnvironment || 'python-simple-env',
- datalayerConfig?.credits || 1
- ).then(serviceManager => {
- (serviceManager as any)['__NAME__'] = 'DatalayerCPUServiceManager';
- setServiceManager(serviceManager);
- setServerless(false);
- setReadonly(false);
- setKernelIndex(0);
- setNbformat(
- notebook?.adapter?.notebookPanel?.content.model?.toJSON() as INotebookContent
- );
- // setWaiting(false);
- });
- break;
- }
- case 4: {
- setWaiting(true);
- setLite(false);
- createDatalayerServiceManager(
- datalayerConfig?.gpuEnvironment || 'pytorch-cuda-env',
- datalayerConfig?.credits || 1
- ).then(serviceManager => {
- setKernelIndex(0);
- (serviceManager as any)['__NAME__'] = 'DatalayerGPUServiceManager';
- setServiceManager(serviceManager);
- setNbformat(
- notebook?.adapter?.notebookPanel?.content.model?.toJSON() as INotebookContent
- );
- setServerless(false);
- setReadonly(false);
- setWaiting(false);
- });
- break;
- }
- }
- };
- return (
-
- <>
-
-
- changeIndex(index)}
- aria-label="jupyter-react-example"
- >
-
- Readonly
-
-
- Browser Kernel
-
-
- OSS Kernel (CPU)
-
-
- Kernel (CPU)
-
-
- Kernel (GPU)
-
-
-
-
- {/*
-
-
- */}
-
-
-
-
-
-
-
-
- Kernel Sessions
-
-
- {sessions.map(session => {
- return (
-
-
- {session.name} {session.id} clientId [
- {session.kernel?.clientId}) - id {session.kernel?.id}
-
-
- );
- })}
-
- {waiting ? (
-
- ) : (
-
- )}
- >
-
- );
-};
-
-const div = document.createElement('div');
-document.body.appendChild(div);
-const root = createRoot(div);
-
-root.render();
diff --git a/packages/react/src/examples/NotebookMutationsServiceManager.tsx b/packages/react/src/examples/NotebookMutationsServiceManager.tsx
deleted file mode 100644
index f3d97b30d..000000000
--- a/packages/react/src/examples/NotebookMutationsServiceManager.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Copyright (c) 2021-2023 Datalayer, Inc.
- *
- * MIT License
- */
-
-import { useState } from 'react';
-import { createRoot } from 'react-dom/client';
-import { Box, SegmentedControl, Label, Text } from '@primer/react';
-import { INotebookContent } from '@jupyterlab/nbformat';
-import { Session, ServiceManager } from '@jupyterlab/services';
-import {
- createLiteServiceManager,
- createServerSettings,
- setJupyterServerUrl,
- getJupyterServerUrl,
- getJupyterServerToken,
- ServiceManagerLess,
- loadJupyterConfig,
- DEFAULT_JUPYTER_SERVER_URL,
- Lite,
-} from '../jupyter';
-import { useJupyterReactStore, OnSessionConnection } from '../state';
-import { useNotebookStore, Notebook, SpinnerCentered } from './../components';
-import { JupyterReactTheme } from '../theme';
-import { createDatalayerServiceManager } from './../providers';
-
-import nbformatExample from './notebooks/NotebookExample1.ipynb.json';
-
-const NOTEBOOK_ID = 'notebook-mutations-id';
-
-loadJupyterConfig();
-
-const SERVICE_MANAGER_LESS = new ServiceManagerLess();
-
-const NotebookMutationsServiceManager = () => {
- const [index, setIndex] = useState(0);
- const [nbformat, setNbformat] = useState(nbformatExample as INotebookContent);
- const [readonly, setReadonly] = useState(true);
- const [serverless, setServerless] = useState(true);
- const [startDefaultKernel, setStartDefaultKernel] = useState(false);
- const [kernelIndex, setKernelIndex] = useState(-1);
- const [waiting, setWaiting] = useState(false);
- const [lite, setLite] = useState(false);
- const [serviceManager, setServiceManager] =
- useState(SERVICE_MANAGER_LESS);
- const [sessions, setSessions] = useState>(
- []
- );
- const { datalayerConfig } = useJupyterReactStore();
- const notebookStore = useNotebookStore();
- const notebook = notebookStore.selectNotebook(NOTEBOOK_ID);
- const onSessionConnection: OnSessionConnection = (
- session: Session.ISessionConnection | undefined
- ) => {
- console.log('Received a Kernel Session.', session);
- if (session) {
- setSessions(sessions.concat(session));
- }
- };
- const changeIndex = (index: number) => {
- setIndex(index);
- switch (index) {
- case 0: {
- setKernelIndex(-1);
- setNbformat(
- notebook?.adapter?.notebookPanel?.content.model?.toJSON() as INotebookContent
- );
- setServerless(true);
- setReadonly(true);
- setLite(false);
- setStartDefaultKernel(false);
- setServiceManager(SERVICE_MANAGER_LESS);
- break;
- }
- case 1: {
- setJupyterServerUrl(location.protocol + '//' + location.host);
- createLiteServiceManager().then(liteServiceManager => {
- console.log('Lite Service Manager is available', liteServiceManager);
- setKernelIndex(-1);
- setNbformat(
- notebook?.adapter?.notebookPanel?.content.model?.toJSON() as INotebookContent
- );
- setServerless(false);
- setReadonly(false);
- setLite(true);
- setStartDefaultKernel(true);
- setServiceManager(liteServiceManager);
- });
- break;
- }
- case 2: {
- setJupyterServerUrl(DEFAULT_JUPYTER_SERVER_URL);
- setKernelIndex(-1);
- setNbformat(
- notebook?.adapter?.notebookPanel?.content.model?.toJSON() as INotebookContent
- );
- setServerless(false);
- setReadonly(false);
- setLite(false);
- setStartDefaultKernel(true);
- const serverSettings = createServerSettings(
- getJupyterServerUrl(),
- getJupyterServerToken()
- );
- const serviceManager = new ServiceManager({ serverSettings });
- (serviceManager as any)['__NAME__'] = 'MutatingServiceManager';
- setServiceManager(serviceManager);
- break;
- }
- case 3: {
- createDatalayerServiceManager(
- datalayerConfig?.cpuEnvironment || 'python-simple-env',
- datalayerConfig?.credits || 1
- ).then(serviceManager => {
- setLite(false);
- setServerless(false);
- setReadonly(false);
- setStartDefaultKernel(false);
- setKernelIndex(0);
- setNbformat(
- notebook?.adapter?.notebookPanel?.content.model?.toJSON() as INotebookContent
- );
- (serviceManager as any)['__NAME__'] = 'DatalayerCPUServiceManager';
- setServiceManager(serviceManager);
- // setWaiting(false);
- });
- break;
- }
- case 4: {
- setWaiting(true);
- setLite(false);
- createDatalayerServiceManager(
- datalayerConfig?.gpuEnvironment || 'pytorch-cuda-env',
- datalayerConfig?.credits || 1
- ).then(serviceManager => {
- setNbformat(
- notebook?.adapter?.notebookPanel?.content.model?.toJSON() as INotebookContent
- );
- setServerless(false);
- setReadonly(false);
- setStartDefaultKernel(false);
- setWaiting(false);
- setKernelIndex(0);
- (serviceManager as any)['__NAME__'] = 'DatalayerGPUServiceManager';
- setServiceManager(serviceManager);
- });
- break;
- }
- }
- };
- return (
-
- <>
-
-
- changeIndex(index)}
- aria-label="jupyter-react-example"
- >
-
- Readonly
-
-
- Browser Kernel
-
-
- OSS Kernel (CPU)
-
-
- Kernel (CPU)
-
-
- Kernel (GPU)
-
-
-
-
- {/*
-
-
- */}
-
-
-
-
-
-
-
-
- Kernel Sessions
-
-
- {sessions.map(session => {
- return (
-
-
- {session.name} {session.id} clientId{' '}
- {session.kernel?.clientId} - id {session.kernel?.id}
-
-
- );
- })}
-
- {waiting ? (
-
- ) : (
-
- )}
- >
-
- );
-};
-
-const div = document.createElement('div');
-document.body.appendChild(div);
-const root = createRoot(div);
-
-root.render();
diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbar.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbar.tsx
index 2c71015f4..668eef730 100644
--- a/packages/react/src/examples/extensions/celltoolbar/CellToolbar.tsx
+++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbar.tsx
@@ -6,16 +6,16 @@
import { ReactWidget } from '@jupyterlab/apputils';
import { CodeCell } from '@jupyterlab/cells';
-import { IDatalayerNotebookExtensionProps } from '../../../components';
+import { INotebookExtensionProps } 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;
+ private _props: INotebookExtensionProps;
- constructor(cell: CodeCell, props: IDatalayerNotebookExtensionProps) {
+ constructor(cell: CodeCell, props: INotebookExtensionProps) {
super();
this._cell = cell;
this._props = props;
diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbarComponent.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbarComponent.tsx
index 7c6e5b6c6..9dfb15486 100644
--- a/packages/react/src/examples/extensions/celltoolbar/CellToolbarComponent.tsx
+++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbarComponent.tsx
@@ -13,14 +13,11 @@ import {
SquareIcon,
XIcon,
} from '@primer/octicons-react';
-import {
- useNotebookStore,
- IDatalayerNotebookExtensionProps,
-} from '../../../components';
+import { useNotebookStore, INotebookExtensionProps } from '../../../components';
type ICellToolbarComponentProps = {
cell: CodeCell;
- extensionProps: IDatalayerNotebookExtensionProps;
+ extensionProps: INotebookExtensionProps;
};
export const CellToolbarComponent = (props: ICellToolbarComponentProps) => {
diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx
index b1f8d46f2..f5bbd1735 100644
--- a/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx
+++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbarExtension.tsx
@@ -7,18 +7,18 @@
import { INotebookModel, NotebookPanel } from '@jupyterlab/notebook';
import { DocumentRegistry } from '@jupyterlab/docregistry';
import {
- DatalayerNotebookExtension,
- IDatalayerNotebookExtensionProps,
+ NotebookExtension,
+ INotebookExtensionProps,
} from '../../../components';
import { CellToolbarWidget } from './CellToolbarWidget';
import './CellToolbarExtension.css';
-export class CellToolbarExtension implements DatalayerNotebookExtension {
- private _props?: IDatalayerNotebookExtensionProps;
+export class CellToolbarExtension implements NotebookExtension {
+ private _props?: INotebookExtensionProps;
/* @override */
- init(props: IDatalayerNotebookExtensionProps) {
+ init(props: INotebookExtensionProps) {
this._props = props;
}
diff --git a/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx b/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx
index 2dc167119..31e4c399f 100644
--- a/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx
+++ b/packages/react/src/examples/extensions/celltoolbar/CellToolbarWidget.tsx
@@ -8,7 +8,7 @@ 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 { INotebookExtensionProps } from '../../../components';
import { CellToolbar, DATALAYER_CELL_TOOLBAR_CLASS } from './CellToolbar';
export interface ICellToolbarSettings {
@@ -18,9 +18,9 @@ export interface ICellToolbarSettings {
export class CellToolbarWidget extends Widget {
private _panel: NotebookPanel;
- private _props: IDatalayerNotebookExtensionProps;
+ private _props: INotebookExtensionProps;
- constructor(panel: NotebookPanel, props: IDatalayerNotebookExtensionProps) {
+ constructor(panel: NotebookPanel, props: INotebookExtensionProps) {
super();
this._panel = panel;
this._props = props;
diff --git a/packages/react/src/examples/extensions/exectime/ExecTimeExtension.tsx b/packages/react/src/examples/extensions/exectime/ExecTimeExtension.tsx
index 369779d7c..2d6104d13 100644
--- a/packages/react/src/examples/extensions/exectime/ExecTimeExtension.tsx
+++ b/packages/react/src/examples/extensions/exectime/ExecTimeExtension.tsx
@@ -7,16 +7,16 @@
import { INotebookModel, NotebookPanel } from '@jupyterlab/notebook';
import { DocumentRegistry } from '@jupyterlab/docregistry';
import {
- DatalayerNotebookExtension,
- IDatalayerNotebookExtensionProps,
+ NotebookExtension,
+ INotebookExtensionProps,
} from '../../../components';
import { ExecTimeWidget } from './ExecTimeWidget';
import './ExecTimeExtension.css';
-export class ExecTimeExtension implements DatalayerNotebookExtension {
+export class ExecTimeExtension implements NotebookExtension {
/* @override */
- init(props: IDatalayerNotebookExtensionProps) {}
+ init(props: INotebookExtensionProps) {}
/* @override */
createNew(
diff --git a/packages/react/src/examples/extensions/toc/TocExtension.tsx b/packages/react/src/examples/extensions/toc/TocExtension.tsx
index b9cde60c8..4a9fff667 100644
--- a/packages/react/src/examples/extensions/toc/TocExtension.tsx
+++ b/packages/react/src/examples/extensions/toc/TocExtension.tsx
@@ -12,8 +12,8 @@ import {
} from '@jupyterlab/toc';
import { BoxPanel } from '@lumino/widgets';
import {
- DatalayerNotebookExtension,
- IDatalayerNotebookExtensionProps,
+ NotebookExtension,
+ INotebookExtensionProps,
notebookStore,
} from '../../../components';
import { JupyterLayoutFactory } from './JupyterLayoutFactory';
@@ -40,8 +40,8 @@ export interface TocExtensionOptions {
}
/** Table of Contents Extension */
-export class TocExtension implements DatalayerNotebookExtension {
- private _props: IDatalayerNotebookExtensionProps;
+export class TocExtension implements NotebookExtension {
+ private _props: INotebookExtensionProps;
private _tocRegistry: TableOfContentsRegistry;
private _tocTracker: TableOfContentsTracker;
private _layoutFactory: TocLayoutFactory;
@@ -51,7 +51,7 @@ export class TocExtension implements DatalayerNotebookExtension {
this._layoutFactory = options.factory ?? new JupyterLayoutFactory();
}
- init(props: IDatalayerNotebookExtensionProps) {
+ init(props: INotebookExtensionProps) {
this._props = props;
this._tocRegistry = new TableOfContentsRegistry();
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 855462bfb..f51932e71 100755
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -8,6 +8,5 @@ export * from './app';
export * from './components';
export * from './jupyter';
export * from './state';
-export * from './providers';
export * from './theme';
export * from './utils';
diff --git a/packages/react/src/jupyter/JupyterConfig.ts b/packages/react/src/jupyter/JupyterConfig.ts
index 4c8b064b6..32fc2ef9a 100644
--- a/packages/react/src/jupyter/JupyterConfig.ts
+++ b/packages/react/src/jupyter/JupyterConfig.ts
@@ -27,11 +27,6 @@ export type IJupyterConfig = {
*/
let config: IJupyterConfig | undefined = undefined;
-/**
- * Datalayer configuration is loaded.
- */
-let datalayerConfigLoaded = false;
-
/**
* Setter for jupyterServerUrl.
*/
@@ -75,37 +70,6 @@ export const getJupyterServerToken = () => {
return config.jupyterServerToken;
};
-/**
- * Get the datalayer configuration fully
- * or for a particular parameter.
- *
- * @param name The parameter name
- * @returns The parameter value if {@link name} is specified, otherwise the full configuration.
- */
-function loadDatalayerConfig(name?: string): any {
- if (!datalayerConfigLoaded) {
- const datalayerConfigData = document.getElementById(
- 'datalayer-config-data'
- );
- if (datalayerConfigData?.textContent) {
- console.log('Found Datalayer config data in page', datalayerConfigData);
- try {
- config = {
- ...config,
- ...JSON.parse(datalayerConfigData.textContent),
- };
- datalayerConfigLoaded = true;
- } catch (error) {
- console.error('Failed to parse the Datalayer configuration.', error);
- }
- } else {
- console.log('No Datalayer config data found in page');
- }
- }
- // @ts-expect-error IJupyterConfig does not have index signature
- return name ? config[name] : config;
-}
-
/**
* Method to load the Jupyter configuration from the host HTML page.
*/
@@ -145,35 +109,24 @@ export const loadJupyterConfig = (
}
// Hub related information ('hubHost' 'hubPrefix' 'hubUser' ,'hubServerName').
config.insideJupyterHub = PageConfig.getOption('hubHost') !== '';
- // Load the Datalayer config.
- loadDatalayerConfig();
- if (datalayerConfigLoaded) {
- // There is a Datalayer config, mix the configs...
- setJupyterServerUrl(jupyterServerUrl || config.jupyterServerUrl);
- setJupyterServerToken(jupyterServerToken || config.jupyterServerToken);
+ // Look for a Jupyter config...
+ if (jupyterConfig) {
+ setJupyterServerUrl(
+ jupyterServerUrl ??
+ jupyterConfig.baseUrl ??
+ location.protocol + '//' + location.host + jupyterConfig.baseUrl
+ );
+ setJupyterServerToken(jupyterServerToken ?? jupyterConfig.token ?? '');
} else {
- // No Datalayer config, look for a Jupyter config...
- if (jupyterConfig) {
- setJupyterServerUrl(
- jupyterServerUrl ??
- jupyterConfig.jupyterServerUrl ??
- location.protocol + '//' + location.host + jupyterConfig.baseUrl
- );
- setJupyterServerToken(jupyterServerToken ?? jupyterConfig.token ?? '');
- } else {
- // No Datalayer and no Jupyter config, rely on location...
- setJupyterServerUrl(
- jupyterServerUrl ??
- config.jupyterServerUrl ??
- location.protocol +
- '//' +
- location.host +
- DEFAULT_API_KERNEL_PREFIX_URL
- );
- setJupyterServerToken(
- jupyterServerToken ?? config.jupyterServerToken ?? ''
- );
- }
+ // No Jupyter config, rely on location...
+ setJupyterServerUrl(
+ jupyterServerUrl ??
+ config.jupyterServerUrl ??
+ location.protocol + '//' + location.host + DEFAULT_API_KERNEL_PREFIX_URL
+ );
+ setJupyterServerToken(
+ jupyterServerToken ?? config.jupyterServerToken ?? ''
+ );
}
if (lite) {
setJupyterServerUrl(location.protocol + '//' + location.host);
diff --git a/packages/react/src/jupyter/collaboration/CollaborationContext.tsx b/packages/react/src/jupyter/collaboration/CollaborationContext.tsx
new file mode 100644
index 000000000..fe80dbf3b
--- /dev/null
+++ b/packages/react/src/jupyter/collaboration/CollaborationContext.tsx
@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 2021-2023 Datalayer, Inc.
+ *
+ * MIT License
+ */
+
+import React, {
+ createContext,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+import { YNotebook } from '@jupyter/ydoc';
+import {
+ ICollaborationProvider,
+ CollaborationStatus,
+} from './ICollaborationProvider';
+
+/**
+ * Collaboration context value
+ */
+export interface ICollaborationContext {
+ /**
+ * The collaboration provider instance
+ */
+ provider: ICollaborationProvider | null;
+ /**
+ * Current connection status
+ */
+ status: CollaborationStatus;
+ /**
+ * Whether the provider is connected
+ */
+ isConnected: boolean;
+ /**
+ * Whether the document is synchronized
+ */
+ isSynced: boolean;
+ /**
+ * Error if any
+ */
+ error: Error | null;
+ /**
+ * Connect to collaboration service
+ */
+ connect: (
+ sharedModel: YNotebook,
+ documentId: string,
+ options?: Record
+ ) => Promise;
+ /**
+ * Disconnect from collaboration service
+ */
+ disconnect: () => void;
+}
+
+const CollaborationContext = createContext(
+ undefined
+);
+
+/**
+ * Props for CollaborationProvider component
+ */
+export interface ICollaborationProviderProps {
+ /**
+ * Collaboration provider instance
+ */
+ provider?: ICollaborationProvider;
+ /**
+ * Children components
+ */
+ children: React.ReactNode;
+}
+
+/**
+ * Collaboration provider component
+ *
+ * This component provides collaboration context to its children.
+ */
+export function CollaborationProvider({
+ provider: providerProp,
+ children,
+}: ICollaborationProviderProps): JSX.Element {
+ const [status, setStatus] = useState(
+ CollaborationStatus.Disconnected
+ );
+ const [isSynced, setIsSynced] = useState(false);
+ const [error, setError] = useState(null);
+
+ // Use the provider instance
+ const provider = useMemo(() => {
+ return providerProp || null;
+ }, [providerProp]);
+
+ // Subscribe to provider events
+ useEffect(() => {
+ if (!provider) {
+ return;
+ }
+
+ const statusHandler = (
+ sender: ICollaborationProvider,
+ newStatus: CollaborationStatus
+ ) => {
+ setStatus(newStatus);
+ };
+
+ const errorHandler = (sender: ICollaborationProvider, error: Error) => {
+ setError(error);
+ };
+
+ const syncHandler = (sender: ICollaborationProvider, synced: boolean) => {
+ setIsSynced(synced);
+ };
+
+ provider.events.statusChanged.connect(statusHandler);
+ provider.events.errorOccurred.connect(errorHandler);
+ provider.events.syncStateChanged.connect(syncHandler);
+
+ // Set initial status
+ setStatus(provider.status);
+
+ return () => {
+ provider.events.statusChanged.disconnect(statusHandler);
+ provider.events.errorOccurred.disconnect(errorHandler);
+ provider.events.syncStateChanged.disconnect(syncHandler);
+ };
+ }, [provider]);
+
+ // Cleanup on unmount
+ useEffect(() => {
+ return () => {
+ provider?.dispose();
+ };
+ }, [provider]);
+
+ const connect = async (
+ sharedModel: YNotebook,
+ documentId: string,
+ options?: Record
+ ): Promise => {
+ if (!provider) {
+ throw new Error('No collaboration provider configured');
+ }
+ setError(null);
+ try {
+ await provider.connect(sharedModel, documentId, options);
+ } catch (err) {
+ setError(err as Error);
+ throw err;
+ }
+ };
+
+ const disconnect = (): void => {
+ provider?.disconnect();
+ setIsSynced(false);
+ setError(null);
+ };
+
+ const value: ICollaborationContext = {
+ provider,
+ status,
+ isConnected: status === CollaborationStatus.Connected,
+ isSynced,
+ error,
+ connect,
+ disconnect,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Hook to use collaboration context
+ *
+ * @returns The collaboration context value
+ * @throws Error if used outside of CollaborationProvider
+ */
+export function useCollaboration(): ICollaborationContext {
+ const context = useContext(CollaborationContext);
+ if (!context) {
+ throw new Error(
+ 'useCollaboration must be used within a CollaborationProvider'
+ );
+ }
+ return context;
+}
+
+/**
+ * Hook to get collaboration status
+ */
+export function useCollaborationStatus(): CollaborationStatus {
+ const { status } = useCollaboration();
+ return status;
+}
+
+/**
+ * Hook to check if collaboration is connected
+ */
+export function useIsCollaborationConnected(): boolean {
+ const { isConnected } = useCollaboration();
+ return isConnected;
+}
diff --git a/packages/react/src/jupyter/collaboration/DatalayerCollaboration.ts b/packages/react/src/jupyter/collaboration/DatalayerCollaboration.ts
deleted file mode 100644
index e70cc9ecb..000000000
--- a/packages/react/src/jupyter/collaboration/DatalayerCollaboration.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (c) 2021-2023 Datalayer, Inc.
- *
- * MIT License
- */
-
-type IFetchSessionId = {
- url: string;
- token?: string;
-};
-
-/**
- * Fetch the session ID of a collaborative documents from Datalayer.
- */
-export async function requestDatalayerollaborationSessionId({
- url,
- token,
-}: IFetchSessionId): Promise {
- const headers: HeadersInit = {
- Accept: 'application/json',
- };
- if (token) {
- headers['Authorization'] = `Bearer ${token}`;
- }
- const response = await fetch(url, {
- method: 'GET',
- headers,
- credentials: token ? 'include' : 'omit',
- mode: 'cors',
- cache: 'no-store',
- });
- if (response.ok) {
- const content = await response.json();
- return content['sessionId'];
- }
- console.error('Failed to fetch session ID.', response);
- throw new Error('Failed to fetch session ID.');
-}
diff --git a/packages/react/src/jupyter/collaboration/ICollaborationProvider.ts b/packages/react/src/jupyter/collaboration/ICollaborationProvider.ts
new file mode 100644
index 000000000..5ae694860
--- /dev/null
+++ b/packages/react/src/jupyter/collaboration/ICollaborationProvider.ts
@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 2021-2023 Datalayer, Inc.
+ *
+ * MIT License
+ */
+
+import { YNotebook } from '@jupyter/ydoc';
+import { WebsocketProvider } from 'y-websocket';
+import { IDisposable } from '@lumino/disposable';
+import { ISignal, Signal } from '@lumino/signaling';
+
+/**
+ * Collaboration provider connection status
+ */
+export enum CollaborationStatus {
+ Disconnected = 'disconnected',
+ Connecting = 'connecting',
+ Connected = 'connected',
+ Error = 'error',
+}
+
+/**
+ * Events emitted by collaboration providers
+ */
+export interface ICollaborationProviderEvents {
+ /**
+ * Signal emitted when connection status changes
+ */
+ statusChanged: ISignal;
+ /**
+ * Signal emitted when an error occurs
+ */
+ errorOccurred: ISignal;
+ /**
+ * Signal emitted when synchronization state changes
+ */
+ syncStateChanged: ISignal;
+}
+
+/**
+ * Interface for collaboration providers
+ *
+ * This interface defines the contract that all collaboration providers must implement.
+ * It provides a uniform way to connect to different collaboration backends while
+ * maintaining the same API for the notebook components.
+ */
+export interface ICollaborationProvider extends IDisposable {
+ /**
+ * Provider type identifier
+ */
+ readonly type: string;
+
+ /**
+ * Current connection status
+ */
+ readonly status: CollaborationStatus;
+
+ /**
+ * Whether the provider is currently connected
+ */
+ readonly isConnected: boolean;
+
+ /**
+ * Provider events
+ */
+ readonly events: ICollaborationProviderEvents;
+
+ /**
+ * Connect to the collaboration service
+ *
+ * @param sharedModel - The shared notebook model
+ * @param documentId - Document identifier
+ * @param options - Additional connection options
+ * @returns Promise that resolves when connected
+ */
+ connect(
+ sharedModel: YNotebook,
+ documentId: string,
+ options?: Record
+ ): Promise;
+
+ /**
+ * Disconnect from the collaboration service
+ */
+ disconnect(): void;
+
+ /**
+ * Get the underlying WebSocket provider
+ *
+ * @returns The WebSocket provider or null if not connected
+ */
+ getProvider(): WebsocketProvider | null;
+
+ /**
+ * Get the shared model
+ *
+ * @returns The shared model or null if not connected
+ */
+ getSharedModel(): YNotebook | null;
+
+ /**
+ * Handle connection close event
+ *
+ * @param event - Close event
+ */
+ handleConnectionClose(event: CloseEvent): void;
+
+ /**
+ * Handle synchronization event
+ *
+ * @param isSynced - Whether the document is synchronized
+ */
+ handleSync(isSynced: boolean): void;
+}
+
+/**
+ * Abstract base class for collaboration providers
+ *
+ * This class provides common functionality for all collaboration providers.
+ */
+export abstract class CollaborationProviderBase
+ implements ICollaborationProvider
+{
+ protected _status: CollaborationStatus = CollaborationStatus.Disconnected;
+ protected _provider: WebsocketProvider | null = null;
+ protected _sharedModel: YNotebook | null = null;
+ protected _statusChanged = new Signal(this);
+ protected _errorOccurred = new Signal(this);
+ protected _syncStateChanged = new Signal(this);
+ protected _isDisposed = false;
+
+ constructor(public readonly type: string) {}
+
+ get status(): CollaborationStatus {
+ return this._status;
+ }
+
+ get isConnected(): boolean {
+ return this._status === CollaborationStatus.Connected;
+ }
+
+ get isDisposed(): boolean {
+ return this._isDisposed;
+ }
+
+ get events(): ICollaborationProviderEvents {
+ return {
+ statusChanged: this._statusChanged,
+ errorOccurred: this._errorOccurred,
+ syncStateChanged: this._syncStateChanged,
+ };
+ }
+
+ abstract connect(
+ sharedModel: YNotebook,
+ documentId: string,
+ options?: Record
+ ): Promise;
+
+ disconnect(): void {
+ if (this._provider) {
+ this._provider.disconnect();
+ this._provider.destroy();
+ this._provider = null;
+ }
+ this._sharedModel = null;
+ this.setStatus(CollaborationStatus.Disconnected);
+ }
+
+ getProvider(): WebsocketProvider | null {
+ return this._provider;
+ }
+
+ getSharedModel(): YNotebook | null {
+ return this._sharedModel;
+ }
+
+ handleConnectionClose(event: CloseEvent): void {
+ if (event.code > 1000) {
+ console.error('Connection closed unexpectedly:', event);
+ this.setStatus(CollaborationStatus.Error);
+ this._errorOccurred.emit(new Error(`Connection closed: ${event.reason}`));
+ }
+ }
+
+ handleSync(isSynced: boolean): void {
+ this._syncStateChanged.emit(isSynced);
+ if (isSynced) {
+ this.setStatus(CollaborationStatus.Connected);
+ }
+ }
+
+ dispose(): void {
+ if (this._isDisposed) {
+ return;
+ }
+ this.disconnect();
+ Signal.clearData(this);
+ this._isDisposed = true;
+ }
+
+ protected setStatus(status: CollaborationStatus): void {
+ if (this._status !== status) {
+ this._status = status;
+ this._statusChanged.emit(status);
+ }
+ }
+}
diff --git a/packages/react/src/jupyter/collaboration/ICollaborative.ts b/packages/react/src/jupyter/collaboration/ICollaborative.ts
deleted file mode 100644
index f89b8982f..000000000
--- a/packages/react/src/jupyter/collaboration/ICollaborative.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (c) 2021-2023 Datalayer, Inc.
- *
- * MIT License
- */
-
-import { ServerConnection } from '@jupyterlab/services';
-
-export type IJupyterCollaborationServer = {
- /**
- * Base server URL
- */
- baseURL: string;
- /**
- * Notebook document name to connect to.
- */
- documentName: string;
- /**
- * JWT token
- */
- token: string;
- /**
- * Server type
- */
- type: 'datalayer';
-};
-
-export type IDatalayerCollaborationServer = {
- /**
- * Notebook path
- */
- path: string;
- /**
- * Jupyter server settings
- */
- serverSettings: ServerConnection.ISettings;
- /**
- * Server type
- */
- type: 'jupyter';
-};
-
-export type ICollaborationServer =
- | IJupyterCollaborationServer
- | IDatalayerCollaborationServer;
-
-export type ICollaborationProvider = 'jupyter' | 'datalayer' | undefined;
-
-export default ICollaborationProvider;
diff --git a/packages/react/src/jupyter/collaboration/JupyterCollaboration.ts b/packages/react/src/jupyter/collaboration/JupyterCollaboration.ts
index 2ea12137a..a45bc000f 100644
--- a/packages/react/src/jupyter/collaboration/JupyterCollaboration.ts
+++ b/packages/react/src/jupyter/collaboration/JupyterCollaboration.ts
@@ -7,7 +7,7 @@
import { URLExt } from '@jupyterlab/coreutils';
import { Contents, ServerConnection } from '@jupyterlab/services';
-export const COLLABORATION_ROOM_URL_PATH = 'api/collaboration/document';
+export const COLLABORATION_ROOM_URL_PATH = 'api/collaboration/room';
export const COLLABORATION_SESSION_URL_PATH = 'api/collaboration/session';
diff --git a/packages/react/src/jupyter/collaboration/index.ts b/packages/react/src/jupyter/collaboration/index.ts
index 7b1f3f96a..2c731f778 100644
--- a/packages/react/src/jupyter/collaboration/index.ts
+++ b/packages/react/src/jupyter/collaboration/index.ts
@@ -4,6 +4,7 @@
* MIT License
*/
-export * from './DatalayerCollaboration';
-export * from './ICollaborative';
export * from './JupyterCollaboration';
+export * from './ICollaborationProvider';
+export * from './CollaborationContext';
+export * from './providers';
diff --git a/packages/react/src/jupyter/collaboration/providers/JupyterCollaborationProvider.ts b/packages/react/src/jupyter/collaboration/providers/JupyterCollaborationProvider.ts
new file mode 100644
index 000000000..4d70b1b41
--- /dev/null
+++ b/packages/react/src/jupyter/collaboration/providers/JupyterCollaborationProvider.ts
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2021-2023 Datalayer, Inc.
+ *
+ * MIT License
+ */
+
+import { YNotebook } from '@jupyter/ydoc';
+import { WebsocketProvider } from 'y-websocket';
+import { URLExt } from '@jupyterlab/coreutils';
+import { ServerConnection } from '@jupyterlab/services';
+import {
+ CollaborationProviderBase,
+ CollaborationStatus,
+} from '../ICollaborationProvider';
+import {
+ COLLABORATION_ROOM_URL_PATH,
+ requestJupyterCollaborationSession,
+} from '../JupyterCollaboration';
+
+/**
+ * Configuration for Jupyter collaboration provider
+ */
+export interface IJupyterCollaborationConfig {
+ /**
+ * Notebook file path (optional - can be provided via connect options)
+ */
+ path?: string;
+ /**
+ * Server settings
+ */
+ serverSettings?: ServerConnection.ISettings;
+ /**
+ * Format of the document
+ */
+ format?: string;
+ /**
+ * Type of the document
+ */
+ documentType?: string;
+}
+
+/**
+ * Jupyter collaboration provider
+ *
+ * This provider connects to Jupyter's collaboration service using WebSockets.
+ */
+export class JupyterCollaborationProvider extends CollaborationProviderBase {
+ private _config: IJupyterCollaborationConfig;
+ private _onSync: ((isSynced: boolean) => void) | null = null;
+ private _onConnectionClose: ((event: CloseEvent) => void) | null = null;
+
+ constructor(config: IJupyterCollaborationConfig = {}) {
+ super('jupyter');
+ this._config = config;
+ }
+
+ async connect(
+ sharedModel: YNotebook,
+ documentId: string,
+ options?: Record
+ ): Promise {
+ if (this.isConnected) {
+ console.warn('Already connected to Jupyter collaboration service');
+ return;
+ }
+
+ this.setStatus(CollaborationStatus.Connecting);
+
+ try {
+ const serverSettings =
+ this._config.serverSettings ?? ServerConnection.makeSettings();
+ const { ydoc, awareness } = sharedModel;
+
+ // Use path from options if provided, otherwise fall back to config
+ const path = options?.path || this._config.path;
+ if (!path) {
+ throw new Error(
+ 'Path is required for Jupyter collaboration. Provide it in the config or via connect options.'
+ );
+ }
+
+ // Request collaboration session from Jupyter
+ const session = await requestJupyterCollaborationSession(
+ this._config.format || 'json',
+ this._config.documentType || 'notebook',
+ path,
+ serverSettings
+ );
+
+ // Build WebSocket URL
+ const wsUrl = serverSettings.wsUrl;
+ if (!wsUrl) {
+ throw new Error('WebSocket URL is not available');
+ }
+ const documentURL = URLExt.join(wsUrl, COLLABORATION_ROOM_URL_PATH);
+ const documentName = `${session.format}:${session.type}:${session.fileId}`;
+
+ // Create WebSocket provider
+ const params: Record = {
+ sessionId: session.sessionId,
+ };
+ if (serverSettings.token) {
+ params.token = serverSettings.token;
+ }
+
+ this._provider = new WebsocketProvider(documentURL, documentName, ydoc, {
+ disableBc: true,
+ params,
+ awareness,
+ ...options,
+ });
+
+ this._sharedModel = sharedModel;
+
+ // Set up event handlers
+ this._onSync = (isSynced: boolean) => {
+ this.handleSync(isSynced);
+ };
+ this._onConnectionClose = (event: CloseEvent) => {
+ this.handleConnectionClose(event);
+ };
+
+ this._provider.on('sync', this._onSync);
+ this._provider.on('connection-close', this._onConnectionClose);
+
+ console.log('Connected to Jupyter collaboration service');
+ } catch (error) {
+ this.setStatus(CollaborationStatus.Error);
+ this._errorOccurred.emit(error as Error);
+ throw error;
+ }
+ }
+
+ disconnect(): void {
+ if (this._provider) {
+ if (this._onSync) {
+ this._provider.off('sync', this._onSync);
+ }
+ if (this._onConnectionClose) {
+ this._provider.off('connection-close', this._onConnectionClose);
+ }
+ }
+ super.disconnect();
+ }
+
+ handleConnectionClose(event: CloseEvent): void {
+ super.handleConnectionClose(event);
+
+ // Handle session expiration (code 4002)
+ if (event.code === 4002) {
+ console.warn('Jupyter collaboration session expired');
+ // Attempt to reconnect could be implemented here?
+ }
+ }
+}
diff --git a/packages/react/src/jupyter/collaboration/providers/NoOpCollaborationProvider.ts b/packages/react/src/jupyter/collaboration/providers/NoOpCollaborationProvider.ts
new file mode 100644
index 000000000..e8a47ed7f
--- /dev/null
+++ b/packages/react/src/jupyter/collaboration/providers/NoOpCollaborationProvider.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2021-2023 Datalayer, Inc.
+ *
+ * MIT License
+ */
+
+import { YNotebook } from '@jupyter/ydoc';
+import { WebsocketProvider } from 'y-websocket';
+import {
+ CollaborationProviderBase,
+ CollaborationStatus,
+} from '../ICollaborationProvider';
+
+/**
+ * Configuration for no-op collaboration provider
+ */
+export interface INoOpCollaborationConfig {
+ type?: 'none' | 'noop';
+}
+
+/**
+ * No-operation collaboration provider
+ *
+ * This provider is used when collaboration is disabled.
+ * It provides a null implementation of the collaboration interface.
+ */
+export class NoOpCollaborationProvider extends CollaborationProviderBase {
+ constructor(config?: INoOpCollaborationConfig) {
+ super('none');
+ }
+
+ async connect(
+ sharedModel: YNotebook,
+ documentId: string,
+ options?: Record
+ ): Promise {
+ // No-op: Just store the shared model without creating any connection
+ this._sharedModel = sharedModel;
+ this.setStatus(CollaborationStatus.Connected);
+ this._syncStateChanged.emit(true);
+ }
+
+ disconnect(): void {
+ this._sharedModel = null;
+ this.setStatus(CollaborationStatus.Disconnected);
+ }
+
+ getProvider(): WebsocketProvider | null {
+ // No WebSocket provider in no-op mode
+ return null;
+ }
+
+ handleConnectionClose(event: CloseEvent): void {
+ // No-op: No connection to close
+ }
+
+ handleSync(isSynced: boolean): void {
+ // No-op: Always considered synced
+ this._syncStateChanged.emit(true);
+ }
+}
diff --git a/packages/react/src/jupyter/collaboration/providers/index.ts b/packages/react/src/jupyter/collaboration/providers/index.ts
new file mode 100644
index 000000000..277ae46b4
--- /dev/null
+++ b/packages/react/src/jupyter/collaboration/providers/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright (c) 2021-2023 Datalayer, Inc.
+ *
+ * MIT License
+ */
+
+export * from './JupyterCollaborationProvider';
+export * from './NoOpCollaborationProvider';
diff --git a/packages/react/src/providers/index.ts b/packages/react/src/providers/index.ts
deleted file mode 100755
index b76d1e76e..000000000
--- a/packages/react/src/providers/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/*
- * Copyright (c) 2021-2023 Datalayer, Inc.
- *
- * MIT License
- */
-
-export * from './kernels';
diff --git a/packages/react/src/providers/kernels/DatalayerKernels.ts b/packages/react/src/providers/kernels/DatalayerKernels.ts
deleted file mode 100755
index 80a95026b..000000000
--- a/packages/react/src/providers/kernels/DatalayerKernels.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (c) 2021-2023 Datalayer, Inc.
- *
- * MIT License
- */
-
-import { URLExt } from '@jupyterlab/coreutils';
-import { jupyterReactStore } from '../../state';
-import { ServerConnection, ServiceManager } from '@jupyterlab/services';
-
-export type KernelRequest = {
- kernel_type: 'notebook';
- kernel_given_name: string;
- credits_limit: number;
- capabilities: string[];
-};
-
-export type KernelResponse = {
- success: boolean;
- message: string;
- kernel: {
- burning_rate: number;
- kernel_type: 'notebook';
- kernel_given_name: string;
- environment_name: string;
- environment_display_name: string;
- jupyter_pod_name: string;
- token: string;
- ingress: string;
- reservation_id: string;
- started_at: string;
- expired_at: string;
- };
-};
-
-export const createDatalayerServiceManager = async (
- environmentName: string,
- credits: number
-) => {
- const datalayerConfig = jupyterReactStore.getState().datalayerConfig;
- const token = datalayerConfig?.token || '';
- const runUrl = datalayerConfig?.runUrl || 'https://prod1.datalayer.io';
- const url = URLExt.join(
- runUrl,
- 'api/jupyter/v1/environment',
- environmentName
- );
- const headers = new Headers();
- headers.set('Accept', 'application/json');
- headers.set('Content-Type', 'application/json');
- headers.set('Authorization', `Bearer ${token}`);
- const request: KernelRequest = {
- kernel_type: 'notebook',
- kernel_given_name: `Jupyter React Kernel - ${new Date()}`,
- credits_limit: credits,
- capabilities: [],
- };
- const response = await fetch(url, {
- method: 'POST',
- headers: headers,
- body: JSON.stringify(request),
- credentials: token ? 'include' : 'omit',
- mode: 'cors',
- cache: 'no-store',
- })
- .then((resp: Response) => {
- if (resp.ok) {
- return resp.json();
- } else {
- throw new Error(resp.statusText);
- }
- })
- .catch((err: Error) => {
- console.error(err);
- return err;
- })
- .finally(() => {});
- if (response instanceof Error) {
- throw response as Error;
- }
- const kernelResponse = response as KernelResponse;
- const serverSettings = ServerConnection.makeSettings({
- baseUrl: kernelResponse.kernel.ingress,
- wsUrl: kernelResponse.kernel.ingress.replace(/^http/, 'ws'),
- token: kernelResponse.kernel.token,
- appendToken: true,
- });
- const serviceManager = new ServiceManager({ serverSettings });
- return serviceManager;
-};
diff --git a/packages/react/src/providers/kernels/index.ts b/packages/react/src/providers/kernels/index.ts
deleted file mode 100755
index b6cc92338..000000000
--- a/packages/react/src/providers/kernels/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/*
- * Copyright (c) 2021-2023 Datalayer, Inc.
- *
- * MIT License
- */
-
-export * from './DatalayerKernels';
diff --git a/packages/react/src/state/IDatalayerConfig.ts b/packages/react/src/state/IDatalayerConfig.ts
deleted file mode 100644
index 413b5050e..000000000
--- a/packages/react/src/state/IDatalayerConfig.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (c) 2021-2023 Datalayer, Inc.
- *
- * MIT License
- */
-
-export type IDatalayerConfig = {
- /**
- * Datalayer RUN URL.
- */
- runUrl: string;
- /**
- * Datalayer Token.
- */
- token: string;
- /**
- * Credits.
- */
- credits: number;
- /**
- * CPU Environment.
- */
- cpuEnvironment: string;
- /**
- * GPU Environment.
- */
- gpuEnvironment: string;
-};
diff --git a/packages/react/src/state/JupyterReactState.ts b/packages/react/src/state/JupyterReactState.ts
index efcd5f9fa..a94a17949 100644
--- a/packages/react/src/state/JupyterReactState.ts
+++ b/packages/react/src/state/JupyterReactState.ts
@@ -23,7 +23,6 @@ import {
import { ServiceManagerLess } from '../jupyter/services';
import { Kernel } from '../jupyter/kernel/Kernel';
import { IJupyterConfig, loadJupyterConfig } from '../jupyter/JupyterConfig';
-import type { IDatalayerConfig } from './IDatalayerConfig';
import { cellsStore, CellsState } from '../components/cell/CellState';
import { consoleStore, ConsoleState } from '../components/console/ConsoleState';
import {
@@ -47,7 +46,6 @@ export type KernelTransfer = {
export type JupyterReactState = {
cellsStore: CellsState;
consoleStore: ConsoleState;
- datalayerConfig?: IDatalayerConfig;
jupyterConfig?: IJupyterConfig;
kernel?: Kernel;
kernelIsLoading: boolean;
@@ -56,26 +54,13 @@ export type JupyterReactState = {
serviceManager?: ServiceManager.IManager;
terminalStore: TerminalState;
version: string;
- setDatalayerConfig: (configuration?: IDatalayerConfig) => void;
setJupyterConfig: (configuration?: IJupyterConfig) => void;
setServiceManager: (serviceManager?: ServiceManager.IManager) => void;
setVersion: (version: string) => void;
};
-let initialDatalayerConfig: IDatalayerConfig | undefined = undefined;
-
-try {
- const pageConfig = document.getElementById('datalayer-config-data');
- if (pageConfig?.innerText) {
- initialDatalayerConfig = JSON.parse(pageConfig?.innerText);
- }
-} catch (error) {
- console.debug('Issue with page configuration.', error);
-}
-
export const jupyterReactStore = createStore((set, get) => ({
collaborative: false,
- datalayerConfig: initialDatalayerConfig,
version: '',
jupyterConfig: undefined,
kernelIsLoading: true,
@@ -87,9 +72,6 @@ export const jupyterReactStore = createStore((set, get) => ({
notebookStore: notebookStore.getState(),
outputStore: outputsStore.getState(),
terminalStore: terminalStore.getState(),
- setDatalayerConfig: (datalayerConfig?: IDatalayerConfig) => {
- set(state => ({ datalayerConfig }));
- },
setJupyterConfig: (jupyterConfig?: IJupyterConfig) => {
set(state => ({ jupyterConfig }));
},
@@ -124,8 +106,8 @@ export function useJupyterReactStoreFromProps(
const {
defaultKernelName = DEFAULT_KERNEL_NAME,
initCode = '',
- jupyterServerToken = props.serviceManager?.serverSettings.token ?? '',
- jupyterServerUrl = props.serviceManager?.serverSettings.baseUrl ?? '',
+ jupyterServerToken = props.serviceManager?.serverSettings.token,
+ jupyterServerUrl = props.serviceManager?.serverSettings.baseUrl,
lite = false,
serverless,
serviceManager: propsServiceManager,
diff --git a/packages/react/src/state/index.ts b/packages/react/src/state/index.ts
index 27f144b10..5088c27a8 100644
--- a/packages/react/src/state/index.ts
+++ b/packages/react/src/state/index.ts
@@ -4,5 +4,4 @@
* MIT License
*/
-export * from './IDatalayerConfig';
export * from './JupyterReactState';
diff --git a/packages/react/webpack.config.js b/packages/react/webpack.config.js
index 031844362..c6039e0f7 100644
--- a/packages/react/webpack.config.js
+++ b/packages/react/webpack.config.js
@@ -50,11 +50,12 @@ const ENTRY =
// './src/examples/NotebookCellToolbar';
// './src/examples/NotebookColormode';
// './src/examples/NotebookCollaborative';
- // './src/examples/NotebookExtension';
- // './src/examples/NotebookKernel';
- // './src/examples/NotebookKernelChange';
- // './src/examples/NotebookLess';
- './src/examples/NotebookLite';
+ './src/examples/Notebook2Collaborative';
+// './src/examples/NotebookExtension';
+// './src/examples/NotebookKernel';
+// './src/examples/NotebookKernelChange';
+// './src/examples/NotebookLess';
+// './src/examples/NotebookLite';
// './src/examples/NotebookLiteContext';
// './src/examples/NotebookLocalServer';
// './src/examples/NotebookMutationsKernel';