Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support placing the gallery in the new launcher #5

Merged
merged 2 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading