Skip to content

Commit

Permalink
Support placing the gallery in the new launcher (#5)
Browse files Browse the repository at this point in the history
* Support placing the gallery in the new launcher

* Fix test
  • Loading branch information
krassowski authored May 27, 2024
1 parent a53b961 commit 8118bd4
Show file tree
Hide file tree
Showing 8 changed files with 319 additions and 31 deletions.
2 changes: 1 addition & 1 deletion jupyterlab_gallery/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
)
)
Expand Down
2 changes: 1 addition & 1 deletion jupyterlab_gallery/tests/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
75 changes: 58 additions & 17 deletions src/gallery.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -16,26 +17,46 @@ export class GalleryWidget extends ReactWidget {
constructor(options: {
trans: TranslationBundle;
openPath: (path: string) => void;
fileChanged: Contents.IManager['fileChanged'];
refreshFileBrowser: () => Promise<void>;
}) {
const { trans } = options;
const { trans, fileChanged } = options;
super();
this._status = trans.__('Gallery loading...');
this._actions = {
open: async (exhibit: IExhibit) => {
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<void>((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);
Expand All @@ -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<IExhibitReply>('exhibits');
Expand All @@ -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 {
Expand All @@ -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 (
<Gallery
exhibits={this.exhibits}
actions={this._actions}
progressStream={this._stream}
/>
);
}
return <div className="jp-Gallery jp-mod-loading">{this._status}</div>;
return (
<UseSignal signal={this._update}>
{() => {
if (this.exhibits) {
return (
<Gallery
exhibits={this.exhibits}
actions={this._actions}
progressStream={this._stream}
/>
);
}
return (
<div className="jp-Gallery jp-mod-loading">{this._status}</div>
);
}}
</UseSignal>
);
}
private _update = new Signal<GalleryWidget, void>(this);
private _exhibits: IExhibit[] | null = null;
private _status: string;
private _actions: IActions;
Expand Down Expand Up @@ -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',
Expand Down
6 changes: 2 additions & 4 deletions src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}
42 changes: 34 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -18,12 +25,13 @@ const plugin: JupyterFrontEndPlugin<void> = {
'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!');

Expand All @@ -37,16 +45,34 @@ const plugin: JupyterFrontEndPlugin<void> = {
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);
Expand Down
4 changes: 4 additions & 0 deletions style/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@
.jp-Exhibit-icon {
max-width: 100%;
}

.jp-Launcher-openExample .jp-Gallery {
display: contents;
}
Loading

0 comments on commit 8118bd4

Please sign in to comment.