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

Merge RetroLab with the JupyterLab Simple Interface? #257

Closed
jtpio opened this issue Oct 25, 2021 · 7 comments
Closed

Merge RetroLab with the JupyterLab Simple Interface? #257

jtpio opened this issue Oct 25, 2021 · 7 comments
Labels
enhancement New feature or request

Comments

@jtpio
Copy link
Member

jtpio commented Oct 25, 2021

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.

@jtpio jtpio added the enhancement New feature or request label Oct 25, 2021
@yuvipanda
Copy link
Contributor

I think this would be an extremely awesome and important step in increasing adoption of JupyterLab in educational settings. There are many places where current JupyterLab is basically a non-starter, and Retrolab is the way in. Having it integrated into JupyterLab itself - with the changes you have proposed - would make my life advocating for this much much much much much easier.

@gutow
Copy link
Contributor

gutow commented Oct 25, 2021

I think this would be good idea. Easing the pathway to adoption would be very good!

@isabela-pf
Copy link
Contributor

I already said it on jupyterlab/jupyterlab #9869, but I agree this seems much more robust that JupyterLab's current simple mode. I would see this as a positive change.

@SylvainCorlay
Copy link
Member

Quick note: I think that the "simple mode" is interesting beyond the notebook UI - and I would find it interesting to keep something like this around.

@Carreau
Copy link

Carreau commented Oct 26, 2021

+1, even before merge, I'd love for jupyter default installation to have "$ jupyter notebook" show you a screen where you can choose between classic / retrolab, then we can slowly iterate on this screen to nudge users toward more recent codebase.

@choldgraf
Copy link

FWIW I think this is a great idea. Retrolab is a much more familiar interface for those coming from notebook and it's simplicity is IMO a great feature. For any group considering switching from the notebook interface, I think retrolab is a much easier stepping stone, and it may be the only option for those who don't want to expose people to the complexity of the lab interface (eg, in teaching situations)

@jtpio
Copy link
Member Author

jtpio commented Jan 7, 2022

Closing as the JEP proposing to make Notebook v7 based on the current RetroLab has now been merged: jupyter/enhancement-proposals#79

Check out the full description of the JEP to learn more: https://jupyter.org/enhancement-proposals/79-notebook-v7/notebook-v7.html

Thanks all!

@jtpio jtpio closed this as completed Jan 7, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

7 participants