From f14a08384c6a3f8ddfdc85f024d3e4e13b558783 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Mon, 4 Nov 2024 16:44:35 +0100 Subject: [PATCH] Add js api --- .../docprovider-extension/src/filebrowser.ts | 1 - .../docprovider-extension/src/forkManager.ts | 23 ++++ packages/docprovider-extension/src/index.ts | 4 +- packages/docprovider/src/component.tsx | 1 - packages/docprovider/src/forkManager.ts | 117 ++++++++++++++++++ packages/docprovider/src/index.ts | 2 + packages/docprovider/src/requests.ts | 41 ++++++ packages/docprovider/src/tokens.ts | 43 +++++++ packages/docprovider/src/ydrive.ts | 1 - 9 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 packages/docprovider-extension/src/forkManager.ts create mode 100644 packages/docprovider/src/forkManager.ts create mode 100644 packages/docprovider/src/tokens.ts diff --git a/packages/docprovider-extension/src/filebrowser.ts b/packages/docprovider-extension/src/filebrowser.ts index bcea4f9a..afc93500 100644 --- a/packages/docprovider-extension/src/filebrowser.ts +++ b/packages/docprovider-extension/src/filebrowser.ts @@ -176,7 +176,6 @@ export const statusBarTimeline: JupyterFrontEndPlugin = { DOCUMENT_TIMELINE_URL, documentPath ); - timelineWidget = new TimelineWidget( fullPath, provider, diff --git a/packages/docprovider-extension/src/forkManager.ts b/packages/docprovider-extension/src/forkManager.ts new file mode 100644 index 00000000..a2d9a530 --- /dev/null +++ b/packages/docprovider-extension/src/forkManager.ts @@ -0,0 +1,23 @@ +import { ICollaborativeDrive } from '@jupyter/collaborative-drive'; +import { + ForkManager, + IForkManager, + IForkManagerToken +} from '@jupyter/docprovider'; + +import { + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; + +export const forkManagerPlugin: JupyterFrontEndPlugin = { + id: '@jupyter/docprovider-extension:forkManager', + autoStart: true, + requires: [ICollaborativeDrive], + provides: IForkManagerToken, + activate: (app: JupyterFrontEnd, drive: ICollaborativeDrive) => { + const eventManager = app.serviceManager.events; + const manager = new ForkManager({ drive, eventManager }); + return manager; + } +}; diff --git a/packages/docprovider-extension/src/index.ts b/packages/docprovider-extension/src/index.ts index 1869c5b7..556f7470 100644 --- a/packages/docprovider-extension/src/index.ts +++ b/packages/docprovider-extension/src/index.ts @@ -16,6 +16,7 @@ import { statusBarTimeline } from './filebrowser'; import { notebookCellExecutor } from './executor'; +import { forkManagerPlugin } from './forkManager'; /** * Export the plugins as default. @@ -27,7 +28,8 @@ const plugins: JupyterFrontEndPlugin[] = [ defaultFileBrowser, logger, notebookCellExecutor, - statusBarTimeline + statusBarTimeline, + forkManagerPlugin ]; export default plugins; diff --git a/packages/docprovider/src/component.tsx b/packages/docprovider/src/component.tsx index 50533c53..ee4282b1 100644 --- a/packages/docprovider/src/component.tsx +++ b/packages/docprovider/src/component.tsx @@ -74,7 +74,6 @@ export const TimelineSliderComponent: React.FC = ({ setData(data); setCurrentTimestampIndex(data.timestamps.length - 1); provider.connectToForkDoc(data.forkRoom, data.sessionId); - sessionRef.current = await requestDocSession( format, contentType, diff --git a/packages/docprovider/src/forkManager.ts b/packages/docprovider/src/forkManager.ts new file mode 100644 index 00000000..fd18b314 --- /dev/null +++ b/packages/docprovider/src/forkManager.ts @@ -0,0 +1,117 @@ +import { ICollaborativeDrive } from '@jupyter/collaborative-drive'; +import { URLExt } from '@jupyterlab/coreutils'; +import { Event } from '@jupyterlab/services'; +import { ISignal, Signal } from '@lumino/signaling'; + +import { requestAPI, ROOM_FORK_URL } from './requests'; +import { + IAllForksResponse, + IForkChangedEvent, + IForkCreationResponse, + IForkManager +} from './tokens'; +import { IForkProvider } from './ydrive'; + +const JUPYTER_COLLABORATION_FORK_EVENTS_URI = + 'https://schema.jupyter.org/jupyter_collaboration/fork/v1'; + +export class ForkManager implements IForkManager { + constructor(options: ForkManager.IOptions) { + const { drive, eventManager } = options; + this._drive = drive; + this._eventManager = eventManager; + this._eventManager.stream.connect(this._handleEvent, this); + } + + get isDisposed(): boolean { + return this._disposed; + } + get forkAdded(): ISignal { + return this._forkAddedSignal; + } + get forkDeleted(): ISignal { + return this._forkDeletedSignal; + } + + dispose(): void { + if (this._disposed) { + return; + } + this._eventManager?.stream.disconnect(this._handleEvent); + this._disposed = true; + } + async createFork(options: { + rootId: string; + synchronize: boolean; + label?: string; + description?: string; + }): Promise { + const { rootId, label, description, synchronize } = options; + const init: RequestInit = { + method: 'PUT', + body: JSON.stringify({ label, description, synchronize }) + }; + const url = URLExt.join(ROOM_FORK_URL, rootId); + const response = await requestAPI(url, init); + return response; + } + + async getAllForks(rootId: string) { + const url = URLExt.join(ROOM_FORK_URL, rootId); + const init = { method: 'GET' }; + const response = await requestAPI(url, init); + return response; + } + + async deleteFork(options: { forkId: string; merge: boolean }): Promise { + const { forkId, merge } = options; + const url = URLExt.join(ROOM_FORK_URL, forkId); + const query = URLExt.objectToQueryString({ merge }); + const init = { method: 'DELETE' }; + await requestAPI(`${url}${query}`, init); + } + getProvider(options: { + documentPath: string; + format: string; + type: string; + }): IForkProvider | undefined { + const { documentPath, format, type } = options; + const drive = this._drive; + if (drive) { + const docPath = documentPath.slice(drive.name.length + 1); + const provider = drive.providers.get(`${format}:${type}:${docPath}`); + return provider as IForkProvider | undefined; + } + return; + } + + private _handleEvent(_: Event.IManager, emission: Event.Emission) { + if (emission.schema_id === JUPYTER_COLLABORATION_FORK_EVENTS_URI) { + switch (emission.action) { + case 'create': { + this._forkAddedSignal.emit(emission as any); + break; + } + case 'delete': { + this._forkDeletedSignal.emit(emission as any); + break; + } + default: + break; + } + } + } + + private _disposed = false; + private _drive: ICollaborativeDrive | undefined; + private _eventManager: Event.IManager | undefined; + private _forkAddedSignal = new Signal(this); + private _forkDeletedSignal = new Signal(this); +} + +export namespace ForkManager { + export interface IOptions { + drive: ICollaborativeDrive; + eventManager: Event.IManager; + } +} diff --git a/packages/docprovider/src/index.ts b/packages/docprovider/src/index.ts index b3cd6572..178b7984 100644 --- a/packages/docprovider/src/index.ts +++ b/packages/docprovider/src/index.ts @@ -13,3 +13,5 @@ export * from './requests'; export * from './ydrive'; export * from './yprovider'; export * from './TimelineSlider'; +export * from './tokens'; +export * from './forkManager'; diff --git a/packages/docprovider/src/requests.ts b/packages/docprovider/src/requests.ts index 51a6ece3..0e374721 100644 --- a/packages/docprovider/src/requests.ts +++ b/packages/docprovider/src/requests.ts @@ -14,6 +14,8 @@ const DOC_SESSION_URL = 'api/collaboration/session'; const DOC_FORK_URL = 'api/collaboration/undo_redo'; const TIMELINE_URL = 'api/collaboration/timeline'; +export const ROOM_FORK_URL = 'api/collaboration/fork'; + /** * Document session model */ @@ -36,6 +38,45 @@ export interface ISessionModel { sessionId: string; } +/** + * Call the API extension + * + * @param endPoint API REST end point for the extension + * @param init Initial values for the request + * @returns The response body interpreted as JSON + */ +export async function requestAPI( + endPoint = '', + init: RequestInit = {} +): Promise { + // Make request to Jupyter API + const settings = ServerConnection.makeSettings(); + const requestUrl = URLExt.join(settings.baseUrl, endPoint); + + let response: Response; + try { + response = await ServerConnection.makeRequest(requestUrl, init, settings); + } catch (error) { + throw new ServerConnection.NetworkError(error as any); + } + + let data: any = await response.text(); + + if (data.length > 0) { + try { + data = JSON.parse(data); + } catch (error) { + console.error('Not a JSON response body.', response); + } + } + + if (!response.ok) { + throw new ServerConnection.ResponseError(response, data.message || data); + } + + return data; +} + export async function requestDocSession( format: string, type: string, diff --git a/packages/docprovider/src/tokens.ts b/packages/docprovider/src/tokens.ts new file mode 100644 index 00000000..e9aebc7e --- /dev/null +++ b/packages/docprovider/src/tokens.ts @@ -0,0 +1,43 @@ +import { Token } from '@lumino/coreutils'; +import { IDisposable } from '@lumino/disposable'; +import { ISignal } from '@lumino/signaling'; +export interface IForkInfo { + description?: string; + root_roomid: string; + synchronize: boolean; + title?: string; +} + +export interface IForkCreationResponse { + fork_info: IForkInfo; + fork_roomid: string; + sessionId: string; +} + +export interface IAllForksResponse { + [forkId: string]: IForkInfo; +} + +export interface IForkChangedEvent { + fork_info: IForkInfo; + fork_roomid: string; + username?: string; +} + +export interface IForkManager extends IDisposable { + createFork(options: { + rootId: string; + synchronize: boolean; + label?: string; + description?: string; + }): Promise; + getAllForks(documentId: string): Promise; + deleteFork(options: { forkId: string; merge: boolean }): Promise; + + forkAdded: ISignal; + forkDeleted: ISignal; +} + +export const IForkManagerToken = new Token( + '@jupyter/docprovider:IForkManagerToken' +); diff --git a/packages/docprovider/src/ydrive.ts b/packages/docprovider/src/ydrive.ts index 96a9106c..68a95723 100644 --- a/packages/docprovider/src/ydrive.ts +++ b/packages/docprovider/src/ydrive.ts @@ -308,7 +308,6 @@ class SharedModelFactory implements ISharedModelFactory { // the `sharedModel` will be the default one. return; } - if (this.documentFactories.has(options.contentType)) { const factory = this.documentFactories.get(options.contentType)!; const sharedModel = factory(options);