Skip to content

Commit

Permalink
Update Voila shell (#1369)
Browse files Browse the repository at this point in the history
* Add extension config

* Add widget to top of bottom of the shell

* Add layout to the shell widget

* Add top, main and bottom area

* Update reveal template
  • Loading branch information
trungleduc authored Aug 3, 2023
1 parent 8c090ee commit a9c29a8
Show file tree
Hide file tree
Showing 12 changed files with 274 additions and 29 deletions.
5 changes: 5 additions & 0 deletions packages/voila/src/plugins/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,5 +149,10 @@ export const renderOutputsPlugin: JupyterFrontEndPlugin<void> = {
Widget.attach(output, container);
}
});
const node = document.getElementById('rendered_cells');
if (node) {
const cells = new Widget({ node });
app.shell.add(cells, 'main');
}
}
};
241 changes: 229 additions & 12 deletions packages/voila/src/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
* *
* The full license is in the file LICENSE, distributed with this software. *
****************************************************************************/

import { JupyterFrontEnd } from '@jupyterlab/application';

import { DocumentRegistry } from '@jupyterlab/docregistry';

import { Widget } from '@lumino/widgets';
import { ArrayExt } from '@lumino/algorithm';
import { IMessageHandler, Message, MessageLoop } from '@lumino/messaging';
import { Debouncer } from '@lumino/polling';
import { Signal } from '@lumino/signaling';
import { BoxLayout, BoxPanel, Panel, Widget } from '@lumino/widgets';

export type IShell = VoilaShell;

Expand All @@ -22,16 +23,67 @@ export namespace IShell {
/**
* The areas of the application shell where widgets can reside.
*/
export type Area = 'main';
export type Area =
| 'main'
| 'header'
| 'top'
| 'menu'
| 'left'
| 'right'
| 'bottom'
| 'down';
}

/**
* The class name added to AppShell instances.
*/
const APPLICATION_SHELL_CLASS = 'jp-LabShell';

/**
* The default rank of items added to a sidebar.
*/
const DEFAULT_RANK = 900;

/**
* The application shell.
*/
export class VoilaShell extends Widget implements JupyterFrontEnd.IShell {
constructor() {
super();
this.id = 'main';
const rootLayout = new BoxLayout();
rootLayout.alignment = 'start';
rootLayout.spacing = 0;
this.addClass(APPLICATION_SHELL_CLASS);

const topHandler = (this._topHandler = new Private.PanelHandler());
topHandler.panel.id = 'voila-top-panel';
topHandler.panel.node.setAttribute('role', 'banner');
BoxLayout.setStretch(topHandler.panel, 0);
topHandler.panel.hide();
rootLayout.addWidget(topHandler.panel);

const hboxPanel = (this._mainPanel = new BoxPanel());
hboxPanel.id = 'jp-main-content-panel';
hboxPanel.direction = 'top-to-bottom';
BoxLayout.setStretch(hboxPanel, 1);
rootLayout.addWidget(hboxPanel);

const bottomPanel = (this._bottomPanel = new Panel());
bottomPanel.node.setAttribute('role', 'contentinfo');
bottomPanel.id = 'voila-bottom-panel';
BoxLayout.setStretch(bottomPanel, 0);
rootLayout.addWidget(bottomPanel);
bottomPanel.hide();

this.layout = rootLayout;
}

/**
* The current widget in the shell's main area.
*/
get currentWidget(): Widget | null {
return this._mainPanel.widgets[0];
}

activateById(id: string): void {
Expand All @@ -52,18 +104,183 @@ export class VoilaShell extends Widget implements JupyterFrontEnd.IShell {
area?: IShell.Area,
options?: DocumentRegistry.IOpenOptions
): void {
// no-op for now
// TODO: support adding widgets to areas?
switch (area) {
case 'top':
this._addToTopArea(widget, options);
break;
case 'bottom':
this._addToBottomArea(widget, options);
break;
case 'main':
this._mainPanel.addWidget(widget);
break;
default:
console.warn(`Area ${area} is not implemented yet!`);
break;
}
}

widgets(area: IShell.Area): IterableIterator<Widget> {
switch (area) {
case 'top':
return this._topHandler.panel.children();
case 'bottom':
return this._bottomPanel.children();
case 'main':
this._mainPanel.children();
break;
default:
return [][Symbol.iterator]();
}
return [][Symbol.iterator]();
}

/**
* The current widget in the shell's main area.
* Add a widget to the top content area.
*
* #### Notes
* Widgets must have a unique `id` property, which will be used as the DOM id.
*/
get currentWidget(): Widget | null {
return null;
private _addToTopArea(
widget: Widget,
options?: DocumentRegistry.IOpenOptions
): void {
if (!widget.id) {
console.error('Widgets added to app shell must have unique id property.');
return;
}
options = options || {};
const rank = options.rank ?? DEFAULT_RANK;
this._topHandler.addWidget(widget, rank);
this._onLayoutModified();
if (this._topHandler.panel.isHidden) {
this._topHandler.panel.show();
}
}

widgets(area: IShell.Area): IterableIterator<Widget> {
return [][Symbol.iterator]();
/**
* Add a widget to the bottom content area.
*
* #### Notes
* Widgets must have a unique `id` property, which will be used as the DOM id.
*/
private _addToBottomArea(
widget: Widget,
options?: DocumentRegistry.IOpenOptions
): void {
if (!widget.id) {
console.error('Widgets added to app shell must have unique id property.');
return;
}
this._bottomPanel.addWidget(widget);
this._onLayoutModified();

if (this._bottomPanel.isHidden) {
this._bottomPanel.show();
}
}

/**
* Handle a change to the layout.
*/
private _onLayoutModified(): void {
void this._layoutDebouncer.invoke();
}

private _topHandler: Private.PanelHandler;
private _mainPanel: BoxPanel;
private _bottomPanel: Panel;
private _layoutDebouncer = new Debouncer(() => {
this._layoutModified.emit(undefined);
}, 0);
private _layoutModified = new Signal<this, void>(this);
}

namespace Private {
/**
* An object which holds a widget and its sort rank.
*/
export interface IRankItem {
/**
* The widget for the item.
*/
widget: Widget;

/**
* The sort rank of the widget.
*/
rank: number;
}

/**
* A less-than comparison function for side bar rank items.
*/
export function itemCmp(first: IRankItem, second: IRankItem): number {
return first.rank - second.rank;
}

/**
* A class which manages a panel and sorts its widgets by rank.
*/
export class PanelHandler {
constructor() {
MessageLoop.installMessageHook(this._panel, this._panelChildHook);
}

/**
* Get the panel managed by the handler.
*/
get panel(): Panel {
return this._panel;
}

/**
* Add a widget to the panel.
*
* If the widget is already added, it will be moved.
*/
addWidget(widget: Widget, rank: number): void {
widget.parent = null;
const item = { widget, rank };
const index = ArrayExt.upperBound(this._items, item, Private.itemCmp);
ArrayExt.insert(this._items, index, item);
this._panel.insertWidget(index, widget);
}

/**
* A message hook for child add/remove messages on the main area dock panel.
*/
private _panelChildHook = (
handler: IMessageHandler,
msg: Message
): boolean => {
switch (msg.type) {
case 'child-added':
{
const widget = (msg as Widget.ChildMessage).child;
// If we already know about this widget, we're done
if (this._items.find((v) => v.widget === widget)) {
break;
}

// Otherwise, add to the end by default
const rank = this._items[this._items.length - 1].rank;
this._items.push({ widget, rank });
}
break;
case 'child-removed':
{
const widget = (msg as Widget.ChildMessage).child;
ArrayExt.removeFirstWhere(this._items, (v) => v.widget === widget);
}
break;
default:
break;
}
return true;
};

private _items = new Array<Private.IRankItem>();
private _panel = new Panel();
}
}
18 changes: 18 additions & 0 deletions packages/voila/style/base.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
body {
padding: 0 !important;
}
div#main {
height: 100vh;
}
div#voila-top-panel {
min-height: var(--jp-private-menubar-height);
display: flex;
}
div#voila-bottom-panel {
min-height: var(--jp-private-menubar-height);
display: flex;
}
div#rendered_cells {
padding: var(--jp-notebook-padding);
overflow: auto;
}
1 change: 1 addition & 0 deletions packages/voila/style/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import url('base.css');
1 change: 1 addition & 0 deletions packages/voila/style/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ import '@jupyterlab/apputils/style/index.js';
import '@jupyterlab/rendermime/style/index.js';
import '@jupyterlab/docregistry/style/index.js';
import '@jupyterlab/markedparser-extension/style/index.js';
import './base.css';
3 changes: 3 additions & 0 deletions share/jupyter/voila/templates/reveal/index.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
.jp-mod-noOutputs.jp-mod-noInput {
display: none;
}
#rendered_cells {
padding: 0px!important
}
</style>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@^5/css/all.min.css" type="text/css" />
Expand Down
8 changes: 8 additions & 0 deletions voila/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,11 @@ def _valid_file_blacklist(self, proposal):
config=True,
help="""The list of disabled JupyterLab extensions, if `None`, all extensions are loaded""",
)

extension_config = Dict(
None,
allow_none=True,
config=True,
help="""The dictionary of extension configuration, this dict is passed to the frontend
through the PageConfig""",
)
3 changes: 1 addition & 2 deletions voila/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,7 @@ async def get_generator(self, path=None):
base_url=self.base_url,
settings=self.settings,
log=self.log,
extension_allowlist=self.voila_configuration.extension_allowlist,
extension_denylist=self.voila_configuration.extension_denylist,
voila_configuration=self.voila_configuration,
),
mathjax_config=mathjax_config,
mathjax_url=mathjax_url,
Expand Down
1 change: 0 additions & 1 deletion voila/notebook_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ async def initialize(self, **kwargs) -> None:
contents_manager=self.contents_manager, # for the image inlining
theme=self.theme, # we now have the theme in two places
base_url=self.base_url,
page_config=self.page_config,
show_margins=self.voila_configuration.show_margins,
mathjax_url=mathjax_full_url,
)
Expand Down
3 changes: 1 addition & 2 deletions voila/tornado/treehandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ def allowed_content(content):
base_url=self.base_url,
settings=self.settings,
log=self.log,
extension_allowlist=self.voila_configuration.extension_allowlist,
extension_denylist=self.voila_configuration.extension_denylist,
voila_configuration=self.voila_configuration,
)
page_config["jupyterLabTheme"] = theme_arg

Expand Down
Loading

0 comments on commit a9c29a8

Please sign in to comment.