From be9a9a0a4031ec89ca2458f3c56e550d052e4451 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Wed, 2 Aug 2023 11:10:17 +0200 Subject: [PATCH 1/5] Add extension config --- voila/configuration.py | 8 ++++++++ voila/handler.py | 3 +-- voila/notebook_renderer.py | 1 - voila/tornado/treehandler.py | 3 +-- voila/utils.py | 16 ++++++---------- voila/voila_kernel_manager.py | 3 +-- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/voila/configuration.py b/voila/configuration.py index 6747ef64f..f94f4261f 100644 --- a/voila/configuration.py +++ b/voila/configuration.py @@ -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""", + ) diff --git a/voila/handler.py b/voila/handler.py index 753787c4e..db653aa2b 100644 --- a/voila/handler.py +++ b/voila/handler.py @@ -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, diff --git a/voila/notebook_renderer.py b/voila/notebook_renderer.py index 363b3d7fe..59c182bf0 100644 --- a/voila/notebook_renderer.py +++ b/voila/notebook_renderer.py @@ -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, ) diff --git a/voila/tornado/treehandler.py b/voila/tornado/treehandler.py index 14348e108..21c95420d 100644 --- a/voila/tornado/treehandler.py +++ b/voila/tornado/treehandler.py @@ -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 diff --git a/voila/utils.py b/voila/utils.py index f787ab09d..f53aa3586 100644 --- a/voila/utils.py +++ b/voila/utils.py @@ -8,12 +8,12 @@ ############################################################################# import asyncio -from copy import deepcopy import json import os import sys import threading import warnings +from copy import deepcopy from functools import partial from pathlib import Path from typing import Awaitable, Dict, List @@ -26,6 +26,7 @@ from markupsafe import Markup from ._version import __version__ +from .configuration import VoilaConfiguration from .static_file_handler import TemplateStaticFileHandler try: @@ -88,13 +89,7 @@ async def _get_request_info(ws_url: str) -> Awaitable: return ri -def get_page_config( - base_url, - settings, - log, - extension_allowlist: List[str] = [], - extension_denylist: List[str] = [], -): +def get_page_config(base_url, settings, log, voila_configuration: VoilaConfiguration): page_config = { "appVersion": __version__, "appUrl": "voila/", @@ -103,6 +98,7 @@ def get_page_config( "terminalsAvailable": False, "fullStaticUrl": url_path_join(base_url, "voila/static"), "fullLabextensionsUrl": url_path_join(base_url, "voila/labextensions"), + "extensionConfig": voila_configuration.extension_config, } mathjax_config = settings.get("mathjax_config") mathjax_url = settings.get("mathjax_url") @@ -129,8 +125,8 @@ def get_page_config( federated_extensions=federated_extensions, disabled_extensions=disabled_extensions, required_extensions=required_extensions, - extension_allowlist=extension_allowlist, - extension_denylist=extension_denylist, + extension_allowlist=voila_configuration.extension_allowlist, + extension_denylist=voila_configuration.extension_denylist, ) return page_config diff --git a/voila/voila_kernel_manager.py b/voila/voila_kernel_manager.py index 060aa9418..6ea3be49a 100644 --- a/voila/voila_kernel_manager.py +++ b/voila/voila_kernel_manager.py @@ -401,8 +401,7 @@ def _notebook_renderer_factory( base_url=self.parent.base_url, settings=self.parent.app.settings, log=self.parent.log, - extension_allowlist=voila_configuration.extension_allowlist, - extension_denylist=voila_configuration.extension_denylist, + voila_configuration=voila_configuration, ), mathjax_config=mathjax_config, mathjax_url=mathjax_url, From 7d15e5965a5b3c79f541a935a66d8b8821739f64 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Wed, 2 Aug 2023 12:13:17 +0200 Subject: [PATCH 2/5] Add widget to top of bottom of the shell --- packages/voila/src/app.ts | 7 +++++++ packages/voila/src/shell.ts | 22 +++++++++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/voila/src/app.ts b/packages/voila/src/app.ts index 3e1c7ea67..27ec9301f 100644 --- a/packages/voila/src/app.ts +++ b/packages/voila/src/app.ts @@ -37,6 +37,13 @@ export class VoilaApp extends JupyterFrontEnd { } } + protected attachShell(id: string): void { + if (this.shell.isAttached || this.shell.node.isConnected) { + // no-op + return; + } + super.attachShell(id); + } /** * The name of the application. */ diff --git a/packages/voila/src/shell.ts b/packages/voila/src/shell.ts index c890a1803..23e84e3cb 100644 --- a/packages/voila/src/shell.ts +++ b/packages/voila/src/shell.ts @@ -22,7 +22,7 @@ export namespace IShell { /** * The areas of the application shell where widgets can reside. */ - export type Area = 'main'; + export type Area = 'top' | 'bottom'; } /** @@ -30,8 +30,8 @@ export namespace IShell { */ export class VoilaShell extends Widget implements JupyterFrontEnd.IShell { constructor() { - super(); - this.id = 'main'; + const node = document.getElementById('rendered_cells'); + super(node ? { node } : undefined); } activateById(id: string): void { @@ -52,8 +52,20 @@ 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': + Widget.attach( + widget, + this.node, + this.node.firstElementChild as HTMLElement + ); + break; + case 'bottom': + Widget.attach(widget, this.node); + break; + default: + break; + } } /** From 2303d75dc4438926645c8226c9df080f0dadb04c Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Wed, 2 Aug 2023 18:49:58 +0200 Subject: [PATCH 3/5] Add layout to the shell widget --- packages/voila/src/plugins/widget.ts | 5 +++++ packages/voila/src/shell.ts | 14 ++++++++++---- packages/voila/style/base.css | 10 ++++++++++ packages/voila/style/index.css | 1 + packages/voila/style/index.js | 1 + 5 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 packages/voila/style/base.css create mode 100644 packages/voila/style/index.css diff --git a/packages/voila/src/plugins/widget.ts b/packages/voila/src/plugins/widget.ts index effddcc81..bddab5804 100644 --- a/packages/voila/src/plugins/widget.ts +++ b/packages/voila/src/plugins/widget.ts @@ -149,5 +149,10 @@ export const renderOutputsPlugin: JupyterFrontEndPlugin = { Widget.attach(output, container); } }); + const node = document.getElementById('rendered_cells'); + if (node) { + const cells = new Widget({ node }); + app.shell.add(cells, 'main'); + } } }; diff --git a/packages/voila/src/shell.ts b/packages/voila/src/shell.ts index 23e84e3cb..a5ffebc39 100644 --- a/packages/voila/src/shell.ts +++ b/packages/voila/src/shell.ts @@ -11,7 +11,7 @@ import { JupyterFrontEnd } from '@jupyterlab/application'; import { DocumentRegistry } from '@jupyterlab/docregistry'; -import { Widget } from '@lumino/widgets'; +import { BoxLayout, Widget } from '@lumino/widgets'; export type IShell = VoilaShell; @@ -22,7 +22,7 @@ export namespace IShell { /** * The areas of the application shell where widgets can reside. */ - export type Area = 'top' | 'bottom'; + export type Area = 'top' | 'bottom' | 'main'; } /** @@ -30,8 +30,11 @@ export namespace IShell { */ export class VoilaShell extends Widget implements JupyterFrontEnd.IShell { constructor() { - const node = document.getElementById('rendered_cells'); - super(node ? { node } : undefined); + super(); + this.id = 'main'; + const rootLayout = new BoxLayout(); + rootLayout.alignment = 'start'; + this.layout = rootLayout; } activateById(id: string): void { @@ -63,6 +66,9 @@ export class VoilaShell extends Widget implements JupyterFrontEnd.IShell { case 'bottom': Widget.attach(widget, this.node); break; + case 'main': + (this.layout as BoxLayout).addWidget(widget); + break; default: break; } diff --git a/packages/voila/style/base.css b/packages/voila/style/base.css new file mode 100644 index 000000000..1805971a0 --- /dev/null +++ b/packages/voila/style/base.css @@ -0,0 +1,10 @@ +body { + padding: 0 !important; +} +div#main { + height: 100vh; +} +div#rendered_cells { + padding: var(--jp-notebook-padding); + overflow: auto; +} diff --git a/packages/voila/style/index.css b/packages/voila/style/index.css new file mode 100644 index 000000000..8a7ea29e6 --- /dev/null +++ b/packages/voila/style/index.css @@ -0,0 +1 @@ +@import url('base.css'); diff --git a/packages/voila/style/index.js b/packages/voila/style/index.js index 770d93357..267061468 100644 --- a/packages/voila/style/index.js +++ b/packages/voila/style/index.js @@ -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'; From 35cf923bf8e0e7c730076c524b93281cee27205d Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Thu, 3 Aug 2023 11:35:26 +0200 Subject: [PATCH 4/5] Add top, main and bottom area --- packages/voila/src/app.ts | 7 - packages/voila/src/shell.ts | 233 +++++++++++++++++++++++++++++++--- packages/voila/style/base.css | 8 ++ 3 files changed, 224 insertions(+), 24 deletions(-) diff --git a/packages/voila/src/app.ts b/packages/voila/src/app.ts index 27ec9301f..3e1c7ea67 100644 --- a/packages/voila/src/app.ts +++ b/packages/voila/src/app.ts @@ -37,13 +37,6 @@ export class VoilaApp extends JupyterFrontEnd { } } - protected attachShell(id: string): void { - if (this.shell.isAttached || this.shell.node.isConnected) { - // no-op - return; - } - super.attachShell(id); - } /** * The name of the application. */ diff --git a/packages/voila/src/shell.ts b/packages/voila/src/shell.ts index a5ffebc39..3182706dc 100644 --- a/packages/voila/src/shell.ts +++ b/packages/voila/src/shell.ts @@ -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 { BoxLayout, 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; @@ -22,9 +23,27 @@ export namespace IShell { /** * The areas of the application shell where widgets can reside. */ - export type Area = 'top' | 'bottom' | '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. */ @@ -34,9 +53,39 @@ export class VoilaShell extends Widget implements JupyterFrontEnd.IShell { 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 { // no-op } @@ -57,31 +106,181 @@ export class VoilaShell extends Widget implements JupyterFrontEnd.IShell { ): void { switch (area) { case 'top': - Widget.attach( - widget, - this.node, - this.node.firstElementChild as HTMLElement - ); + this._addToTopArea(widget, options); break; case 'bottom': - Widget.attach(widget, this.node); + this._addToBottomArea(widget, options); break; case 'main': - (this.layout as BoxLayout).addWidget(widget); + this._mainPanel.addWidget(widget); break; default: + console.warn(`Area ${area} is not implemented yet!`); break; } } + widgets(area: IShell.Area): IterableIterator { + 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 { - 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); +} + +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 _panel = new Panel(); } } diff --git a/packages/voila/style/base.css b/packages/voila/style/base.css index 1805971a0..05ef1dab7 100644 --- a/packages/voila/style/base.css +++ b/packages/voila/style/base.css @@ -4,6 +4,14 @@ body { 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; From 7c0a7bd1195e717666792a4bed86e68a05d9c5af Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Thu, 3 Aug 2023 12:25:17 +0200 Subject: [PATCH 5/5] Update reveal template --- share/jupyter/voila/templates/reveal/index.html.j2 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/share/jupyter/voila/templates/reveal/index.html.j2 b/share/jupyter/voila/templates/reveal/index.html.j2 index 5de865a6a..281a9aebd 100644 --- a/share/jupyter/voila/templates/reveal/index.html.j2 +++ b/share/jupyter/voila/templates/reveal/index.html.j2 @@ -45,6 +45,9 @@ .jp-mod-noOutputs.jp-mod-noInput { display: none; } + #rendered_cells { + padding: 0px!important + }