From b5cedfcc8debe317ef5fe719dd9f945ae6271f06 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:02:06 +0000 Subject: [PATCH] Use content providers (v2) Co-authored-by: davidbrochart --- packages/collaborative-drive/src/tokens.ts | 24 +- .../docprovider-extension/src/filebrowser.ts | 233 ++++++------------ .../docprovider-extension/src/forkManager.ts | 11 +- packages/docprovider-extension/src/index.ts | 8 +- packages/docprovider/src/forkManager.ts | 24 +- packages/docprovider/src/ydrive.ts | 97 ++++---- 6 files changed, 152 insertions(+), 245 deletions(-) diff --git a/packages/collaborative-drive/src/tokens.ts b/packages/collaborative-drive/src/tokens.ts index 6db3d76e..f985c771 100644 --- a/packages/collaborative-drive/src/tokens.ts +++ b/packages/collaborative-drive/src/tokens.ts @@ -1,8 +1,8 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -import { DocumentChange, IAwareness, YDocument } from '@jupyter/ydoc'; -import { Contents } from '@jupyterlab/services'; +import { IAwareness } from '@jupyter/ydoc'; +import { Contents, SharedDocumentFactory } from '@jupyterlab/services'; import { IDisposable } from '@lumino/disposable'; import { Token } from '@lumino/coreutils'; @@ -10,9 +10,10 @@ import { Token } from '@lumino/coreutils'; /** * The collaborative drive. */ -export const ICollaborativeDrive = new Token( - '@jupyter/collaboration-extension:ICollaborativeDrive' -); +export const ICollaborativeContentProvider = + new Token( + '@jupyter/collaboration-extension:ICollaborativeContentProvider' + ); /** * The global awareness token. @@ -21,18 +22,7 @@ export const IGlobalAwareness = new Token( '@jupyter/collaboration:IGlobalAwareness' ); -/** - * A document factory for registering shared models - */ -export type SharedDocumentFactory = ( - options: Contents.ISharedFactoryOptions -) => YDocument; - -/** - * A Collaborative implementation for an `IDrive`, talking to the - * server using the Jupyter REST API and a WebSocket connection. - */ -export interface ICollaborativeDrive extends Contents.IDrive { +export interface ICollaborativeContentProvider { /** * SharedModel factory for the YDrive. */ diff --git a/packages/docprovider-extension/src/filebrowser.ts b/packages/docprovider-extension/src/filebrowser.ts index ee3af263..50db8003 100644 --- a/packages/docprovider-extension/src/filebrowser.ts +++ b/packages/docprovider-extension/src/filebrowser.ts @@ -4,45 +4,44 @@ */ import { - ILabShell, - IRouter, JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; import { Dialog, showDialog } from '@jupyterlab/apputils'; import { DocumentWidget, IDocumentWidget } from '@jupyterlab/docregistry'; import { Widget } from '@lumino/widgets'; -import { - FileBrowser, - IDefaultFileBrowser, - IFileBrowserFactory -} from '@jupyterlab/filebrowser'; + import { IStatusBar } from '@jupyterlab/statusbar'; +import { ContentsManager } from '@jupyterlab/services'; -import { IEditorTracker } from '@jupyterlab/fileeditor'; +import { + IEditorTracker, + IEditorWidgetFactory, + FileEditorFactory +} from '@jupyterlab/fileeditor'; import { ILogger, ILoggerRegistry } from '@jupyterlab/logconsole'; -import { INotebookTracker } from '@jupyterlab/notebook'; +import { + INotebookTracker, + INotebookWidgetFactory, + NotebookWidgetFactory +} from '@jupyterlab/notebook'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { ITranslator, nullTranslator } from '@jupyterlab/translation'; -import { CommandRegistry } from '@lumino/commands'; - import { YFile, YNotebook } from '@jupyter/ydoc'; import { - ICollaborativeDrive, + ICollaborativeContentProvider, IGlobalAwareness } from '@jupyter/collaborative-drive'; -import { IForkProvider, TimelineWidget, YDrive } from '@jupyter/docprovider'; +import { + IForkProvider, + TimelineWidget, + RtcContentProvider +} from '@jupyter/docprovider'; import { Awareness } from 'y-protocols/awareness'; import { URLExt } from '@jupyterlab/coreutils'; -/** - * The command IDs used by the file browser plugin. - */ -namespace CommandIDs { - export const openPath = 'filebrowser:open-path'; -} const DOCUMENT_TIMELINE_URL = 'api/collaboration/timeline'; const TWO_SESSIONS_WARNING = @@ -50,26 +49,43 @@ const TWO_SESSIONS_WARNING = 'This is not supported. Please close this view; otherwise, ' + 'some of your edits may not be saved properly.'; -/** - * The default collaborative drive provider. - */ -export const drive: JupyterFrontEndPlugin = { - id: '@jupyter/docprovider-extension:drive', - description: 'The default collaborative drive provider', - provides: ICollaborativeDrive, - requires: [ITranslator], - optional: [IGlobalAwareness], - activate: ( - app: JupyterFrontEnd, - translator: ITranslator, - globalAwareness: Awareness | null - ): ICollaborativeDrive => { - const trans = translator.load('jupyter_collaboration'); - const drive = new YDrive(app.serviceManager.user, trans, globalAwareness); - app.serviceManager.contents.addDrive(drive); - return drive; - } -}; +export const rtcContentProvider: JupyterFrontEndPlugin = + { + id: '@jupyter/docprovider-extension:content-provider', + description: 'The RTC content provider', + provides: ICollaborativeContentProvider, + requires: [ITranslator], + optional: [IGlobalAwareness], + activate: ( + app: JupyterFrontEnd, + translator: ITranslator, + globalAwareness: Awareness | null + ): ICollaborativeContentProvider => { + const trans = translator.load('jupyter_collaboration'); + const defaultDrive = (app.serviceManager.contents as ContentsManager) + .defaultDrive; + if (!defaultDrive) { + throw Error( + 'Cannot initialize content provider: default drive property not accessible on contents manager instance.' + ); + } + const registry = defaultDrive.contentProviderRegistry; + if (!registry) { + throw Error( + 'Cannot initialize content provider: no content provider registry.' + ); + } + const rtcContentProvider = new RtcContentProvider({ + apiEndpoint: '/api/contents', + serverSettings: defaultDrive.serverSettings, + user: app.serviceManager.user, + trans, + globalAwareness + }); + registry.register('rtc', rtcContentProvider); + return rtcContentProvider; + } + }; /** * Plugin to register the shared model factory for the content type 'file'. @@ -79,13 +95,20 @@ export const yfile: JupyterFrontEndPlugin = { description: "Plugin to register the shared model factory for the content type 'file'", autoStart: true, - requires: [ICollaborativeDrive], - optional: [], - activate: (app: JupyterFrontEnd, drive: ICollaborativeDrive): void => { + requires: [ICollaborativeContentProvider, IEditorWidgetFactory], + activate: ( + app: JupyterFrontEnd, + contentProvider: ICollaborativeContentProvider, + editorFactory: FileEditorFactory.IFactory + ): void => { const yFileFactory = () => { return new YFile(); }; - drive.sharedModelFactory.registerDocumentFactory('file', yFileFactory); + contentProvider.sharedModelFactory.registerDocumentFactory( + 'file', + yFileFactory + ); + editorFactory.contentProviderId = 'rtc'; } }; @@ -97,11 +120,12 @@ export const ynotebook: JupyterFrontEndPlugin = { description: "Plugin to register the shared model factory for the content type 'notebook'", autoStart: true, - requires: [ICollaborativeDrive], + requires: [ICollaborativeContentProvider, INotebookWidgetFactory], optional: [ISettingRegistry], activate: ( app: JupyterFrontEnd, - drive: YDrive, + contentProvider: ICollaborativeContentProvider, + notebookFactory: NotebookWidgetFactory.IFactory, settingRegistry: ISettingRegistry | null ): void => { let disableDocumentWideUndoRedo = true; @@ -131,10 +155,11 @@ export const ynotebook: JupyterFrontEndPlugin = { disableDocumentWideUndoRedo }); }; - drive.sharedModelFactory.registerDocumentFactory( + contentProvider.sharedModelFactory.registerDocumentFactory( 'notebook', yNotebookFactory ); + notebookFactory.contentProviderId = 'rtc'; } }; /** @@ -144,11 +169,11 @@ export const statusBarTimeline: JupyterFrontEndPlugin = { id: '@jupyter/docprovider-extension:statusBarTimeline', description: 'Plugin to add a timeline slider to the status bar', autoStart: true, - requires: [IStatusBar, ICollaborativeDrive], + requires: [IStatusBar, ICollaborativeContentProvider], activate: async ( app: JupyterFrontEnd, statusBar: IStatusBar, - drive: ICollaborativeDrive + contentProvider: ICollaborativeContentProvider ): Promise => { try { let sliderItem: Widget | null = null; @@ -158,10 +183,9 @@ export const statusBarTimeline: JupyterFrontEndPlugin = { documentPath: string, documentId: string ) => { - if (documentId && documentPath.split(':')[0] === 'RTC') { - if (drive) { - // Remove 'RTC:' from document path - documentPath = documentPath.slice(drive.name.length + 1); + // TODO: check if documentId uses RTC + if (documentId) { + if (contentProvider) { // Dispose of the previous timelineWidget if it exists if (timelineWidget) { timelineWidget.dispose(); @@ -169,7 +193,7 @@ export const statusBarTimeline: JupyterFrontEndPlugin = { } const [format, type] = documentId.split(':'); - const provider = drive.providers.get( + const provider = contentProvider.providers.get( `${format}:${type}:${documentPath}` ) as unknown as IForkProvider; const fullPath = URLExt.join( @@ -238,6 +262,7 @@ export const statusBarTimeline: JupyterFrontEndPlugin = { currentWidget.context.model.sharedModel.getState( 'document_id' ) as string; + // TODO return !!documentId && documentPath.split(':')[0] === 'RTC'; } return false; @@ -251,52 +276,6 @@ export const statusBarTimeline: JupyterFrontEndPlugin = { } }; -/** - * The default file browser factory provider. - */ -export const defaultFileBrowser: JupyterFrontEndPlugin = { - id: '@jupyter/docprovider-extension:defaultFileBrowser', - description: 'The default file browser factory provider', - provides: IDefaultFileBrowser, - requires: [ICollaborativeDrive, IFileBrowserFactory], - optional: [IRouter, JupyterFrontEnd.ITreeResolver, ILabShell, ITranslator], - activate: async ( - app: JupyterFrontEnd, - drive: YDrive, - fileBrowserFactory: IFileBrowserFactory, - router: IRouter | null, - tree: JupyterFrontEnd.ITreeResolver | null, - labShell: ILabShell | null, - translator: ITranslator | null - ): Promise => { - const { commands } = app; - const trans = (translator ?? nullTranslator).load('jupyterlab'); - app.serviceManager.contents.addDrive(drive); - - // Manually restore and load the default file browser. - const defaultBrowser = fileBrowserFactory.createFileBrowser('filebrowser', { - auto: false, - restore: false, - driveName: drive.name - }); - defaultBrowser.node.setAttribute('role', 'region'); - defaultBrowser.node.setAttribute( - 'aria-label', - trans.__('File Browser Section') - ); - - void Private.restoreBrowser( - defaultBrowser, - commands, - router, - tree, - labShell - ); - - return defaultBrowser; - } -}; - /** * The default collaborative drive provider. */ @@ -383,59 +362,3 @@ export const logger: JupyterFrontEndPlugin = { })(); } }; - -namespace Private { - /** - * Restores file browser state and overrides state if tree resolver resolves. - */ - export async function restoreBrowser( - browser: FileBrowser, - commands: CommandRegistry, - router: IRouter | null, - tree: JupyterFrontEnd.ITreeResolver | null, - labShell: ILabShell | null - ): Promise { - const restoring = 'jp-mod-restoring'; - - browser.addClass(restoring); - - if (!router) { - await browser.model.restore(browser.id); - await browser.model.refresh(); - browser.removeClass(restoring); - return; - } - - const listener = async () => { - router.routed.disconnect(listener); - - const paths = await tree?.paths; - - if (paths?.file || paths?.browser) { - // Restore the model without populating it. - await browser.model.restore(browser.id, false); - if (paths.file) { - await commands.execute(CommandIDs.openPath, { - path: paths.file, - dontShowBrowser: true - }); - } - if (paths.browser) { - await commands.execute(CommandIDs.openPath, { - path: paths.browser, - dontShowBrowser: true - }); - } - } else { - await browser.model.restore(browser.id); - await browser.model.refresh(); - } - browser.removeClass(restoring); - - if (labShell?.isEmpty('main')) { - void commands.execute('launcher:create'); - } - }; - router.routed.connect(listener); - } -} diff --git a/packages/docprovider-extension/src/forkManager.ts b/packages/docprovider-extension/src/forkManager.ts index 5c2cf916..7cb327f1 100644 --- a/packages/docprovider-extension/src/forkManager.ts +++ b/packages/docprovider-extension/src/forkManager.ts @@ -3,7 +3,7 @@ * Distributed under the terms of the Modified BSD License. */ -import { ICollaborativeDrive } from '@jupyter/collaborative-drive'; +import { ICollaborativeContentProvider } from '@jupyter/collaborative-drive'; import { ForkManager, IForkManager, @@ -18,11 +18,14 @@ import { export const forkManagerPlugin: JupyterFrontEndPlugin = { id: '@jupyter/docprovider-extension:forkManager', autoStart: true, - requires: [ICollaborativeDrive], + requires: [ICollaborativeContentProvider], provides: IForkManagerToken, - activate: (app: JupyterFrontEnd, drive: ICollaborativeDrive) => { + activate: ( + app: JupyterFrontEnd, + contentProvider: ICollaborativeContentProvider + ) => { const eventManager = app.serviceManager.events; - const manager = new ForkManager({ drive, eventManager }); + const manager = new ForkManager({ contentProvider, eventManager }); return manager; } }; diff --git a/packages/docprovider-extension/src/index.ts b/packages/docprovider-extension/src/index.ts index 556f7470..420c72cf 100644 --- a/packages/docprovider-extension/src/index.ts +++ b/packages/docprovider-extension/src/index.ts @@ -8,10 +8,9 @@ import { JupyterFrontEndPlugin } from '@jupyterlab/application'; import { - drive, + rtcContentProvider, yfile, ynotebook, - defaultFileBrowser, logger, statusBarTimeline } from './filebrowser'; @@ -21,11 +20,10 @@ import { forkManagerPlugin } from './forkManager'; /** * Export the plugins as default. */ -const plugins: JupyterFrontEndPlugin[] = [ - drive, +const plugins: JupyterFrontEndPlugin[] = [ + rtcContentProvider, yfile, ynotebook, - defaultFileBrowser, logger, notebookCellExecutor, statusBarTimeline, diff --git a/packages/docprovider/src/forkManager.ts b/packages/docprovider/src/forkManager.ts index f781e802..99031c17 100644 --- a/packages/docprovider/src/forkManager.ts +++ b/packages/docprovider/src/forkManager.ts @@ -3,7 +3,7 @@ * Distributed under the terms of the Modified BSD License. */ -import { ICollaborativeDrive } from '@jupyter/collaborative-drive'; +import { ICollaborativeContentProvider } from '@jupyter/collaborative-drive'; import { URLExt } from '@jupyterlab/coreutils'; import { Event } from '@jupyterlab/services'; import { ISignal, Signal } from '@lumino/signaling'; @@ -22,8 +22,8 @@ export const JUPYTER_COLLABORATION_FORK_EVENTS_URI = export class ForkManager implements IForkManager { constructor(options: ForkManager.IOptions) { - const { drive, eventManager } = options; - this._drive = drive; + const { contentProvider, eventManager } = options; + this._contentProvider = contentProvider; this._eventManager = eventManager; this._eventManager.stream.connect(this._handleEvent, this); } @@ -81,14 +81,12 @@ export class ForkManager implements IForkManager { type: string; }): IForkProvider | undefined { const { documentPath, format, type } = options; - const drive = this._drive; - if (drive) { - const driveName = drive.name; - let docPath = documentPath; - if (documentPath.startsWith(driveName)) { - docPath = documentPath.slice(driveName.length + 1); - } - const provider = drive.providers.get(`${format}:${type}:${docPath}`); + const contentProvider = this._contentProvider; + if (contentProvider) { + const docPath = documentPath; + const provider = contentProvider.providers.get( + `${format}:${type}:${docPath}` + ); return provider as IForkProvider | undefined; } return; @@ -112,7 +110,7 @@ export class ForkManager implements IForkManager { } private _disposed = false; - private _drive: ICollaborativeDrive | undefined; + private _contentProvider: ICollaborativeContentProvider | undefined; private _eventManager: Event.IManager | undefined; private _forkAddedSignal = new Signal(this); private _forkDeletedSignal = new Signal(this); @@ -120,7 +118,7 @@ export class ForkManager implements IForkManager { export namespace ForkManager { export interface IOptions { - drive: ICollaborativeDrive; + contentProvider: ICollaborativeContentProvider; eventManager: Event.IManager; } } diff --git a/packages/docprovider/src/ydrive.ts b/packages/docprovider/src/ydrive.ts index f013d3df..5cd1f9d5 100644 --- a/packages/docprovider/src/ydrive.ts +++ b/packages/docprovider/src/ydrive.ts @@ -3,16 +3,22 @@ import { PageConfig, URLExt } from '@jupyterlab/coreutils'; import { TranslationBundle } from '@jupyterlab/translation'; -import { Contents, Drive, User } from '@jupyterlab/services'; +import { + Contents, + IContentProvider, + RestContentProvider, + SharedDocumentFactory, + ServerConnection, + User +} from '@jupyterlab/services'; import { ISignal, Signal } from '@lumino/signaling'; import { DocumentChange, ISharedDocument, YDocument } from '@jupyter/ydoc'; import { WebSocketProvider } from './yprovider'; import { - ICollaborativeDrive, - ISharedModelFactory, - SharedDocumentFactory + IDocumentProvider, + ISharedModelFactory } from '@jupyter/collaborative-drive'; import { Awareness } from 'y-protocols/awareness'; @@ -31,55 +37,50 @@ export interface IForkProvider { format: string; } -/** - * A Collaborative implementation for an `IDrive`, talking to the - * server using the Jupyter REST API and a WebSocket connection. - */ -export class YDrive extends Drive implements ICollaborativeDrive { - /** - * Construct a new drive object. - * - * @param user - The user manager to add the identity to the awareness of documents. - */ - constructor( - user: User.IManager, - translator: TranslationBundle, - globalAwareness: Awareness | null - ) { - super({ name: 'RTC' }); - this._user = user; - this._trans = translator; - this._globalAwareness = globalAwareness; - this._providers = new Map(); +namespace RtcContentProvider { + export interface IOptions extends RestContentProvider.IOptions { + user: User.IManager; + trans: TranslationBundle; + globalAwareness: Awareness | null; + } +} +export class RtcContentProvider + extends RestContentProvider + implements IContentProvider +{ + constructor(options: RtcContentProvider.IOptions) { + super(options); + this._user = options.user; + this._trans = options.trans; + this._globalAwareness = options.globalAwareness; + this._serverSettings = options.serverSettings; this.sharedModelFactory = new SharedModelFactory(this._onCreate); + this._providers = new Map(); + /* super.fileChanged.connect((_, change) => { // pass through any events from the Drive superclass this._ydriveFileChanged.emit(change); }); + */ } - /** - * SharedModel factory for the YDrive. - */ - readonly sharedModelFactory: ISharedModelFactory; - - get providers(): Map { + get providers(): Map { return this._providers; } /** - * Dispose of the resources held by the manager. + * A signal emitted when a file operation takes place. */ - dispose(): void { - if (this.isDisposed) { - return; - } - this._providers.forEach(p => p.dispose()); - this._providers.clear(); - super.dispose(); + get fileChanged(): ISignal { + return this._ydriveFileChanged; } + /** + * SharedModel factory for the YDrive. + */ + readonly sharedModelFactory: ISharedModelFactory; + /** * Get a file or directory. * @@ -88,13 +89,12 @@ export class YDrive extends Drive implements ICollaborativeDrive { * @param options: The options used to fetch the file. * * @returns A promise which resolves with the file content. - * - * Uses the [Jupyter Notebook API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml#!/contents) and validates the response model. */ async get( localPath: string, options?: Contents.IFetchOptions ): Promise { + console.log('RTC get'); if (options && options.format && options.type) { const key = `${options.format}:${options.type}:${localPath}`; const provider = this._providers.get(key); @@ -150,13 +150,6 @@ export class YDrive extends Drive implements ICollaborativeDrive { return super.save(localPath, options); } - /** - * A signal emitted when a file operation takes place. - */ - get fileChanged(): ISignal { - return this._ydriveFileChanged; - } - private _onCreate = ( options: Contents.ISharedFactoryOptions, sharedModel: YDocument @@ -166,7 +159,7 @@ export class YDrive extends Drive implements ICollaborativeDrive { } try { const provider = new WebSocketProvider({ - url: URLExt.join(this.serverSettings.wsUrl, DOCUMENT_PROVIDER_URL), + url: URLExt.join(this._serverSettings.wsUrl, DOCUMENT_PROVIDER_URL), path: options.path, format: options.format, contentType: options.contentType, @@ -179,7 +172,7 @@ export class YDrive extends Drive implements ICollaborativeDrive { const state = this._globalAwareness?.getLocalState() || {}; const documents: any[] = state.documents || []; if (!documents.includes(options.path)) { - documents.push(`${this.name}:${options.path}`); + documents.push(`${this._prefix}:${options.path}`); this._globalAwareness?.setLocalStateField('documents', documents); } @@ -228,7 +221,7 @@ export class YDrive extends Drive implements ICollaborativeDrive { // Remove the document path from the list of opened ones for this user. const state = this._globalAwareness?.getLocalState() || {}; const documents: any[] = state.documents || []; - const index = documents.indexOf(`${this.name}:${options.path}`); + const index = documents.indexOf(`${this._prefix}:${options.path}`); if (index > -1) { documents.splice(index, 1); } @@ -245,9 +238,11 @@ export class YDrive extends Drive implements ICollaborativeDrive { private _user: User.IManager; private _trans: TranslationBundle; - private _providers: Map; private _globalAwareness: Awareness | null; + private _providers: Map; private _ydriveFileChanged = new Signal(this); + private _serverSettings: ServerConnection.ISettings; + private _prefix = 'RTC'; // TODO } /**