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

Update Voila shell #1369

Merged
merged 5 commits into from
Aug 3, 2023
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
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
Loading