diff --git a/jupyterlab_gallery/handlers.py b/jupyterlab_gallery/handlers.py index c2b5144..d5f3761 100644 --- a/jupyterlab_gallery/handlers.py +++ b/jupyterlab_gallery/handlers.py @@ -110,7 +110,7 @@ def get(self): prepare_exhibit(exhibit_config, exhibit_id=i) for i, exhibit_config in enumerate(exhibits) ], - "api_version": "1.0", + "apiVersion": "1.0", } ) ) diff --git a/jupyterlab_gallery/tests/test_handlers.py b/jupyterlab_gallery/tests/test_handlers.py index 4eb37d7..a62442f 100644 --- a/jupyterlab_gallery/tests/test_handlers.py +++ b/jupyterlab_gallery/tests/test_handlers.py @@ -6,4 +6,4 @@ async def test_exhibits(jp_fetch): assert response.code == 200 payload = json.loads(response.body) assert payload["exhibits"] - assert payload["api_version"] == "1.0" + assert payload["apiVersion"] == "1.0" diff --git a/package.json b/package.json index d257582..44e0e6c 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ }, "devDependencies": { "@jupyterlab/builder": "^4.0.0", + "@jupyterlab/launcher": "^4.0.0", "@jupyterlab/testutils": "^4.0.0", "@types/jest": "^29.2.0", "@types/json-schema": "^7.0.11", @@ -76,6 +77,7 @@ "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^5.0.0", "jest": "^29.2.0", + "jupyterlab-new-launcher": "^0.4.0", "mkdirp": "^1.0.3", "npm-run-all": "^4.1.5", "prettier": "^3.0.0", diff --git a/src/gallery.tsx b/src/gallery.tsx index 054a00f..f20060b 100644 --- a/src/gallery.tsx +++ b/src/gallery.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import { ReactWidget, showErrorMessage } from '@jupyterlab/apputils'; -import { Button } from '@jupyterlab/ui-components'; -import { IStream, Stream } from '@lumino/signaling'; +import { Button, UseSignal } from '@jupyterlab/ui-components'; +import { Contents } from '@jupyterlab/services'; +import { IStream, Stream, Signal } from '@lumino/signaling'; import { TranslationBundle } from '@jupyterlab/translation'; import { IExhibit } from './types'; import { IExhibitReply } from './types'; @@ -16,8 +17,10 @@ export class GalleryWidget extends ReactWidget { constructor(options: { trans: TranslationBundle; openPath: (path: string) => void; + fileChanged: Contents.IManager['fileChanged']; + refreshFileBrowser: () => Promise; }) { - const { trans } = options; + const { trans, fileChanged } = options; super(); this._status = trans.__('Gallery loading...'); this._actions = { @@ -25,17 +28,35 @@ export class GalleryWidget extends ReactWidget { options.openPath(exhibit.localPath); // TODO: should it open the directory in the file browser? // should it also open a readme for this repository? - //options. }, download: async (exhibit: IExhibit) => { + const done = new Promise((resolve, reject) => { + this._stream.connect((_, e) => { + if (e.exhibit_id === exhibit.id) { + if (e.phase === 'finished') { + resolve(); + } else if (e.phase === 'error') { + reject(); + } + } + }); + }); await requestAPI('pull', { method: 'POST', body: JSON.stringify({ exhibit_id: exhibit.id }) }); + await done; await this._load(); + await options.refreshFileBrowser(); } }; - eventStream( + // if user deletes a directory, reload the state + fileChanged.connect((_, args) => { + if (args.type === 'delete') { + this._load(); + } + }); + this._eventSource = eventStream( 'pull', message => { this._stream.emit(message); @@ -48,6 +69,13 @@ export class GalleryWidget extends ReactWidget { void this._load(); } + dispose() { + super.dispose(); + this._eventSource.close(); + } + + private _eventSource: EventSource; + private async _load() { try { const data = await requestAPI('exhibits'); @@ -61,6 +89,7 @@ export class GalleryWidget extends ReactWidget { } catch (reason) { this._status = `jupyterlab_gallery server failed:\n${reason}`; } + this.update(); } get exhibits(): IExhibit[] | null { @@ -69,21 +98,34 @@ export class GalleryWidget extends ReactWidget { set exhibits(value: IExhibit[] | null) { this._exhibits = value; - this.update(); + } + + update() { + super.update(); + this._update.emit(); } render(): JSX.Element { - if (this.exhibits) { - return ( - - ); - } - return
{this._status}
; + return ( + + {() => { + if (this.exhibits) { + return ( + + ); + } + return ( +
{this._status}
+ ); + }} +
+ ); } + private _update = new Signal(this); private _exhibits: IExhibit[] | null = null; private _status: string; private _actions: IActions; @@ -123,7 +165,6 @@ function Exhibit(props: { if (exhibitId !== exhibit.id) { return; } - console.log('matched stream', message); if (message.phase === 'error') { showErrorMessage( 'Could not download', diff --git a/src/handler.ts b/src/handler.ts index 4c2b8c7..2677412 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -55,7 +55,7 @@ export function eventStream( endPoint = '', onStream: (message: IStreamMessage) => void, onError: (error: Event) => void -) { +): EventSource { const settings = ServerConnection.makeSettings(); const requestUrl = URLExt.join( settings.baseUrl, @@ -65,12 +65,10 @@ export function eventStream( const eventSource = new EventSource(requestUrl); eventSource.addEventListener('message', event => { const data = JSON.parse(event.data); - if (data.phase === 'finished' || data.phase === 'error') { - eventSource.close(); - } onStream(data); }); eventSource.addEventListener('error', error => { onError(error); }); + return eventSource; } diff --git a/src/index.ts b/src/index.ts index 5a72380..241c6b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,9 +6,16 @@ import { ITranslator, nullTranslator } from '@jupyterlab/translation'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { IFileBrowserCommands } from '@jupyterlab/filebrowser'; +import { ILauncher } from '@jupyterlab/launcher'; +import type { INewLauncher } from 'jupyterlab-new-launcher/lib/types'; + import { GalleryWidget } from './gallery'; import { galleryIcon } from './icons'; +function isNewLauncher(launcher: ILauncher): launcher is INewLauncher { + return 'addSection' in launcher; +} + /** * Initialization data for the jupyterlab-gallery extension. */ @@ -18,12 +25,13 @@ const plugin: JupyterFrontEndPlugin = { 'A JupyterLab gallery extension for presenting and downloading examples from remote repositories', autoStart: true, requires: [ISettingRegistry], - optional: [IFileBrowserCommands, ITranslator], + optional: [IFileBrowserCommands, ITranslator, ILauncher], activate: async ( app: JupyterFrontEnd, settingRegistry: ISettingRegistry, fileBrowserCommands: IFileBrowserCommands | null, - translator: ITranslator | null + translator: ITranslator | null, + launcher: ILauncher | null ) => { console.log('JupyterLab extension jupyterlab-gallery is activated!'); @@ -37,16 +45,34 @@ const plugin: JupyterFrontEndPlugin = { throw Error('filebrowser not available'); } app.commands.execute(fileBrowserCommands.openPath, { path }); + }, + fileChanged: app.serviceManager.contents.fileChanged, + refreshFileBrowser: () => { + return app.commands.execute('filebrowser:refresh'); } }); - // TODO: should we put it in the sidebar, or in the main area? + const title = trans.__('Gallery'); // add the widget to sidebar before waiting for server reply to reduce UI jitter - widget.id = 'jupyterlab-gallery:sidebar'; - widget.title.icon = galleryIcon; - widget.title.caption = trans.__('Gallery'); - widget.show(); - app.shell.add(widget, 'left', { rank: 850 }); + if (launcher && isNewLauncher(launcher)) { + launcher.addSection({ + title, + className: 'jp-Launcher-openExample', + icon: galleryIcon, + id: 'gallery', + rank: 2.5, + render: () => { + return widget.render(); + } + }); + } else { + // fallback to placing it in the sidebar if new launcher is not installed + widget.id = 'jupyterlab-gallery:sidebar'; + widget.title.icon = galleryIcon; + widget.title.caption = title; + widget.show(); + app.shell.add(widget, 'left', { rank: 850 }); + } try { const settings = await settingRegistry.load(plugin.id); diff --git a/style/base.css b/style/base.css index 216a66f..201086a 100644 --- a/style/base.css +++ b/style/base.css @@ -22,3 +22,7 @@ .jp-Exhibit-icon { max-width: 100%; } + +.jp-Launcher-openExample .jp-Gallery { + display: contents; +} diff --git a/yarn.lock b/yarn.lock index 86e3ce7..9496dd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2156,6 +2156,35 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/apputils@npm:^4.3.1": + version: 4.3.1 + resolution: "@jupyterlab/apputils@npm:4.3.1" + dependencies: + "@jupyterlab/coreutils": ^6.2.1 + "@jupyterlab/observables": ^5.2.1 + "@jupyterlab/rendermime-interfaces": ^3.10.1 + "@jupyterlab/services": ^7.2.1 + "@jupyterlab/settingregistry": ^4.2.1 + "@jupyterlab/statedb": ^4.2.1 + "@jupyterlab/statusbar": ^4.2.1 + "@jupyterlab/translation": ^4.2.1 + "@jupyterlab/ui-components": ^4.2.1 + "@lumino/algorithm": ^2.0.1 + "@lumino/commands": ^2.3.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/domutils": ^2.0.1 + "@lumino/messaging": ^2.0.1 + "@lumino/signaling": ^2.1.2 + "@lumino/virtualdom": ^2.0.1 + "@lumino/widgets": ^2.3.2 + "@types/react": ^18.0.26 + react: ^18.2.0 + sanitize-html: ~2.12.1 + checksum: 380d9059dd14ee47bb50a821515e0b4a92a2b60b6fed2bf15fb73b9192a2e95d1e6c97337f11d0c26870dba2dc89ee19604f068483df505e78d798510a61bf01 + languageName: node + linkType: hard + "@jupyterlab/attachments@npm:^4.2.0": version: 4.2.0 resolution: "@jupyterlab/attachments@npm:4.2.0" @@ -2327,6 +2356,20 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/coreutils@npm:^6.2.1": + version: 6.2.1 + resolution: "@jupyterlab/coreutils@npm:6.2.1" + dependencies: + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/signaling": ^2.1.2 + minimist: ~1.2.0 + path-browserify: ^1.0.0 + url-parse: ~1.5.4 + checksum: c8167bd8d4472471297e5669d6b3ee7c9d5c1246e8413680713b15f8a81926d2c97bc6a3c0b26c16603b197b412e01b443cc74b02a3676adea5690aac41964be + languageName: node + linkType: hard + "@jupyterlab/docmanager@npm:^4.2.0": version: 4.2.0 resolution: "@jupyterlab/docmanager@npm:4.2.0" @@ -2425,6 +2468,24 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/launcher@npm:^4.0.0": + version: 4.2.1 + resolution: "@jupyterlab/launcher@npm:4.2.1" + dependencies: + "@jupyterlab/apputils": ^4.3.1 + "@jupyterlab/translation": ^4.2.1 + "@jupyterlab/ui-components": ^4.2.1 + "@lumino/algorithm": ^2.0.1 + "@lumino/commands": ^2.3.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/properties": ^2.0.1 + "@lumino/widgets": ^2.3.2 + react: ^18.2.0 + checksum: cb376579427da1e6be1f2d5aa159430df5c984421e61abe167244da384c8598bfe96523150e3a303a200e5e3a1bbb1c2e176d81aa0425ecb5cfda20764b604a8 + languageName: node + linkType: hard + "@jupyterlab/lsp@npm:^4.2.0": version: 4.2.0 resolution: "@jupyterlab/lsp@npm:4.2.0" @@ -2457,6 +2518,15 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/nbformat@npm:^4.2.1": + version: 4.2.1 + resolution: "@jupyterlab/nbformat@npm:4.2.1" + dependencies: + "@lumino/coreutils": ^2.1.2 + checksum: 192167e2a9019bf91e1e7088c9eaaae7b1037f5e7b5db15b97687b052323e6e75913b301ca7a9783d0e59aa36f18ddff90fc71a90a8153e0c89e32fd92b2519c + languageName: node + linkType: hard + "@jupyterlab/notebook@npm:^4.2.0": version: 4.2.0 resolution: "@jupyterlab/notebook@npm:4.2.0" @@ -2508,6 +2578,19 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/observables@npm:^5.2.1": + version: 5.2.1 + resolution: "@jupyterlab/observables@npm:5.2.1" + dependencies: + "@lumino/algorithm": ^2.0.1 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/messaging": ^2.0.1 + "@lumino/signaling": ^2.1.2 + checksum: 3833d3ad0640a6160fdc5254ec08a600e628e235103e311ca8ee90ade11b73e045ab78b82282153da700f9ae796a99ef36da223baad6c21ad7af0ea84b9514b6 + languageName: node + linkType: hard + "@jupyterlab/outputarea@npm:^4.2.0": version: 4.2.0 resolution: "@jupyterlab/outputarea@npm:4.2.0" @@ -2540,6 +2623,16 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/rendermime-interfaces@npm:^3.10.1": + version: 3.10.1 + resolution: "@jupyterlab/rendermime-interfaces@npm:3.10.1" + dependencies: + "@lumino/coreutils": ^1.11.0 || ^2.1.2 + "@lumino/widgets": ^1.37.2 || ^2.3.2 + checksum: 537fe7d96f8e157d89de0035149bf98bfaf1b9fde92d4f58c1e879ce87cab586311aa18dfb02a218bd24aa3cd1f24122e256a70cb2a0a437cc4fea1c9a3f2fa1 + languageName: node + linkType: hard + "@jupyterlab/rendermime@npm:^4.2.0": version: 4.2.0 resolution: "@jupyterlab/rendermime@npm:4.2.0" @@ -2579,6 +2672,25 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/services@npm:^7.2.1": + version: 7.2.1 + resolution: "@jupyterlab/services@npm:7.2.1" + dependencies: + "@jupyter/ydoc": ^2.0.1 + "@jupyterlab/coreutils": ^6.2.1 + "@jupyterlab/nbformat": ^4.2.1 + "@jupyterlab/settingregistry": ^4.2.1 + "@jupyterlab/statedb": ^4.2.1 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/polling": ^2.1.2 + "@lumino/properties": ^2.0.1 + "@lumino/signaling": ^2.1.2 + ws: ^8.11.0 + checksum: f07be2f3a174466c17ab5c22f8ef622fc623e8c61f2220b8bfb465a263971313cb9129e84bba32606e6ab7d1e0be3a9754b97f98e173e9c95eaf0b1c6cd8110a + languageName: node + linkType: hard + "@jupyterlab/settingregistry@npm:^4.0.0, @jupyterlab/settingregistry@npm:^4.2.0": version: 4.2.0 resolution: "@jupyterlab/settingregistry@npm:4.2.0" @@ -2598,6 +2710,25 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/settingregistry@npm:^4.2.1": + version: 4.2.1 + resolution: "@jupyterlab/settingregistry@npm:4.2.1" + dependencies: + "@jupyterlab/nbformat": ^4.2.1 + "@jupyterlab/statedb": ^4.2.1 + "@lumino/commands": ^2.3.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/signaling": ^2.1.2 + "@rjsf/utils": ^5.13.4 + ajv: ^8.12.0 + json5: ^2.2.3 + peerDependencies: + react: ">=16" + checksum: 794e5ecde19a40e1b95c0d636eed7b56bbdc46857c8f3b4ef446c1bc90e8ea660c2ccf8f36a238bc312002f106a5a8522bb057742d9c0d674b2974ef21a786d7 + languageName: node + linkType: hard + "@jupyterlab/statedb@npm:^4.2.0": version: 4.2.0 resolution: "@jupyterlab/statedb@npm:4.2.0" @@ -2611,6 +2742,19 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/statedb@npm:^4.2.1": + version: 4.2.1 + resolution: "@jupyterlab/statedb@npm:4.2.1" + dependencies: + "@lumino/commands": ^2.3.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/properties": ^2.0.1 + "@lumino/signaling": ^2.1.2 + checksum: 51e07db85269883bcd58fc5ba890db122e260e8d1ce4046f0b188453726694c2d909f27ca069ee3cd6944a93d70fcb8360074f87cdb13d611af2e24f6b14af30 + languageName: node + linkType: hard + "@jupyterlab/statusbar@npm:^4.2.0": version: 4.2.0 resolution: "@jupyterlab/statusbar@npm:4.2.0" @@ -2627,6 +2771,22 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/statusbar@npm:^4.2.1": + version: 4.2.1 + resolution: "@jupyterlab/statusbar@npm:4.2.1" + dependencies: + "@jupyterlab/ui-components": ^4.2.1 + "@lumino/algorithm": ^2.0.1 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/messaging": ^2.0.1 + "@lumino/signaling": ^2.1.2 + "@lumino/widgets": ^2.3.2 + react: ^18.2.0 + checksum: 65a0e4e0fa29ddd088d8dd2ee007a5166f783aa2852acd4217f2ed52fa04f492119c6e5b6e4f83884766fe7cfed3135ddd8c89b564ac3cc34ed6559457994885 + languageName: node + linkType: hard + "@jupyterlab/testing@npm:^4.2.0": version: 4.2.0 resolution: "@jupyterlab/testing@npm:4.2.0" @@ -2698,6 +2858,19 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/translation@npm:^4.2.1": + version: 4.2.1 + resolution: "@jupyterlab/translation@npm:4.2.1" + dependencies: + "@jupyterlab/coreutils": ^6.2.1 + "@jupyterlab/rendermime-interfaces": ^3.10.1 + "@jupyterlab/services": ^7.2.1 + "@jupyterlab/statedb": ^4.2.1 + "@lumino/coreutils": ^2.1.2 + checksum: 509c9fd8790f852faaa7f956c2ac660247a8d1610cb9f08fd5a357f784a7f32f838ac388a47626da66ee207769253d16ea72235d608112d560dbc10417d9b8e4 + languageName: node + linkType: hard + "@jupyterlab/ui-components@npm:^4.2.0": version: 4.2.0 resolution: "@jupyterlab/ui-components@npm:4.2.0" @@ -2729,6 +2902,37 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/ui-components@npm:^4.2.1": + version: 4.2.1 + resolution: "@jupyterlab/ui-components@npm:4.2.1" + dependencies: + "@jupyter/react-components": ^0.15.3 + "@jupyter/web-components": ^0.15.3 + "@jupyterlab/coreutils": ^6.2.1 + "@jupyterlab/observables": ^5.2.1 + "@jupyterlab/rendermime-interfaces": ^3.10.1 + "@jupyterlab/translation": ^4.2.1 + "@lumino/algorithm": ^2.0.1 + "@lumino/commands": ^2.3.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/messaging": ^2.0.1 + "@lumino/polling": ^2.1.2 + "@lumino/properties": ^2.0.1 + "@lumino/signaling": ^2.1.2 + "@lumino/virtualdom": ^2.0.1 + "@lumino/widgets": ^2.3.2 + "@rjsf/core": ^5.13.4 + "@rjsf/utils": ^5.13.4 + react: ^18.2.0 + react-dom: ^18.2.0 + typestyle: ^2.0.4 + peerDependencies: + react: ^18.2.0 + checksum: 7032d7755a7b69e98acc6378d9dedcc56d016cd0d4d6091bda3593baf89876a5e00f84116ab2a5ab5cc68439e07c2194eb7d211b6b3cff0a03cdfd11b03951bd + languageName: node + linkType: hard + "@lezer/common@npm:^1.0.0, @lezer/common@npm:^1.0.2, @lezer/common@npm:^1.1.0, @lezer/common@npm:^1.2.0, @lezer/common@npm:^1.2.1": version: 1.2.1 resolution: "@lezer/common@npm:1.2.1" @@ -7246,6 +7450,7 @@ __metadata: "@jupyterlab/application": ^4.0.0 "@jupyterlab/builder": ^4.0.0 "@jupyterlab/coreutils": ^6.0.0 + "@jupyterlab/launcher": ^4.0.0 "@jupyterlab/services": ^7.0.0 "@jupyterlab/settingregistry": ^4.0.0 "@jupyterlab/testutils": ^4.0.0 @@ -7260,6 +7465,7 @@ __metadata: eslint-config-prettier: ^8.8.0 eslint-plugin-prettier: ^5.0.0 jest: ^29.2.0 + jupyterlab-new-launcher: ^0.4.0 mkdirp: ^1.0.3 npm-run-all: ^4.1.5 prettier: ^3.0.0 @@ -7276,6 +7482,17 @@ __metadata: languageName: unknown linkType: soft +"jupyterlab-new-launcher@npm:^0.4.0": + version: 0.4.0 + resolution: "jupyterlab-new-launcher@npm:0.4.0" + dependencies: + "@jupyterlab/application": ^4.0.0 + "@jupyterlab/launcher": ^4.0.0 + "@jupyterlab/settingregistry": ^4.0.0 + checksum: a963895f4f651bb69cf5cc50b5f61810f6643920ae7faad2a6e0a971769b5804b2d1e778cda8a10610303ddb3d0dee382b2c1d158960e69f59d0ea122c191e1b + languageName: node + linkType: hard + "keyv@npm:^4.5.3": version: 4.5.4 resolution: "keyv@npm:4.5.4"