Skip to content
This repository was archived by the owner on Feb 16, 2023. It is now read-only.
This repository was archived by the owner on Feb 16, 2023. It is now read-only.

Merge RetroLab with the JupyterLab Simple Interface? #257

Closed
@jtpio

Description

@jtpio

Problem

The Simple Interface in JupyterLab has been around for a while, and was improved for the 3.0 release:

1*ZLDBAmTHYyGVp3RNQN09KQ

RetroLab was developed a couple of weeks before the 3.0 release, back in December 2020. The goal was to develop a notebook UI that looks like the classic notebook, but made with JupyterLab components. The main motivation was this issue: jupyterlab/jupyterlab#8450

However as RetroLab matures and starts looking more and more like the classic notebook, we should consider merging the two efforts to offer a consistent frontend to the users.

Here is a screenshot comparing RetroLab to the Classic Notebook:

image

Proposed Solution

If we want to unify the Simple Interface and RetroLab, and eventually offer RetroLab as part of the JupyterLab main package, we should consider a few UI, UX and technical details.

Left, right and botton areas

For now RetroLab does not expose any left, right, or bottom area. This is on purpose, to keep the interface as simple as possible.

However the Simple Interface still gives access to these areas. Which can be convenient for example for the Table of Contents or the Debugger.

Ideally RetroLab should keep good defaults: as simple as possible by default. These areas can exist, but should be hidden by default. More advanced users can toggle them on if they want to.

RetroShell vs LabShell

This is slightly similar to the previous point. For now RetroLab uses a custom shell:

/**
* The application shell.
*/
export class RetroShell extends Widget implements JupyterFrontEnd.IShell {
constructor() {
super();
this.id = 'main';
const rootLayout = new BoxLayout();
this._topHandler = new Private.PanelHandler();
this._menuHandler = new Private.PanelHandler();
this._main = new Panel();
this._topHandler.panel.id = 'top-panel';
this._menuHandler.panel.id = 'menu-panel';
this._main.id = 'main-panel';
// create wrappers around the top and menu areas
const topWrapper = (this._topWrapper = new Panel());
topWrapper.id = 'top-panel-wrapper';
topWrapper.addWidget(this._topHandler.panel);
const menuWrapper = (this._menuWrapper = new Panel());
menuWrapper.id = 'menu-panel-wrapper';
menuWrapper.addWidget(this._menuHandler.panel);
BoxLayout.setStretch(topWrapper, 0);
BoxLayout.setStretch(menuWrapper, 0);
BoxLayout.setStretch(this._main, 1);
this._spacer = new Widget();
this._spacer.id = 'spacer-widget';
rootLayout.spacing = 0;
rootLayout.addWidget(topWrapper);
rootLayout.addWidget(menuWrapper);
rootLayout.addWidget(this._spacer);
rootLayout.addWidget(this._main);
this.layout = rootLayout;
}
/**
* A signal emitted when the current widget changes.
*/
get currentChanged(): ISignal<RetroShell, void> {
return this._currentChanged;
}
/**
* The current widget in the shell's main area.
*/
get currentWidget(): Widget | null {
return this._main.widgets[0] ?? null;
}
/**
* Get the top area wrapper panel
*/
get top(): Widget {
return this._topWrapper;
}
/**
* Get the menu area wrapper panel
*/
get menu(): Widget {
return this._menuWrapper;
}
/**
* Activate a widget in its area.
*/
activateById(id: string): void {
const widget = find(this.widgets('main'), w => w.id === id);
if (widget) {
widget.activate();
}
}
/**
* Add a widget to the application shell.
*
* @param widget - The widget being added.
*
* @param area - Optional region in the shell into which the widget should
* be added.
*
* @param options - Optional open options.
*
*/
add(
widget: Widget,
area?: Shell.Area,
options?: DocumentRegistry.IOpenOptions
): void {
const rank = options?.rank ?? DEFAULT_RANK;
if (area === 'top') {
return this._topHandler.addWidget(widget, rank);
}
if (area === 'menu') {
return this._menuHandler.addWidget(widget, rank);
}
if (area === 'main' || area === undefined) {
if (this._main.widgets.length > 0) {
// do not add the widget if there is already one
return;
}
this._main.addWidget(widget);
this._main.update();
this._currentChanged.emit(void 0);
}
}
/**
* Collapse the top area and the spacer to make the view more compact.
*/
collapseTop(): void {
this._topWrapper.setHidden(true);
this._spacer.setHidden(true);
}
/**
* Expand the top area to show the header and the spacer.
*/
expandTop(): void {
this._topWrapper.setHidden(false);
this._spacer.setHidden(false);
}
/**
* Return the list of widgets for the given area.
*
* @param area The area
*/
widgets(area: Shell.Area): IIterator<Widget> {
switch (area ?? 'main') {
case 'top':
return iter(this._topHandler.panel.widgets);
case 'menu':
return iter(this._menuHandler.panel.widgets);
case 'main':
return iter(this._main.widgets);
default:
throw new Error(`Invalid area: ${area}`);
}
}
private _topWrapper: Panel;
private _topHandler: Private.PanelHandler;
private _menuWrapper: Panel;
private _menuHandler: Private.PanelHandler;
private _spacer: Widget;
private _main: Panel;
private _currentChanged = new Signal<this, void>(this);
}

And a custom app:

/**
* App is the main application class. It is instantiated once and shared.
*/
export class RetroApp extends JupyterFrontEnd<IRetroShell> {
/**
* Construct a new RetroApp object.
*
* @param options The instantiation options for an application.
*/
constructor(options: RetroApp.IOptions = { shell: new RetroShell() }) {
super({
...options,
shell: options.shell ?? new RetroShell()
});
if (options.mimeExtensions) {
for (const plugin of createRendermimePlugins(options.mimeExtensions)) {
this.registerPlugin(plugin);
}
}
void this._formatter.invoke();
}
/**
* The name of the application.
*/
readonly name = 'RetroLab';
/**
* A namespace/prefix plugins may use to denote their provenance.
*/
readonly namespace = this.name;
/**
* The application busy and dirty status signals and flags.
*/
readonly status = new LabStatus(this);
/**
* The version of the application.
*/
readonly version = PageConfig.getOption('appVersion') ?? 'unknown';
/**
* The JupyterLab application paths dictionary.
*/
get paths(): JupyterFrontEnd.IPaths {
return {
urls: {
base: PageConfig.getOption('baseUrl'),
notFound: PageConfig.getOption('notFoundUrl'),
app: PageConfig.getOption('appUrl'),
static: PageConfig.getOption('staticUrl'),
settings: PageConfig.getOption('settingsUrl'),
themes: PageConfig.getOption('themesUrl'),
doc: PageConfig.getOption('docUrl'),
translations: PageConfig.getOption('translationsApiUrl'),
hubHost: PageConfig.getOption('hubHost') || undefined,
hubPrefix: PageConfig.getOption('hubPrefix') || undefined,
hubUser: PageConfig.getOption('hubUser') || undefined,
hubServerName: PageConfig.getOption('hubServerName') || undefined
},
directories: {
appSettings: PageConfig.getOption('appSettingsDir'),
schemas: PageConfig.getOption('schemasDir'),
static: PageConfig.getOption('staticDir'),
templates: PageConfig.getOption('templatesDir'),
themes: PageConfig.getOption('themesDir'),
userSettings: PageConfig.getOption('userSettingsDir'),
serverRoot: PageConfig.getOption('serverRoot'),
workspaces: PageConfig.getOption('workspacesDir')
}
};
}
/**
* Handle the DOM events for the application.
*
* @param event - The DOM event sent to the application.
*/
handleEvent(event: Event): void {
super.handleEvent(event);
if (event.type === 'resize') {
void this._formatter.invoke();
}
}
/**
* Register plugins from a plugin module.
*
* @param mod - The plugin module to register.
*/
registerPluginModule(mod: RetroApp.IPluginModule): void {
let data = mod.default;
// Handle commonjs exports.
if (!Object.prototype.hasOwnProperty.call(mod, '__esModule')) {
data = mod as any;
}
if (!Array.isArray(data)) {
data = [data];
}
data.forEach(item => {
try {
this.registerPlugin(item);
} catch (error) {
console.error(error);
}
});
}
/**
* Register the plugins from multiple plugin modules.
*
* @param mods - The plugin modules to register.
*/
registerPluginModules(mods: RetroApp.IPluginModule[]): void {
mods.forEach(mod => {
this.registerPluginModule(mod);
});
}
private _formatter = new Throttler(() => {
Private.setFormat(this);
}, 250);
}

RetroLab already supports the prebuilt JupyterLab extensions: https://github.com/jupyterlab/retrolab#support-for-prebuilt-extensions-. Most of them work just fine. However if for example a third-party extension adds a widget to the left area, nothing happens in RetroLab.

Maybe we should look into using the same shell and app for both lab and retro? This would have the advantage of better compatibility with third-party extensions that expect some areas or component to be available.

Open in a new browser tab

It should be possible to open the notebooks, terminals and the file browser in different browser tabs.

Also this sounds like a minor detail, but there should still not be any splash screen in Retro / Simple Interface. A splash screen indicates this is a heavier app while in RetroLab we only expect to be browsing a "normal" web page.

Seamless switch between JupyterLab and RetroLab / Simple Interface

RetroLab ships with a prebuilt lab extension by default, which adds buttons to the notebook to easily switch between JupyterLab, RetroLab and the Classic Notebook.

In RetroLab:

image

In JupyterLab:

image

These are simple buttons, but they already let users easily switch between interfaces with a single click. We should iterate on that idea: #256

More generic way of opening JupyterLab widgets in new browser tabs?

This might be more of a long term experiment. It would be interesting to be able to open arbitrary widgets in new browser tabs. Taking the inspiration from the current RetroLab, but with more widgets than just the notebook, console, terminal and file editor.

Writing this here as something to keep in mind while we iterate on the other items.

Additional context

There have been a lot of discussions in jupyterlab/jupyterlab#9869

Although RetroLab already looks and feels a lot like the classic notebook, a few concerns have also been expressed in jupyter/notebook#6210.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions