diff --git a/app/index.js b/app/index.js index aa67a3e639..2be1ad9ffe 100644 --- a/app/index.js +++ b/app/index.js @@ -83,6 +83,7 @@ async function main() { require('@jupyterlab/apputils-extension').default.filter(({ id }) => [ '@jupyterlab/apputils-extension:palette', + '@jupyter/apputils-extension:sanitizer', '@jupyterlab/apputils-extension:settings', '@jupyterlab/apputils-extension:state', '@jupyterlab/apputils-extension:themes', @@ -175,11 +176,30 @@ async function main() { } case 'notebooks': { baseMods = baseMods.concat([ + require('@jupyterlab/celltags-extension'), require('@jupyterlab/cell-toolbar-extension'), + require('@jupyterlab/debugger-extension').default.filter(({ id }) => + [ + '@jupyterlab/debugger-extension:config', + '@jupyterlab/debugger-extension:main', + '@jupyterlab/debugger-extension:notebooks', + '@jupyterlab/debugger-extension:service', + '@jupyterlab/debugger-extension:sidebar', + '@jupyterlab/debugger-extension:sources' + ].includes(id) + ), require('@jupyterlab/notebook-extension').default.filter(({ id }) => [ '@jupyterlab/notebook-extension:completer', - '@jupyterlab/notebook-extension:search' + '@jupyterlab/notebook-extension:search', + '@jupyterlab/notebook-extension:toc', + '@jupyterlab/notebook-extension:tools' + ].includes(id) + ), + require('@jupyterlab/toc-extension').default.filter(({ id }) => + [ + '@jupyterlab/toc-extension:registry', + '@jupyterlab/toc-extension:tracker' ].includes(id) ), require('@jupyterlab/tooltip-extension').default.filter(({ id }) => diff --git a/app/package.json b/app/package.json index 821a59d41b..75f23604cf 100644 --- a/app/package.json +++ b/app/package.json @@ -29,6 +29,7 @@ "@jupyterlab/cell-toolbar": "~4.0.0-alpha.14", "@jupyterlab/cell-toolbar-extension": "~4.0.0-alpha.14", "@jupyterlab/celltags": "~4.0.0-alpha.14", + "@jupyterlab/celltags-extension": "~4.0.0-alpha.14", "@jupyterlab/codeeditor": "~4.0.0-alpha.14", "@jupyterlab/codemirror-extension": "~4.0.0-alpha.14", "@jupyterlab/collaboration": "~4.0.0-alpha.14", @@ -76,6 +77,7 @@ "@jupyterlab/terminal-extension": "~4.0.0-alpha.14", "@jupyterlab/theme-dark-extension": "~4.0.0-alpha.14", "@jupyterlab/theme-light-extension": "~4.0.0-alpha.14", + "@jupyterlab/toc-extension": "~6.0.0-alpha.14", "@jupyterlab/tooltip": "~4.0.0-alpha.14", "@jupyterlab/tooltip-extension": "~4.0.0-alpha.14", "@jupyterlab/translation": "~4.0.0-alpha.14", @@ -114,11 +116,13 @@ "@jupyterlab/apputils-extension": "^4.0.0-alpha.14", "@jupyterlab/cell-toolbar-extension": "^4.0.0-alpha.14", "@jupyterlab/celltags": "^4.0.0-alpha.14", + "@jupyterlab/celltags-extension": "^4.0.0-alpha.14", "@jupyterlab/codemirror-extension": "^4.0.0-alpha.14", "@jupyterlab/collaboration-extension": "^4.0.0-alpha.14", "@jupyterlab/completer-extension": "^4.0.0-alpha.14", "@jupyterlab/console-extension": "^4.0.0-alpha.14", "@jupyterlab/coreutils": "^6.0.0-alpha.14", + "@jupyterlab/debugger-extension": "^4.0.0-alpha.14", "@jupyterlab/docmanager-extension": "^4.0.0-alpha.14", "@jupyterlab/docprovider-extension": "^4.0.0-alpha.14", "@jupyterlab/documentsearch-extension": "^4.0.0-alpha.14", @@ -140,6 +144,7 @@ "@jupyterlab/terminal-extension": "^4.0.0-alpha.14", "@jupyterlab/theme-dark-extension": "^4.0.0-alpha.14", "@jupyterlab/theme-light-extension": "^4.0.0-alpha.14", + "@jupyterlab/toc-extension": "^6.0.0-alpha.14", "@jupyterlab/tooltip-extension": "^4.0.0-alpha.14", "@jupyterlab/translation-extension": "^4.0.0-alpha.14", "@jupyterlab/vega5-extension": "^4.0.0-alpha.14" @@ -181,6 +186,7 @@ "@jupyterlab/codemirror-extension", "@jupyterlab/completer-extension", "@jupyterlab/console-extension", + "@jupyterlab/debugger-extension", "@jupyterlab/docmanager-extension", "@jupyterlab/documentsearch-extension", "@jupyterlab/filebrowser-extension", @@ -211,6 +217,7 @@ "@jupyterlab/completer", "@jupyterlab/console", "@jupyterlab/coreutils", + "@jupyterlab/debugger", "@jupyterlab/docmanager", "@jupyterlab/docprovider", "@jupyterlab/documentsearch", diff --git a/packages/application-extension/src/index.ts b/packages/application-extension/src/index.ts index eb3d469d1c..486ec23f07 100644 --- a/packages/application-extension/src/index.ts +++ b/packages/application-extension/src/index.ts @@ -35,16 +35,24 @@ import { ITranslator } from '@jupyterlab/translation'; import { NotebookApp, NotebookShell, - INotebookShell + INotebookShell, + SideBarPanel, + SideBarHandler } from '@jupyter-notebook/application'; import { jupyterIcon } from '@jupyter-notebook/ui-components'; import { PromiseDelegate } from '@lumino/coreutils'; -import { DisposableDelegate, DisposableSet } from '@lumino/disposable'; +import { + DisposableDelegate, + DisposableSet, + IDisposable +} from '@lumino/disposable'; + +import { Menu, Widget } from '@lumino/widgets'; -import { Widget } from '@lumino/widgets'; +import { SideBarPalette } from './sidebarpalette'; /** * A regular expression to match path to notebooks and documents @@ -65,6 +73,11 @@ namespace CommandIDs { */ export const toggleTop = 'application:toggle-top'; + /** + * Toggle sidebar visibility + */ + export const togglePanel = 'application:toggle-panel'; + /** * Toggle the Zen mode */ @@ -173,7 +186,7 @@ const opener: JupyterFrontEndPlugin = { const file = decodeURIComponent(path); const urlParams = new URLSearchParams(parsed.search); const factory = urlParams.get('factory') ?? 'default'; - app.restored.then(async () => { + app.started.then(async () => { docManager.open(file, factory, undefined, { ref: '_noref' }); @@ -187,8 +200,6 @@ const opener: JupyterFrontEndPlugin = { /** * A plugin to customize menus - * - * TODO: use this plugin to customize the menu items and their order */ const menus: JupyterFrontEndPlugin = { id: '@jupyter-notebook/application-extension:menus', @@ -557,6 +568,219 @@ const topVisibility: JupyterFrontEndPlugin = { autoStart: true }; +/** + * Plugin to toggle the left or right sidebar's visibility. + */ +const sidebarVisibility: JupyterFrontEndPlugin = { + id: '@jupyter-notebook/application-extension:sidebar', + requires: [INotebookShell, ITranslator], + optional: [IMainMenu, ICommandPalette], + autoStart: true, + activate: ( + app: JupyterFrontEnd, + notebookShell: INotebookShell, + translator: ITranslator, + menu: IMainMenu | null, + palette: ICommandPalette | null + ) => { + const trans = translator.load('notebook'); + + /* Arguments for togglePanel command: + * side, left or right area + * title, widget title to show in the menu + * id, widget ID to activate in the sidebar + */ + app.commands.addCommand(CommandIDs.togglePanel, { + label: args => args['title'] as string, + caption: args => { + // We do not substitute the parameter into the string because the parameter is not + // localized (e.g., it is always 'left') even though the string is localized. + if (args['side'] === 'left') { + return trans.__( + 'Show %1 in the left sidebar', + args['title'] as string + ); + } else if (args['side'] === 'right') { + return trans.__( + 'Show %1 in the right sidebar', + args['title'] as string + ); + } + return trans.__('Show %1 in the sidebar', args['title'] as string); + }, + execute: args => { + switch (args['side'] as string) { + case 'left': + if (notebookShell.leftCollapsed) { + notebookShell.expandLeft(args.id as string); + } else if ( + notebookShell.leftHandler.currentWidget?.id !== args.id + ) { + notebookShell.expandLeft(args.id as string); + } else { + notebookShell.collapseLeft(); + if (notebookShell.currentWidget) { + notebookShell.activateById(notebookShell.currentWidget.id); + } + } + break; + case 'right': + if (notebookShell.rightCollapsed) { + notebookShell.expandRight(args.id as string); + } else if ( + notebookShell.rightHandler.currentWidget?.id !== args.id + ) { + notebookShell.expandRight(args.id as string); + } else { + notebookShell.collapseRight(); + if (notebookShell.currentWidget) { + notebookShell.activateById(notebookShell.currentWidget.id); + } + } + break; + } + }, + isToggled: args => { + switch (args['side'] as string) { + case 'left': { + if (notebookShell.leftCollapsed) { + return false; + } + const currentWidget = notebookShell.leftHandler.currentWidget; + if (!currentWidget) { + return false; + } + + return currentWidget.id === (args['id'] as string); + } + case 'right': { + if (notebookShell.rightCollapsed) { + return false; + } + const currentWidget = notebookShell.rightHandler.currentWidget; + if (!currentWidget) { + return false; + } + + return currentWidget.id === (args['id'] as string); + } + } + return false; + } + }); + + const sideBarMenu: { [area in SideBarPanel.Area]: IDisposable | null } = { + left: null, + right: null + }; + + /** + * The function which adds entries to the View menu for each widget of a sidebar. + * + * @param area - 'left' or 'right', the area of the side bar. + * @param entryLabel - the name of the main entry in the View menu for that sidebar. + * @returns - The disposable menu added to the View menu or null. + */ + const updateMenu = (area: SideBarPanel.Area, entryLabel: string) => { + if (menu === null) { + return null; + } + + // Remove the previous menu entry for this sidebar. + sideBarMenu[area]?.dispose(); + + // Creates a new menu entry and populates it with sidebar widgets. + const newMenu = new Menu({ commands: app.commands }); + newMenu.title.label = entryLabel; + const widgets = notebookShell.widgets(area); + let menuToAdd = false; + + for (let widget of widgets) { + newMenu.addItem({ + command: CommandIDs.togglePanel, + args: { + side: area, + title: `Show ${widget.title.caption}`, + id: widget.id + } + }); + menuToAdd = true; + } + + // If there are widgets, add the menu to the main menu entry. + if (menuToAdd) { + sideBarMenu[area] = menu.viewMenu.addItem({ + type: 'submenu', + submenu: newMenu + }); + } + }; + + app.restored.then(() => { + // Create menu entries for the left and right panel. + if (menu) { + const getSideBarLabel = (area: SideBarPanel.Area): string => { + if (area === 'left') { + return trans.__(`Left Sidebar`); + } else { + return trans.__(`Right Sidebar`); + } + }; + const leftArea = notebookShell.leftHandler.area; + const leftLabel = getSideBarLabel(leftArea); + updateMenu(leftArea, leftLabel); + + const rightArea = notebookShell.rightHandler.area; + const rightLabel = getSideBarLabel(rightArea); + updateMenu(rightArea, rightLabel); + + const handleSideBarChange = ( + sidebar: SideBarHandler, + widget: Widget + ) => { + const label = getSideBarLabel(sidebar.area); + updateMenu(sidebar.area, label); + }; + + notebookShell.leftHandler.widgetAdded.connect(handleSideBarChange); + notebookShell.leftHandler.widgetRemoved.connect(handleSideBarChange); + notebookShell.rightHandler.widgetAdded.connect(handleSideBarChange); + notebookShell.rightHandler.widgetRemoved.connect(handleSideBarChange); + } + + // Add palette entries for side panels. + if (palette) { + const sideBarPalette = new SideBarPalette({ + commandPalette: palette as ICommandPalette, + command: CommandIDs.togglePanel + }); + + notebookShell.leftHandler.widgets.forEach(widget => { + sideBarPalette.addItem(widget, notebookShell.leftHandler.area); + }); + + notebookShell.rightHandler.widgets.forEach(widget => { + sideBarPalette.addItem(widget, notebookShell.rightHandler.area); + }); + + // Update menu and palette when widgets are added or removed from sidebars. + notebookShell.leftHandler.widgetAdded.connect((sidebar, widget) => { + sideBarPalette.addItem(widget, sidebar.area); + }); + notebookShell.leftHandler.widgetRemoved.connect((sidebar, widget) => { + sideBarPalette.removeItem(widget, sidebar.area); + }); + notebookShell.rightHandler.widgetAdded.connect((sidebar, widget) => { + sideBarPalette.addItem(widget, sidebar.area); + }); + notebookShell.rightHandler.widgetRemoved.connect((sidebar, widget) => { + sideBarPalette.removeItem(widget, sidebar.area); + }); + } + }); + } +}; + /** * The default tree route resolver plugin. */ @@ -711,6 +935,7 @@ const plugins: JupyterFrontEndPlugin[] = [ router, sessionDialogs, shell, + sidebarVisibility, status, tabTitle, title, diff --git a/packages/application-extension/src/sidebarpalette.ts b/packages/application-extension/src/sidebarpalette.ts new file mode 100644 index 0000000000..e47c33be6c --- /dev/null +++ b/packages/application-extension/src/sidebarpalette.ts @@ -0,0 +1,114 @@ +import { ICommandPalette } from '@jupyterlab/apputils'; +import { IDisposable } from '@lumino/disposable'; +import { Widget } from '@lumino/widgets'; + +/** + * A class to manages the palette entries associated to the side bar. + */ +export class SideBarPalette { + /** + * Construct a new side bar palette. + */ + constructor(options: SideBarPaletteOption) { + this._commandPalette = options.commandPalette; + this._command = options.command; + } + + /** + * Get a command palette item from the widget id and the area. + */ + getItem( + widget: Readonly, + area: 'left' | 'right' + ): SideBarPaletteItem | null { + const itemList = this._items; + for (let i = 0; i < itemList.length; i++) { + const item = itemList[i]; + if (item.widgetId == widget.id && item.area == area) { + return item; + } + } + return null; + } + + /** + * Add an item to the command palette. + */ + addItem(widget: Readonly, area: 'left' | 'right'): void { + // Check if the item does not already exist. + if (this.getItem(widget, area)) { + return; + } + + // Add a new item in command palette. + const disposableDelegate = this._commandPalette.addItem({ + command: this._command, + category: 'View', + args: { + side: area, + title: `Show ${widget.title.caption}`, + id: widget.id + } + }); + + // Keep the disposableDelegate objet to be able to dispose of the item if the widget + // is remove from the side bar. + this._items.push({ + widgetId: widget.id, + area: area, + disposable: disposableDelegate + }); + } + + /** + * Remove an item from the command palette. + */ + removeItem(widget: Readonly, area: 'left' | 'right'): void { + const item = this.getItem(widget, area); + if (item) { + item.disposable.dispose(); + } + } + + _command: string; + _commandPalette: ICommandPalette; + _items: SideBarPaletteItem[] = []; +} + +type SideBarPaletteItem = { + /** + * The ID of the widget associated to the command palette. + */ + widgetId: string; + + /** + * The area of the panel associated to the command palette. + */ + area: 'left' | 'right'; + + /** + * The disposable object to remove the item from command palette. + */ + disposable: IDisposable; +}; + +/** + * An interface for the options to include in SideBarPalette constructor. + */ +type SideBarPaletteOption = { + /** + * The commands palette. + */ + commandPalette: ICommandPalette; + + /** + * The command to call from each sidebar menu entry. + * + * ### Notes + * That command required 3 args : + * side: 'left' | 'right', the area to toggle + * title: string, label of the command + * id: string, id of the widget to activate + */ + command: string; +}; diff --git a/packages/application/src/app.ts b/packages/application/src/app.ts index 36efb573ab..5f1acc1e62 100644 --- a/packages/application/src/app.ts +++ b/packages/application/src/app.ts @@ -34,6 +34,9 @@ export class NotebookApp extends JupyterFrontEnd { this.registerPlugin(plugin); } } + + this.restored = this.shell.restored; + this.restored.then(() => this._formatter.invoke()); } @@ -52,6 +55,11 @@ export class NotebookApp extends JupyterFrontEnd { */ readonly status = new LabStatus(this); + /** + * Promise that resolves when the state is first restored + */ + readonly restored: Promise; + /** * The version of the application. */ diff --git a/packages/application/src/shell.ts b/packages/application/src/shell.ts index 395362f392..fc97e4393a 100644 --- a/packages/application/src/shell.ts +++ b/packages/application/src/shell.ts @@ -2,18 +2,21 @@ // Distributed under the terms of the Modified BSD License. import { JupyterFrontEnd } from '@jupyterlab/application'; - import { DocumentRegistry } from '@jupyterlab/docregistry'; +import { closeIcon } from '@jupyterlab/ui-components'; -import { ArrayExt } from '@lumino/algorithm'; - -import { Token } from '@lumino/coreutils'; - +import { ArrayExt, find } from '@lumino/algorithm'; +import { PromiseDelegate, Token } from '@lumino/coreutils'; import { Message, MessageLoop, IMessageHandler } from '@lumino/messaging'; - import { ISignal, Signal } from '@lumino/signaling'; -import { Panel, Widget, BoxLayout } from '@lumino/widgets'; +import { + BoxLayout, + Panel, + SplitPanel, + StackedPanel, + Widget +} from '@lumino/widgets'; /** * The Jupyter Notebook application shell token. @@ -40,37 +43,75 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell { super(); this.id = 'main'; - const rootLayout = new BoxLayout(); - this._topHandler = new Private.PanelHandler(); this._menuHandler = new Private.PanelHandler(); + this._leftHandler = new SideBarHandler('left'); + this._rightHandler = new SideBarHandler('right'); this._main = new Panel(); + const topWrapper = (this._topWrapper = new Panel()); + const menuWrapper = (this._menuWrapper = new Panel()); this._topHandler.panel.id = 'top-panel'; this._menuHandler.panel.id = 'menu-panel'; this._main.id = 'main-panel'; + this._spacer = new Widget(); + this._spacer.id = 'spacer-widget'; + // 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); + const rootLayout = new BoxLayout(); + const leftHandler = this._leftHandler; + const rightHandler = this._rightHandler; + + leftHandler.panel.id = 'jp-left-stack'; + rightHandler.panel.id = 'jp-right-stack'; + + // Hide the side panels by default. + leftHandler.hide(); + rightHandler.hide(); + + const middleLayout = new BoxLayout({ + spacing: 0, + direction: 'top-to-bottom' + }); + BoxLayout.setStretch(this._topWrapper, 0); + BoxLayout.setStretch(this._menuWrapper, 0); BoxLayout.setStretch(this._main, 1); - this._spacer = new Widget(); - this._spacer.id = 'spacer-widget'; + const middlePanel = new Panel({ layout: middleLayout }); + middlePanel.addWidget(this._topWrapper); + middlePanel.addWidget(this._menuWrapper); + middlePanel.addWidget(this._spacer); + middlePanel.addWidget(this._main); + middlePanel.layout = middleLayout; + + // TODO: Consider storing this as an attribute this._hsplitPanel if saving/restoring layout needed + const hsplitPanel = new SplitPanel(); + hsplitPanel.id = 'main-split-panel'; + hsplitPanel.spacing = 1; + BoxLayout.setStretch(hsplitPanel, 1); + + SplitPanel.setStretch(leftHandler.panel, 0); + SplitPanel.setStretch(rightHandler.panel, 0); + SplitPanel.setStretch(middlePanel, 1); + + hsplitPanel.addWidget(leftHandler.panel); + hsplitPanel.addWidget(middlePanel); + hsplitPanel.addWidget(rightHandler.panel); + + // Use relative sizing to set the width of the side panels. + // This will still respect the min-size of children widget in the stacked + // panel. + hsplitPanel.setRelativeSizes([1, 2.5, 1]); rootLayout.spacing = 0; - rootLayout.addWidget(topWrapper); - rootLayout.addWidget(menuWrapper); - rootLayout.addWidget(this._spacer); - rootLayout.addWidget(this._main); + rootLayout.addWidget(hsplitPanel); this.layout = rootLayout; } @@ -103,13 +144,59 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell { return this._menuWrapper; } + /** + * Get the left area handler + */ + get leftHandler(): SideBarHandler { + return this._leftHandler; + } + + /** + * Get the right area handler + */ + get rightHandler(): SideBarHandler { + return this._rightHandler; + } + + /** + * Is the left sidebar visible? + */ + get leftCollapsed(): boolean { + return !(this._leftHandler.isVisible && this._leftHandler.panel.isVisible); + } + + /** + * Is the right sidebar visible? + */ + get rightCollapsed(): boolean { + return !( + this._rightHandler.isVisible && this._rightHandler.panel.isVisible + ); + } + + /** + * Promise that resolves when the main widget is loaded + */ + get restored(): Promise { + return this._mainWidgetLoaded.promise; + } + /** * Activate a widget in its area. */ activateById(id: string): void { - const widget = this._main.widgets.find(w => w.id === id); - if (widget) { - widget.activate(); + // Search all areas that can have widgets for this widget, starting with main. + for (const area of ['main', 'top', 'left', 'right', 'menu']) { + const widget = find(this.widgets(area as Shell.Area), w => w.id === id); + if (widget) { + if (area === 'left') { + this.expandLeft(id); + } else if (area === 'right') { + this.expandRight(id); + } else { + widget.activate(); + } + } } } @@ -130,20 +217,28 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell { 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); + switch (area) { + case 'top': + return this._topHandler.addWidget(widget, rank); + case 'menu': + return this._menuHandler.addWidget(widget, rank); + case 'main': + case 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); + this._mainWidgetLoaded.resolve(); + break; + case 'left': + return this._leftHandler.addWidget(widget, rank); + case 'right': + return this._rightHandler.addWidget(widget, rank); + default: + console.warn(`Cannot add widget to area: ${area}`); } } @@ -179,19 +274,60 @@ export class NotebookShell extends Widget implements JupyterFrontEnd.IShell { case 'main': yield* this._main.widgets; return; + case 'left': + yield* this._leftHandler.widgets; + return; + case 'right': + yield* this._rightHandler.widgets; + return; default: console.error(`This shell has no area called "${area}"`); return; } } + /** + * Expand the left panel to show the sidebar with its widget. + */ + expandLeft(id?: string): void { + this._leftHandler.panel.show(); + this._leftHandler.expand(id); // Show the current widget, if any + } + + /** + * Collapse the left panel + */ + collapseLeft(): void { + this._leftHandler.collapse(); + this._leftHandler.panel.hide(); + } + + /** + * Expand the right panel to show the sidebar with its widget. + */ + expandRight(id?: string): void { + this._rightHandler.panel.show(); + this._rightHandler.expand(id); // Show the current widget, if any + } + + /** + * Collapse the right panel + */ + collapseRight(): void { + this._rightHandler.collapse(); + this._rightHandler.panel.hide(); + } + private _topWrapper: Panel; private _topHandler: Private.PanelHandler; private _menuWrapper: Panel; private _menuHandler: Private.PanelHandler; + private _leftHandler: SideBarHandler; + private _rightHandler: SideBarHandler; private _spacer: Widget; private _main: Panel; private _currentChanged = new Signal(this); + private _mainWidgetLoaded = new PromiseDelegate(); } /** @@ -201,7 +337,243 @@ export namespace Shell { /** * The areas of the application shell where widgets can reside. */ - export type Area = 'main' | 'top' | 'menu'; + export type Area = 'main' | 'top' | 'left' | 'right' | 'menu'; +} + +/** + * A name space for SideBarPanel functions. + */ +export namespace SideBarPanel { + /** + * The areas of the sidebar panel + */ + export type Area = 'left' | 'right'; +} + +/** + * A class which manages a side bar that can show at most one widget at a time. + */ +export class SideBarHandler { + /** + * Construct a new side bar handler. + */ + constructor(area: SideBarPanel.Area) { + this._area = area; + this._panel = new Panel(); + this._panel.hide(); + + this._currentWidget = null; + this._lastCurrentWidget = null; + + this._widgetPanel = new StackedPanel(); + this._widgetPanel.widgetRemoved.connect(this._onWidgetRemoved, this); + + const closeButton = document.createElement('button'); + closeIcon.element({ + container: closeButton, + height: '16px', + width: 'auto' + }); + closeButton.onclick = () => { + this.collapse(); + this.hide(); + }; + closeButton.className = 'jp-Button jp-SidePanel-collapse'; + const icon = new Widget({ node: closeButton }); + this._panel.addWidget(icon); + this._panel.addWidget(this._widgetPanel); + } + + /** + * Get the current widget in the sidebar panel. + */ + get currentWidget(): Widget | null { + return ( + this._currentWidget || + this._lastCurrentWidget || + (this._items.length > 0 ? this._items[0].widget : null) + ); + } + + /** + * Get the area of the side panel + */ + get area(): SideBarPanel.Area { + return this._area; + } + + /** + * Whether the panel is visible + */ + get isVisible(): boolean { + return this._panel.isVisible; + } + + /** + * Get the stacked panel managed by the handler + */ + get panel(): Panel { + return this._panel; + } + + /** + * Get the widgets list. + */ + get widgets(): Readonly { + return this._items.map(obj => obj.widget); + } + + /** + * Signal fired when a widget is added to the panel + */ + get widgetAdded(): ISignal { + return this._widgetAdded; + } + + /** + * Signal fired when a widget is removed from the panel + */ + get widgetRemoved(): ISignal { + return this._widgetRemoved; + } + + /** + * Expand the sidebar. + * + * #### Notes + * This will open the most recently used widget, or the first widget + * if there is no most recently used. + */ + expand(id?: string): void { + if (this._currentWidget) { + this.collapse(); + } + if (id) { + this.activate(id); + } else { + const visibleWidget = this.currentWidget; + if (visibleWidget) { + this._currentWidget = visibleWidget; + this.activate(visibleWidget.id); + } + } + } + + /** + * Activate a widget residing in the stacked panel by ID. + * + * @param id - The widget's unique ID. + */ + activate(id: string): void { + const widget = this._findWidgetByID(id); + if (widget) { + this._currentWidget = widget; + widget.show(); + widget.activate(); + } + } + + /** + * Test whether the sidebar has the given widget by id. + */ + has(id: string): boolean { + return this._findWidgetByID(id) !== null; + } + + /** + * Collapse the sidebar so no items are expanded. + */ + collapse(): void { + this._currentWidget?.hide(); + this._currentWidget = null; + } + + /** + * Add a widget and its title to the stacked panel. + * + * If the widget is already added, it will be moved. + */ + addWidget(widget: Widget, rank: number): void { + widget.parent = null; + widget.hide(); + const item = { widget, rank }; + const index = this._findInsertIndex(item); + ArrayExt.insert(this._items, index, item); + this._widgetPanel.insertWidget(index, widget); + + this._refreshVisibility(); + + this._widgetAdded.emit(widget); + } + + /** + * Hide the side panel + */ + hide(): void { + this._isHiddenByUser = true; + this._refreshVisibility(); + } + + /** + * Show the side panel + */ + show(): void { + this._isHiddenByUser = false; + this._refreshVisibility(); + } + + /** + * Find the insertion index for a rank item. + */ + private _findInsertIndex(item: Private.IRankItem): number { + return ArrayExt.upperBound(this._items, item, Private.itemCmp); + } + + /** + * Find the index of the item with the given widget, or `-1`. + */ + private _findWidgetIndex(widget: Widget): number { + return ArrayExt.findFirstIndex(this._items, i => i.widget === widget); + } + + /** + * Find the widget with the given id, or `null`. + */ + private _findWidgetByID(id: string): Widget | null { + const item = find(this._items, value => value.widget.id === id); + return item ? item.widget : null; + } + + /** + * Refresh the visibility of the stacked panel. + */ + private _refreshVisibility(): void { + this._panel.setHidden(this._isHiddenByUser); + } + + /* + * Handle the `widgetRemoved` signal from the panel. + */ + private _onWidgetRemoved(sender: StackedPanel, widget: Widget): void { + if (widget === this._lastCurrentWidget) { + this._lastCurrentWidget = null; + } + ArrayExt.removeAt(this._items, this._findWidgetIndex(widget)); + + this._refreshVisibility(); + + this._widgetRemoved.emit(widget); + } + + private _area: SideBarPanel.Area; + private _isHiddenByUser = false; + private _items = new Array(); + private _panel: Panel; + private _widgetPanel: StackedPanel; + private _currentWidget: Widget | null; + private _lastCurrentWidget: Widget | null; + private _widgetAdded: Signal = new Signal(this); + private _widgetRemoved: Signal = new Signal(this); } /** diff --git a/packages/application/style/index.css b/packages/application/style/index.css index 4c504055e4..80ff961161 100644 --- a/packages/application/style/index.css +++ b/packages/application/style/index.css @@ -8,3 +8,4 @@ @import url('~@jupyterlab/ui-components/style/index.css'); @import url('./base.css'); +@import url('./sidepanel.css'); diff --git a/packages/application/style/index.js b/packages/application/style/index.js index bac719f165..a84ae1e2b5 100644 --- a/packages/application/style/index.js +++ b/packages/application/style/index.js @@ -8,3 +8,4 @@ import '@jupyterlab/mainmenu/style/index.js'; import '@jupyterlab/ui-components/style/index.js'; import './base.css'; +import './sidepanel.css'; diff --git a/packages/application/style/sidepanel.css b/packages/application/style/sidepanel.css new file mode 100644 index 0000000000..61057e362c --- /dev/null +++ b/packages/application/style/sidepanel.css @@ -0,0 +1,48 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +| +| Adapted from JupyterLab's packages/application/style/sidepanel.css. +|----------------------------------------------------------------------------*/ + +/*----------------------------------------------------------------------------- +| Variables +|----------------------------------------------------------------------------*/ + +:root { + --jp-private-sidebar-tab-width: 32px; +} + +/*----------------------------------------------------------------------------- +| SideBar +|----------------------------------------------------------------------------*/ + +/* Stack panels */ + +#jp-right-stack, +#jp-left-stack { + display: flex; + flex-direction: column; + min-width: var(--jp-sidebar-min-width); +} + +#jp-left-stack .jp-SidePanel-collapse, +#jp-right-stack .jp-SidePanel-collapse { + display: flex; + flex: 0 0 auto; + min-height: 0; + padding: 0; +} + +#jp-left-stack .jp-SidePanel-collapse { + justify-content: right; +} + +#jp-right-stack .jp-SidePanel-collapse { + justify-content: left; +} + +#jp-left-stack .lm-StackedPanel, +#jp-right-stack .lm-StackedPanel { + flex: 1 1 auto; +} diff --git a/packages/application/test/shell.spec.ts b/packages/application/test/shell.spec.ts index d44d3bfe55..b419d0305c 100644 --- a/packages/application/test/shell.spec.ts +++ b/packages/application/test/shell.spec.ts @@ -1,13 +1,17 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -import { NotebookShell, INotebookShell } from '@jupyter-notebook/application'; +import { + INotebookShell, + NotebookShell, + Shell +} from '@jupyter-notebook/application'; import { JupyterFrontEnd } from '@jupyterlab/application'; import { Widget } from '@lumino/widgets'; -describe('Shell', () => { +describe('Shell for notebooks', () => { let shell: INotebookShell; beforeEach(() => { @@ -23,10 +27,17 @@ describe('Shell', () => { it('should create a LabShell instance', () => { expect(shell).toBeInstanceOf(NotebookShell); }); + + it('should make all areas empty initially', () => { + ['main', 'top', 'left', 'right', 'menu'].forEach(area => { + const widgets = Array.from(shell.widgets(area as Shell.Area)); + expect(widgets.length).toEqual(0); + }); + }); }); describe('#widgets()', () => { - it('should add widgets to existing areas', () => { + it('should add widgets to main area', () => { const widget = new Widget(); shell.add(widget, 'main'); const widgets = Array.from(shell.widgets('main')); @@ -36,7 +47,7 @@ describe('Shell', () => { it('should be empty and console.error if area does not exist', () => { const spy = jest.spyOn(console, 'error'); const jupyterFrontEndShell = shell as JupyterFrontEnd.IShell; - expect(Array.from(jupyterFrontEndShell.widgets('left'))).toHaveLength(0); + expect(Array.from(jupyterFrontEndShell.widgets('fake'))).toHaveLength(0); expect(spy).toHaveBeenCalled(); }); }); @@ -82,4 +93,86 @@ describe('Shell', () => { expect(widgets.length).toBeGreaterThan(0); }); }); + + describe('#add(widget, "left")', () => { + it('should add a widget to the left area', () => { + const widget = new Widget(); + widget.id = 'foo'; + shell.add(widget, 'left'); + const widgets = Array.from(shell.widgets('left')); + expect(widgets.length).toBeGreaterThan(0); + }); + }); + + describe('#add(widget, "right")', () => { + it('should add a widget to the right area', () => { + const widget = new Widget(); + widget.id = 'foo'; + shell.add(widget, 'right'); + const widgets = Array.from(shell.widgets('right')); + expect(widgets.length).toBeGreaterThan(0); + }); + }); +}); + +describe('Shell for tree view', () => { + let shell: INotebookShell; + + beforeEach(() => { + shell = new NotebookShell(); + Widget.attach(shell, document.body); + }); + + afterEach(() => { + shell.dispose(); + }); + + describe('#constructor()', () => { + it('should create a LabShell instance', () => { + expect(shell).toBeInstanceOf(NotebookShell); + }); + + it('should make all areas empty initially', () => { + ['main', 'top', 'left', 'right', 'menu'].forEach(area => { + const widgets = Array.from(shell.widgets(area as Shell.Area)); + expect(widgets.length).toEqual(0); + }); + }); + }); + + describe('#widgets()', () => { + it('should add widgets to existing areas', () => { + const widget = new Widget(); + shell.add(widget, 'main'); + const widgets = Array.from(shell.widgets('main')); + expect(widgets).toEqual([widget]); + }); + + it('should throw an exception if a fake area does not exist', () => { + const spy = jest.spyOn(console, 'error'); + const jupyterFrontEndShell = shell as JupyterFrontEnd.IShell; + expect(Array.from(jupyterFrontEndShell.widgets('fake'))).toHaveLength(0); + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('#add(widget, "left")', () => { + it('should add a widget to the left area', () => { + const widget = new Widget(); + widget.id = 'foo'; + shell.add(widget, 'left'); + const widgets = Array.from(shell.widgets('left')); + expect(widgets.length).toBeGreaterThan(0); + }); + }); + + describe('#add(widget, "right")', () => { + it('should add a widget to the right area', () => { + const widget = new Widget(); + widget.id = 'foo'; + shell.add(widget, 'right'); + const widgets = Array.from(shell.widgets('right')); + expect(widgets.length).toBeGreaterThan(0); + }); + }); }); diff --git a/packages/notebook-extension/src/index.ts b/packages/notebook-extension/src/index.ts index c32ac3687a..163da86426 100644 --- a/packages/notebook-extension/src/index.ts +++ b/packages/notebook-extension/src/index.ts @@ -18,7 +18,11 @@ import { Text, Time } from '@jupyterlab/coreutils'; import { IDocumentManager } from '@jupyterlab/docmanager'; -import { NotebookPanel, INotebookTracker } from '@jupyterlab/notebook'; +import { + NotebookPanel, + INotebookTracker, + INotebookTools +} from '@jupyterlab/notebook'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; @@ -327,6 +331,34 @@ const scrollOutput: JupyterFrontEndPlugin = { } }; +/** + * A plugin to add the NotebookTools to the side panel; + */ +const notebookToolsWidget: JupyterFrontEndPlugin = { + id: '@jupyter-notebook/notebook-extension:notebook-tools', + autoStart: true, + requires: [INotebookShell], + optional: [INotebookTools], + activate: ( + app: JupyterFrontEnd, + shell: INotebookShell, + notebookTools: INotebookTools | null + ) => { + const onChange = async () => { + const current = shell.currentWidget; + if (!(current instanceof NotebookPanel)) { + return; + } + + // Add the notebook tools in right area. + if (notebookTools) { + shell.add(notebookTools, 'right'); + } + }; + shell.currentChanged.connect(onChange); + } +}; + /** * Export the plugins as default. */ @@ -334,7 +366,8 @@ const plugins: JupyterFrontEndPlugin[] = [ checkpoints, kernelLogo, kernelStatus, - scrollOutput + scrollOutput, + notebookToolsWidget ]; export default plugins; diff --git a/ui-tests/test/general.spec.ts b/ui-tests/test/general.spec.ts index 6ee5e92754..57cc907621 100644 --- a/ui-tests/test/general.spec.ts +++ b/ui-tests/test/general.spec.ts @@ -6,6 +6,7 @@ import path from 'path'; import { expect } from '@playwright/test'; import { test } from './fixtures'; + import { waitForKernelReady } from './utils'; test.describe('General', () => { diff --git a/ui-tests/test/general.spec.ts-snapshots/notebook-chromium-linux.png b/ui-tests/test/general.spec.ts-snapshots/notebook-chromium-linux.png index 5e6a20f6f5..70d9aba2ed 100644 Binary files a/ui-tests/test/general.spec.ts-snapshots/notebook-chromium-linux.png and b/ui-tests/test/general.spec.ts-snapshots/notebook-chromium-linux.png differ diff --git a/ui-tests/test/general.spec.ts-snapshots/notebook-firefox-linux.png b/ui-tests/test/general.spec.ts-snapshots/notebook-firefox-linux.png index d40340ac0c..7079711594 100644 Binary files a/ui-tests/test/general.spec.ts-snapshots/notebook-firefox-linux.png and b/ui-tests/test/general.spec.ts-snapshots/notebook-firefox-linux.png differ diff --git a/ui-tests/test/menus.spec.ts b/ui-tests/test/menus.spec.ts index f1c1b95af3..7b957d3c92 100644 --- a/ui-tests/test/menus.spec.ts +++ b/ui-tests/test/menus.spec.ts @@ -6,6 +6,7 @@ import path from 'path'; import { test } from './fixtures'; import { expect } from '@playwright/test'; +import { waitForKernelReady } from './utils'; const NOTEBOOK = 'empty.ipynb'; @@ -32,19 +33,18 @@ test.describe('Notebook Menus', () => { ); }); - test.afterEach(async ({ page }) => { - await page.kernel.shutdownAll(); - }); - MENU_PATHS.forEach(menuPath => { test(`Open menu item ${menuPath}`, async ({ page, tmpPath }) => { await page.goto(`notebooks/${tmpPath}/${NOTEBOOK}`); + await waitForKernelReady(page); + await page.menu.open(menuPath); expect(await page.menu.isOpen(menuPath)).toBeTruthy(); const imageName = `opened-menu-${menuPath.replace(/>/g, '-')}.png`; const menu = await page.menu.getOpenMenu(); - expect(await menu.screenshot()).toMatchSnapshot(imageName.toLowerCase()); + expect(menu).toBeDefined(); + expect(await menu!.screenshot()).toMatchSnapshot(imageName.toLowerCase()); }); }); }); diff --git a/ui-tests/test/menus.spec.ts-snapshots/opened-menu-edit-firefox-linux.png b/ui-tests/test/menus.spec.ts-snapshots/opened-menu-edit-firefox-linux.png index 532ee2d260..7699903bf9 100644 Binary files a/ui-tests/test/menus.spec.ts-snapshots/opened-menu-edit-firefox-linux.png and b/ui-tests/test/menus.spec.ts-snapshots/opened-menu-edit-firefox-linux.png differ diff --git a/ui-tests/test/menus.spec.ts-snapshots/opened-menu-kernel-chromium-linux.png b/ui-tests/test/menus.spec.ts-snapshots/opened-menu-kernel-chromium-linux.png index 4a8ca8a0f0..2832e5b538 100644 Binary files a/ui-tests/test/menus.spec.ts-snapshots/opened-menu-kernel-chromium-linux.png and b/ui-tests/test/menus.spec.ts-snapshots/opened-menu-kernel-chromium-linux.png differ diff --git a/ui-tests/test/menus.spec.ts-snapshots/opened-menu-kernel-firefox-linux.png b/ui-tests/test/menus.spec.ts-snapshots/opened-menu-kernel-firefox-linux.png index 7900a8cd77..27910d69f2 100644 Binary files a/ui-tests/test/menus.spec.ts-snapshots/opened-menu-kernel-firefox-linux.png and b/ui-tests/test/menus.spec.ts-snapshots/opened-menu-kernel-firefox-linux.png differ diff --git a/ui-tests/test/menus.spec.ts-snapshots/opened-menu-view-chromium-linux.png b/ui-tests/test/menus.spec.ts-snapshots/opened-menu-view-chromium-linux.png index 5c0712c35d..cfc60f6e5b 100644 Binary files a/ui-tests/test/menus.spec.ts-snapshots/opened-menu-view-chromium-linux.png and b/ui-tests/test/menus.spec.ts-snapshots/opened-menu-view-chromium-linux.png differ diff --git a/ui-tests/test/menus.spec.ts-snapshots/opened-menu-view-firefox-linux.png b/ui-tests/test/menus.spec.ts-snapshots/opened-menu-view-firefox-linux.png index 0410efbf6c..9cb32711f5 100644 Binary files a/ui-tests/test/menus.spec.ts-snapshots/opened-menu-view-firefox-linux.png and b/ui-tests/test/menus.spec.ts-snapshots/opened-menu-view-firefox-linux.png differ diff --git a/ui-tests/test/mobile.spec.ts b/ui-tests/test/mobile.spec.ts index 1cafd84d4f..fd4eb17dc3 100644 --- a/ui-tests/test/mobile.spec.ts +++ b/ui-tests/test/mobile.spec.ts @@ -6,9 +6,10 @@ import path from 'path'; import { expect } from '@playwright/test'; import { test } from './fixtures'; + import { waitForKernelReady } from './utils'; -test.use({ autoGoto: false, viewport: { width: 512, height: 768 } }); +test.use({ autoGoto: false }); test.describe('Mobile', () => { test('The layout should be more compact on the file browser page', async ({ @@ -16,7 +17,13 @@ test.describe('Mobile', () => { tmpPath }) => { await page.goto(`tree/${tmpPath}`); + + // temporary workaround to trigger a toolbar resize + // TODO: investigate in https://github.com/jupyter/notebook/issues/6553 + await page.setViewportSize({ width: 524, height: 800 }); + await page.waitForSelector('#top-panel-wrapper', { state: 'hidden' }); + expect(await page.screenshot()).toMatchSnapshot('tree.png'); }); @@ -30,12 +37,14 @@ test.describe('Mobile', () => { `${tmpPath}/${notebook}` ); await page.goto(`notebooks/${tmpPath}/${notebook}`); - // TODO: investigate why this does not run the cells in Jupyter Notebook - // await page.notebook.run(); // wait for the kernel status animations to be finished await waitForKernelReady(page); + // temporary workaround to trigger a toolbar resize + // TODO: investigate in https://github.com/jupyter/notebook/issues/6553 + await page.setViewportSize({ width: 524, height: 800 }); + // force switching back to command mode to avoid capturing the cursor in the screenshot await page.evaluate(async () => { await window.jupyterapp.commands.execute('notebook:enter-command-mode'); diff --git a/ui-tests/test/mobile.spec.ts-snapshots/notebook-chromium-linux.png b/ui-tests/test/mobile.spec.ts-snapshots/notebook-chromium-linux.png index 4dd10c34ce..3b05e53703 100644 Binary files a/ui-tests/test/mobile.spec.ts-snapshots/notebook-chromium-linux.png and b/ui-tests/test/mobile.spec.ts-snapshots/notebook-chromium-linux.png differ diff --git a/ui-tests/test/mobile.spec.ts-snapshots/notebook-firefox-linux.png b/ui-tests/test/mobile.spec.ts-snapshots/notebook-firefox-linux.png index 2502663206..6edc57d8c5 100644 Binary files a/ui-tests/test/mobile.spec.ts-snapshots/notebook-firefox-linux.png and b/ui-tests/test/mobile.spec.ts-snapshots/notebook-firefox-linux.png differ diff --git a/ui-tests/test/mobile.spec.ts-snapshots/tree-chromium-linux.png b/ui-tests/test/mobile.spec.ts-snapshots/tree-chromium-linux.png index 0ea79afbb6..38244f82e4 100644 Binary files a/ui-tests/test/mobile.spec.ts-snapshots/tree-chromium-linux.png and b/ui-tests/test/mobile.spec.ts-snapshots/tree-chromium-linux.png differ diff --git a/ui-tests/test/mobile.spec.ts-snapshots/tree-firefox-linux.png b/ui-tests/test/mobile.spec.ts-snapshots/tree-firefox-linux.png index 017ff95b22..d7e8db6959 100644 Binary files a/ui-tests/test/mobile.spec.ts-snapshots/tree-firefox-linux.png and b/ui-tests/test/mobile.spec.ts-snapshots/tree-firefox-linux.png differ diff --git a/ui-tests/test/notebook.spec.ts b/ui-tests/test/notebook.spec.ts index 45531ca2fa..3bfbbf095f 100644 --- a/ui-tests/test/notebook.spec.ts +++ b/ui-tests/test/notebook.spec.ts @@ -7,7 +7,7 @@ import { expect } from '@playwright/test'; import { test } from './fixtures'; -import { runAndAdvance } from './utils'; +import { runAndAdvance, waitForKernelReady } from './utils'; const NOTEBOOK = 'example.ipynb'; @@ -99,4 +99,60 @@ test.describe('Notebook', () => { // check the short output area is not auto scrolled expect(await checkCell(1)).toBe(false); }); + + test('Open table of content left panel', async ({ page, tmpPath }) => { + const notebook = 'simple_toc.ipynb'; + const menuPath = 'View>Left Sidebar>Show Table of Contents'; + await page.contents.uploadFile( + path.resolve(__dirname, `./notebooks/${notebook}`), + `${tmpPath}/${notebook}` + ); + await page.goto(`notebooks/${tmpPath}/${notebook}`); + + await waitForKernelReady(page); + + await page.menu.clickMenuItem(menuPath); + + const panel = page.locator('#jp-left-stack'); + expect(await panel.isVisible()); + + await expect( + panel.locator( + '.jp-SidePanel-content > .jp-TableOfContents-tree > .jp-TableOfContents-content' + ) + ).toHaveCount(1); + await expect( + panel.locator( + '.jp-SidePanel-content > .jp-TableOfContents-tree > .jp-TableOfContents-content > .jp-tocItem' + ) + ).toHaveCount(3); + + const imageName = `toc-left-panel.png`; + + expect(await panel.screenshot()).toMatchSnapshot(imageName); + }); + + test('Open notebook tools right panel', async ({ page, tmpPath }) => { + const notebook = 'simple.ipynb'; + const menuPath = 'View>Right Sidebar>Show Notebook Tools'; + await page.contents.uploadFile( + path.resolve(__dirname, `./notebooks/${notebook}`), + `${tmpPath}/${notebook}` + ); + await page.goto(`notebooks/${tmpPath}/${notebook}`); + + await waitForKernelReady(page); + + await page.menu.clickMenuItem(menuPath); + + const panel = page.locator('#jp-right-stack'); + expect(await panel.isVisible()); + + await page.isVisible('#notebook-tools.jp-NotebookTools'); + + await page.isVisible('#notebook-tools.jp-NotebookTools > #add-tag.tag'); + + const imageName = `notebooktools-right-panel.png`; + expect(await panel.screenshot()).toMatchSnapshot(imageName); + }); }); diff --git a/ui-tests/test/notebook.spec.ts-snapshots/notebooktools-right-panel-chromium-linux.png b/ui-tests/test/notebook.spec.ts-snapshots/notebooktools-right-panel-chromium-linux.png new file mode 100644 index 0000000000..d584708382 Binary files /dev/null and b/ui-tests/test/notebook.spec.ts-snapshots/notebooktools-right-panel-chromium-linux.png differ diff --git a/ui-tests/test/notebook.spec.ts-snapshots/notebooktools-right-panel-firefox-linux.png b/ui-tests/test/notebook.spec.ts-snapshots/notebooktools-right-panel-firefox-linux.png new file mode 100644 index 0000000000..9ff3a2026f Binary files /dev/null and b/ui-tests/test/notebook.spec.ts-snapshots/notebooktools-right-panel-firefox-linux.png differ diff --git a/ui-tests/test/notebook.spec.ts-snapshots/toc-left-panel-chromium-linux.png b/ui-tests/test/notebook.spec.ts-snapshots/toc-left-panel-chromium-linux.png new file mode 100644 index 0000000000..14a9fb1154 Binary files /dev/null and b/ui-tests/test/notebook.spec.ts-snapshots/toc-left-panel-chromium-linux.png differ diff --git a/ui-tests/test/notebook.spec.ts-snapshots/toc-left-panel-firefox-linux.png b/ui-tests/test/notebook.spec.ts-snapshots/toc-left-panel-firefox-linux.png new file mode 100644 index 0000000000..a952731615 Binary files /dev/null and b/ui-tests/test/notebook.spec.ts-snapshots/toc-left-panel-firefox-linux.png differ diff --git a/ui-tests/test/notebooks/simple_toc.ipynb b/ui-tests/test/notebooks/simple_toc.ipynb new file mode 100644 index 0000000000..d74f1600cf --- /dev/null +++ b/ui-tests/test/notebooks/simple_toc.ipynb @@ -0,0 +1,65 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# part 1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## subpart 1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## subpart 2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# part 2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# part 3" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.5" + }, + "vscode": { + "interpreter": { + "hash": "0508733a7f73e6ddc798c911e704189485d436785f398d29dd8c20885fc63cb3" + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/ui-tests/test/smoke.spec.ts b/ui-tests/test/smoke.spec.ts index 9895ed5d4c..c9e944eac4 100644 --- a/ui-tests/test/smoke.spec.ts +++ b/ui-tests/test/smoke.spec.ts @@ -72,7 +72,7 @@ math.pi`); // Shut down the kernels await tree2.click('text="Running"'); - await tree2.click('text="Shut Down All"'); + await tree2.click('#main-panel button :text("Shut Down All")'); await tree2.press('.jp-Dialog', 'Enter'); // Close the pages @@ -89,9 +89,10 @@ math.pi`); await page.goto(`tree/${tmpPath}`); // Open JupyterLab - await page.menu.clickMenuItem('View>Open JupyterLab'); - - const lab = await page.waitForEvent('popup'); + const [lab] = await Promise.all([ + page.waitForEvent('popup'), + page.menu.clickMenuItem('View>Open JupyterLab') + ]); await lab.waitForSelector('.jp-Launcher'); await lab.close(); diff --git a/ui-tests/test/utils.ts b/ui-tests/test/utils.ts index e48bd08c92..46b258e42d 100644 --- a/ui-tests/test/utils.ts +++ b/ui-tests/test/utils.ts @@ -32,4 +32,5 @@ export async function waitForKernelReady( }, true); return finished; }); + await page.waitForSelector('.jp-DebuggerBugButton[aria-disabled="false"]'); } diff --git a/yarn.lock b/yarn.lock index 206d12e1fd..4cb5e3043f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1429,10 +1429,10 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jupyter-notebook/application-extension@file:packages/application-extension": - version "7.0.0-alpha.5" + version "7.0.0-alpha.6" dependencies: - "@jupyter-notebook/application" "^7.0.0-alpha.5" - "@jupyter-notebook/ui-components" "^7.0.0-alpha.5" + "@jupyter-notebook/application" "^7.0.0-alpha.6" + "@jupyter-notebook/ui-components" "^7.0.0-alpha.6" "@jupyterlab/application" "^4.0.0-alpha.14" "@jupyterlab/apputils" "^4.0.0-alpha.14" "@jupyterlab/celltags" "^4.0.0-alpha.14" @@ -1450,7 +1450,7 @@ "@lumino/widgets" "^2.0.0-alpha.6" "@jupyter-notebook/application@file:packages/application": - version "7.0.0-alpha.5" + version "7.0.0-alpha.6" dependencies: "@jupyterlab/application" "^4.0.0-alpha.14" "@jupyterlab/coreutils" "^6.0.0-alpha.14" @@ -1465,7 +1465,7 @@ "@lumino/widgets" "^2.0.0-alpha.6" "@jupyter-notebook/console-extension@file:packages/console-extension": - version "7.0.0-alpha.5" + version "7.0.0-alpha.6" dependencies: "@jupyterlab/application" "^4.0.0-alpha.14" "@jupyterlab/console" "^4.0.0-alpha.14" @@ -1473,7 +1473,7 @@ "@lumino/algorithm" "^2.0.0-alpha.6" "@jupyter-notebook/docmanager-extension@file:packages/docmanager-extension": - version "7.0.0-alpha.5" + version "7.0.0-alpha.6" dependencies: "@jupyterlab/application" "^4.0.0-alpha.14" "@jupyterlab/coreutils" "^6.0.0-alpha.14" @@ -1483,26 +1483,26 @@ "@lumino/algorithm" "^2.0.0-alpha.6" "@jupyter-notebook/documentsearch-extension@file:packages/documentsearch-extension": - version "7.0.0-alpha.5" + version "7.0.0-alpha.6" dependencies: - "@jupyter-notebook/application" "^7.0.0-alpha.5" + "@jupyter-notebook/application" "^7.0.0-alpha.6" "@jupyterlab/application" "^4.0.0-alpha.14" "@jupyterlab/documentsearch" "^4.0.0-alpha.14" "@lumino/widgets" "^2.0.0-alpha.6" "@jupyter-notebook/help-extension@file:packages/help-extension": - version "7.0.0-alpha.5" + version "7.0.0-alpha.6" dependencies: - "@jupyter-notebook/ui-components" "^7.0.0-alpha.5" + "@jupyter-notebook/ui-components" "^7.0.0-alpha.6" "@jupyterlab/application" "^4.0.0-alpha.14" "@jupyterlab/apputils" "^4.0.0-alpha.14" "@jupyterlab/mainmenu" "^4.0.0-alpha.14" "@jupyterlab/translation" "^4.0.0-alpha.14" "@jupyter-notebook/lab-extension@file:packages/lab-extension": - version "7.0.0-alpha.5" + version "7.0.0-alpha.6" dependencies: - "@jupyter-notebook/application" "^7.0.0-alpha.5" + "@jupyter-notebook/application" "^7.0.0-alpha.6" "@jupyterlab/application" "^4.0.0-alpha.14" "@jupyterlab/apputils" "^4.0.0-alpha.14" "@jupyterlab/coreutils" "^6.0.0-alpha.14" @@ -1513,9 +1513,9 @@ "@lumino/disposable" "^2.0.0-alpha.6" "@jupyter-notebook/notebook-extension@file:packages/notebook-extension": - version "7.0.0-alpha.5" + version "7.0.0-alpha.6" dependencies: - "@jupyter-notebook/application" "^7.0.0-alpha.5" + "@jupyter-notebook/application" "^7.0.0-alpha.6" "@jupyterlab/application" "^4.0.0-alpha.14" "@jupyterlab/apputils" "^4.0.0-alpha.14" "@jupyterlab/cells" "^4.0.0-alpha.14" @@ -1527,7 +1527,7 @@ "@lumino/widgets" "^2.0.0-alpha.6" "@jupyter-notebook/terminal-extension@file:packages/terminal-extension": - version "7.0.0-alpha.5" + version "7.0.0-alpha.6" dependencies: "@jupyterlab/application" "^4.0.0-alpha.14" "@jupyterlab/coreutils" "^6.0.0-alpha.14" @@ -1535,10 +1535,10 @@ "@lumino/algorithm" "^2.0.0-alpha.6" "@jupyter-notebook/tree-extension@file:packages/tree-extension": - version "7.0.0-alpha.5" + version "7.0.0-alpha.6" dependencies: - "@jupyter-notebook/application" "^7.0.0-alpha.5" - "@jupyter-notebook/tree" "^7.0.0-alpha.5" + "@jupyter-notebook/application" "^7.0.0-alpha.6" + "@jupyter-notebook/tree" "^7.0.0-alpha.6" "@jupyterlab/application" "^4.0.0-alpha.14" "@jupyterlab/apputils" "^4.0.0-alpha.14" "@jupyterlab/coreutils" "^6.0.0-alpha.14" @@ -1555,9 +1555,9 @@ "@lumino/widgets" "^2.0.0-alpha.6" "@jupyter-notebook/tree@file:packages/tree": - version "7.0.0-alpha.5" + version "7.0.0-alpha.6" dependencies: - "@jupyter-notebook/application" "^7.0.0-alpha.5" + "@jupyter-notebook/application" "^7.0.0-alpha.6" "@jupyterlab/application" "^4.0.0-alpha.14" "@jupyterlab/apputils" "^4.0.0-alpha.14" "@jupyterlab/coreutils" "^6.0.0-alpha.14" @@ -1575,7 +1575,7 @@ "@lumino/widgets" "^2.0.0-alpha.6" "@jupyter-notebook/ui-components@file:packages/ui-components": - version "7.0.0-alpha.5" + version "7.0.0-alpha.6" dependencies: "@jupyterlab/ui-components" "^4.0.0-alpha.29" react "^17.0.1" @@ -1813,6 +1813,16 @@ "@lumino/widgets" "^2.0.0-alpha.6" react "^17.0.1" +"@jupyterlab/celltags-extension@^4.0.0-alpha.14": + version "4.0.0-alpha.14" + resolved "https://registry.yarnpkg.com/@jupyterlab/celltags-extension/-/celltags-extension-4.0.0-alpha.14.tgz#cad83537b31e31910f10099ae260a26aa66a65e8" + integrity sha512-S7o/l8n2K5eCnbXCFKsGaOlbl4PnQb10oeRo1PQDQSPAvpN2WgUzp83YYMXlLjngsDs41DGaSBj+7h6qfGCVmw== + dependencies: + "@jupyterlab/application" "^4.0.0-alpha.14" + "@jupyterlab/celltags" "^4.0.0-alpha.14" + "@jupyterlab/notebook" "^4.0.0-alpha.14" + "@jupyterlab/translation" "^4.0.0-alpha.14" + "@jupyterlab/celltags@^4.0.0-alpha.14": version "4.0.0-alpha.14" resolved "https://registry.yarnpkg.com/@jupyterlab/celltags/-/celltags-4.0.0-alpha.14.tgz#d1ad45da532c62b0ba1f3778e2feff04d0800698" @@ -2041,6 +2051,61 @@ path-browserify "^1.0.0" url-parse "~1.5.4" +"@jupyterlab/debugger-extension@^4.0.0-alpha.14": + version "4.0.0-alpha.14" + resolved "https://registry.yarnpkg.com/@jupyterlab/debugger-extension/-/debugger-extension-4.0.0-alpha.14.tgz#598b6046b40e10e3d811d11ae7b1b2c3611ce031" + integrity sha512-K5u32ACz4YNaz2inh+g4mYkUdJTuyAxQSquFvVAImp27oyGKn4GxYrtcHoUCoJlib/0DyDTOg2LOCup6iocMng== + dependencies: + "@jupyterlab/application" "^4.0.0-alpha.14" + "@jupyterlab/apputils" "^4.0.0-alpha.14" + "@jupyterlab/codeeditor" "^4.0.0-alpha.14" + "@jupyterlab/console" "^4.0.0-alpha.14" + "@jupyterlab/coreutils" "^6.0.0-alpha.14" + "@jupyterlab/debugger" "^4.0.0-alpha.14" + "@jupyterlab/docregistry" "^4.0.0-alpha.14" + "@jupyterlab/fileeditor" "^4.0.0-alpha.14" + "@jupyterlab/logconsole" "^4.0.0-alpha.14" + "@jupyterlab/notebook" "^4.0.0-alpha.14" + "@jupyterlab/rendermime" "^4.0.0-alpha.14" + "@jupyterlab/services" "^7.0.0-alpha.14" + "@jupyterlab/settingregistry" "^4.0.0-alpha.14" + "@jupyterlab/translation" "^4.0.0-alpha.14" + +"@jupyterlab/debugger@^4.0.0-alpha.14": + version "4.0.0-alpha.14" + resolved "https://registry.yarnpkg.com/@jupyterlab/debugger/-/debugger-4.0.0-alpha.14.tgz#d9d0c48800b30ec9d083add8bde32cfac8a8c620" + integrity sha512-qtewnV9Dfkl2iV9KC911k1LU+Jv9vn7JABvaTM7vZyS8FqVuZ15zigk1HessvRGy657MD5SZoUtpBa4ETcerOQ== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + "@jupyterlab/application" "^4.0.0-alpha.14" + "@jupyterlab/apputils" "^4.0.0-alpha.14" + "@jupyterlab/cells" "^4.0.0-alpha.14" + "@jupyterlab/codeeditor" "^4.0.0-alpha.14" + "@jupyterlab/codemirror" "^4.0.0-alpha.14" + "@jupyterlab/console" "^4.0.0-alpha.14" + "@jupyterlab/coreutils" "^6.0.0-alpha.14" + "@jupyterlab/docregistry" "^4.0.0-alpha.14" + "@jupyterlab/fileeditor" "^4.0.0-alpha.14" + "@jupyterlab/notebook" "^4.0.0-alpha.14" + "@jupyterlab/observables" "^5.0.0-alpha.14" + "@jupyterlab/rendermime" "^4.0.0-alpha.14" + "@jupyterlab/services" "^7.0.0-alpha.14" + "@jupyterlab/shared-models" "^4.0.0-alpha.14" + "@jupyterlab/translation" "^4.0.0-alpha.14" + "@jupyterlab/ui-components" "^4.0.0-alpha.29" + "@lumino/algorithm" "^2.0.0-alpha.6" + "@lumino/commands" "^2.0.0-alpha.6" + "@lumino/coreutils" "^2.0.0-alpha.6" + "@lumino/datagrid" "^1.0.0-alpha.6" + "@lumino/disposable" "^2.0.0-alpha.6" + "@lumino/messaging" "^2.0.0-alpha.6" + "@lumino/polling" "^2.0.0-alpha.6" + "@lumino/signaling" "^2.0.0-alpha.6" + "@lumino/widgets" "^2.0.0-alpha.6" + "@vscode/debugprotocol" "^1.51.0" + react "^17.0.1" + "@jupyterlab/docmanager-extension@^4.0.0-alpha.14": version "4.0.0-alpha.14" resolved "https://registry.yarnpkg.com/@jupyterlab/docmanager-extension/-/docmanager-extension-4.0.0-alpha.14.tgz#dd31c98e924c12d44ced652410e09451bded155b" @@ -2795,6 +2860,17 @@ "@jupyterlab/apputils" "^4.0.0-alpha.14" "@jupyterlab/translation" "^4.0.0-alpha.14" +"@jupyterlab/toc-extension@^6.0.0-alpha.14": + version "6.0.0-alpha.14" + resolved "https://registry.yarnpkg.com/@jupyterlab/toc-extension/-/toc-extension-6.0.0-alpha.14.tgz#706e5c2f7abf72546592dea5f675c55dc257bcea" + integrity sha512-ilksYGjsXszgdhl9beZtFRmRJLm+ZN2lQ0WJDxsnRaNG+d6tRfiw8toAS111bbSmxyfRNBysdqNIbtNfbioFEg== + dependencies: + "@jupyterlab/application" "^4.0.0-alpha.14" + "@jupyterlab/settingregistry" "^4.0.0-alpha.14" + "@jupyterlab/toc" "^6.0.0-alpha.14" + "@jupyterlab/translation" "^4.0.0-alpha.14" + "@jupyterlab/ui-components" "^4.0.0-alpha.29" + "@jupyterlab/toc@^6.0.0-alpha.14": version "6.0.0-alpha.14" resolved "https://registry.yarnpkg.com/@jupyterlab/toc/-/toc-6.0.0-alpha.14.tgz#6fad008f8252cbdcd87921ade3c4f42fbfd46012" @@ -3735,6 +3811,21 @@ resolved "https://registry.yarnpkg.com/@lumino/coreutils/-/coreutils-2.0.0-alpha.6.tgz#77279303fc72932e3131a085dcaa34e808a09739" integrity sha512-W0qqJZoPRHscHL5k/DHSOea7LugPVx7DmART925bdrD8PU1Rw4K0mUzKb/Zsin4m1O5IMBoPuGEdEG5Jhq3KyA== +"@lumino/datagrid@^1.0.0-alpha.6": + version "1.0.0-alpha.6" + resolved "https://registry.yarnpkg.com/@lumino/datagrid/-/datagrid-1.0.0-alpha.6.tgz#90364797ab0cc7cdb60b72b6448462d56e07219c" + integrity sha512-dwQvaEzka8DBbaHHi/HHtzI4sW+U0iX/RgBFfOlFQWtprZfV+Rjeaw2TWPmXPiR/ugy8zGBYhwxPigfR4zO2Kg== + dependencies: + "@lumino/algorithm" "^2.0.0-alpha.6" + "@lumino/coreutils" "^2.0.0-alpha.6" + "@lumino/disposable" "^2.0.0-alpha.6" + "@lumino/domutils" "^2.0.0-alpha.6" + "@lumino/dragdrop" "^2.0.0-alpha.6" + "@lumino/keyboard" "^2.0.0-alpha.6" + "@lumino/messaging" "^2.0.0-alpha.6" + "@lumino/signaling" "^2.0.0-alpha.6" + "@lumino/widgets" "^2.0.0-alpha.6" + "@lumino/disposable@^2.0.0-alpha.6": version "2.0.0-alpha.6" resolved "https://registry.yarnpkg.com/@lumino/disposable/-/disposable-2.0.0-alpha.6.tgz#ccb899b7b4ad6db359c351fe2b2ba989977b06a7" @@ -4559,6 +4650,11 @@ resolved "https://registry.yarnpkg.com/@verdaccio/ui-theme/-/ui-theme-6.0.0-6-next.48.tgz#23bbc8037bf9e1b27600f5160a0ff716982db48b" integrity sha512-1jls+cpfEXqXc1ZzqLGGNs6YCyG6B6QwDCezEkSvgKm+9A49FnSJ2n2dNIGcQYOszwHmd8EvwN98OEIx3Bbtrw== +"@vscode/debugprotocol@^1.51.0": + version "1.57.0" + resolved "https://registry.yarnpkg.com/@vscode/debugprotocol/-/debugprotocol-1.57.0.tgz#f055c0422b1f77358a12b1415623099ba0541647" + integrity sha512-eww0WhAtj3lPX7+7tGkxQ3P7IRC3hS7+SVL7fmM8CAat2DMM+PVjg1FQbTCtMw6EwNSmT/qMx1iZCyzQguJJKA== + "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"