diff --git a/packages/application-extension/src/index.ts b/packages/application-extension/src/index.ts index 13c02193b52..9bf2ba9f0cd 100644 --- a/packages/application-extension/src/index.ts +++ b/packages/application-extension/src/index.ts @@ -47,6 +47,8 @@ import { SidePanel, SidePanelHandler, SidePanelPalette, + INotebookPathOpener, + defaultNotebookPathOpener, } from '@jupyter-notebook/application'; import { jupyterIcon } from '@jupyter-notebook/ui-components'; @@ -301,7 +303,7 @@ const pages: JupyterFrontEndPlugin = { activate: ( app: JupyterFrontEnd, translator: ITranslator, - palette: ICommandPalette | null + palette: ICommandPalette | null, ): void => { const trans = translator.load('notebook'); const baseUrl = PageConfig.getBaseUrl(); @@ -309,7 +311,7 @@ const pages: JupyterFrontEndPlugin = { app.commands.addCommand(CommandIDs.openLab, { label: trans.__('Open JupyterLab'), execute: () => { - window.open(`${baseUrl}lab`); + window.open(URLExt.join(baseUrl, 'lab')); }, }); const page = PageConfig.getOption('notebookPage'); @@ -320,7 +322,7 @@ const pages: JupyterFrontEndPlugin = { if (page === 'tree') { app.commands.execute('filebrowser:activate'); } else { - window.open(`${baseUrl}tree`); + window.open(URLExt.join(baseUrl, 'tree')); } }, }); @@ -332,6 +334,18 @@ const pages: JupyterFrontEndPlugin = { }, }; +/** + * A plugin to open paths in new browser tabs. + */ +const pathOpener: JupyterFrontEndPlugin = { + id: '@jupyter-notebook/application-extension:path-opener', + autoStart: true, + provides: INotebookPathOpener, + activate: (app: JupyterFrontEnd): INotebookPathOpener => { + return defaultNotebookPathOpener; + } +}; + /** * The default paths for a Jupyter Notebook app. */ @@ -361,6 +375,7 @@ const rendermime: JupyterFrontEndPlugin = { ISanitizer, IMarkdownParser, ITranslator, + INotebookPathOpener, ], activate: ( app: JupyterFrontEnd, @@ -368,9 +383,11 @@ const rendermime: JupyterFrontEndPlugin = { latexTypesetter: ILatexTypesetter | null, sanitizer: IRenderMime.ISanitizer | null, markdownParser: IMarkdownParser | null, - translator: ITranslator | null + translator: ITranslator | null, + notebookPathOpener: INotebookPathOpener | null, ) => { const trans = (translator ?? nullTranslator).load('jupyterlab'); + const opener = notebookPathOpener ?? defaultNotebookPathOpener; if (docManager) { app.commands.addCommand(CommandIDs.handleLink, { label: trans.__('Handle Local Link'), @@ -382,10 +399,12 @@ const rendermime: JupyterFrontEndPlugin = { return docManager.services.contents .get(path, { content: false }) .then((model) => { - // Open in a new browser tab - const url = PageConfig.getBaseUrl(); - const treeUrl = URLExt.join(url, 'tree', model.path); - window.open(treeUrl, '_blank'); + const baseUrl = PageConfig.getBaseUrl(); + opener.open({ + route: URLExt.join(baseUrl, 'tree'), + path: model.path, + target: '_blank', + }) }); }, }); @@ -395,18 +414,18 @@ const rendermime: JupyterFrontEndPlugin = { linkHandler: !docManager ? undefined : { - handleLink: (node: HTMLElement, path: string, id?: string) => { - // If node has the download attribute explicitly set, use the - // default browser downloading behavior. - if (node.tagName === 'A' && node.hasAttribute('download')) { - return; - } - app.commandLinker.connectNode(node, CommandIDs.handleLink, { - path, - id, - }); - }, + handleLink: (node: HTMLElement, path: string, id?: string) => { + // If node has the download attribute explicitly set, use the + // default browser downloading behavior. + if (node.tagName === 'A' && node.hasAttribute('download')) { + return; + } + app.commandLinker.connectNode(node, CommandIDs.handleLink, { + path, + id, + }); }, + }, latexTypesetter: latexTypesetter ?? undefined, markdownParser: markdownParser ?? undefined, translator: translator ?? undefined, @@ -1089,6 +1108,7 @@ const plugins: JupyterFrontEndPlugin[] = [ menuSpacer, opener, pages, + pathOpener, paths, rendermime, shell, diff --git a/packages/application/src/app.ts b/packages/application/src/app.ts index f909fa4f3a7..648b01ea02a 100644 --- a/packages/application/src/app.ts +++ b/packages/application/src/app.ts @@ -18,9 +18,7 @@ import { IRenderMime } from '@jupyterlab/rendermime-interfaces'; import { Throttler } from '@lumino/polling'; -import { NotebookShell } from './shell'; - -import { INotebookShell } from './tokens'; +import { INotebookShell, NotebookShell } from './shell'; /** * App is the main application class. It is instantiated once and shared. @@ -165,7 +163,7 @@ export namespace NotebookApp { */ export interface IOptions extends JupyterFrontEnd.IOptions, - Partial {} + Partial { } /** * The information about a Jupyter Notebook application. diff --git a/packages/application/src/index.ts b/packages/application/src/index.ts index 3ea563baafd..c726fb4561d 100644 --- a/packages/application/src/index.ts +++ b/packages/application/src/index.ts @@ -4,4 +4,5 @@ export * from './app'; export * from './shell'; export * from './panelhandler'; +export * from './pathopener'; export * from './tokens'; diff --git a/packages/application/src/pathopener.ts b/packages/application/src/pathopener.ts new file mode 100644 index 00000000000..66048e4bf0a --- /dev/null +++ b/packages/application/src/pathopener.ts @@ -0,0 +1,25 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { URLExt } from "@jupyterlab/coreutils"; + +import { INotebookPathOpener } from "./tokens"; + +/** + * A class to open path in new browser tabs in the Notebook application. + */ +class DefaultNotebookPathOpener implements INotebookPathOpener { + /** + * Open a path in a new browser tab. + */ + open(options: INotebookPathOpener.IOpenOptions): WindowProxy | null { + const { route, path, searchParams, target, features } = options; + const url = new URL(URLExt.join(route, path ?? ''), window.location.origin); + if (searchParams) { + url.search = searchParams.toString(); + } + return window.open(url, target, features); + } +} + +export const defaultNotebookPathOpener = new DefaultNotebookPathOpener(); diff --git a/packages/application/src/shell.ts b/packages/application/src/shell.ts index 5a9ff9476ba..2180a55140c 100644 --- a/packages/application/src/shell.ts +++ b/packages/application/src/shell.ts @@ -6,14 +6,23 @@ import { DocumentRegistry } from '@jupyterlab/docregistry'; import { ITranslator, nullTranslator } from '@jupyterlab/translation'; import { find } from '@lumino/algorithm'; -import { JSONExt, PromiseDelegate } from '@lumino/coreutils'; +import { JSONExt, PromiseDelegate, Token } from '@lumino/coreutils'; import { ISignal, Signal } from '@lumino/signaling'; import { BoxLayout, Panel, SplitPanel, Widget } from '@lumino/widgets'; - import { PanelHandler, SidePanelHandler } from './panelhandler'; -import { INotebookShell } from './tokens'; +/** + * The Jupyter Notebook application shell token. + */ +export const INotebookShell = new Token( + '@jupyter-notebook/application:INotebookShell' +); + +/** + * The Jupyter Notebook application shell interface. + */ +export interface INotebookShell extends NotebookShell {} /** * The namespace for INotebookShell type information. diff --git a/packages/application/src/tokens.ts b/packages/application/src/tokens.ts index eb7083b8946..503aaf7ae3c 100644 --- a/packages/application/src/tokens.ts +++ b/packages/application/src/tokens.ts @@ -1,29 +1,54 @@ import { Token } from '@lumino/coreutils'; -import { NotebookShell } from './shell'; - /** * The INotebookPathOpener interface. */ export interface INotebookPathOpener { - open: (route: string, path?: string) => void; + /** + * Open a path in the application. + * + * @param options - The options used to open the path. + */ + open: (options: INotebookPathOpener.IOpenOptions) => WindowProxy | null; +} + +export namespace INotebookPathOpener { + export interface IOpenOptions { + /** + * The base route, which should include the base URL + */ + route: string + + /** + * The path to open in the application, e.g `setup.py`, or `notebooks/example.ipynb` + */ + path?: string, + + /** + * The extra search params to use in the URL. + */ + searchParams?: URLSearchParams; + + /** + * Name of the browsing context the resource is being loaded into. + * See https://developer.mozilla.org/en-US/docs/Web/API/Window/open for more details. + */ + target?: string; + + /** + * + * See https://developer.mozilla.org/en-US/docs/Web/API/Window/open for more details. + */ + features?: string; + } } /** * The INotebookPathOpener token. + * The main purpose of this token is to allow other extensions or downstream application + * to override the default behavior of opening a notebook in a new tab. + * It also allows to pass the path open as a search parame, or other options to the window.open call. */ export const INotebookPathOpener = new Token( '@jupyter-notebook/application:INotebookPathOpener' ); - -/** - * The Jupyter Notebook application shell interface. - */ -export interface INotebookShell extends NotebookShell {} - -/** - * The Jupyter Notebook application shell token. - */ -export const INotebookShell = new Token( - '@jupyter-notebook/application:INotebookShell' -); diff --git a/packages/console-extension/src/index.ts b/packages/console-extension/src/index.ts index 92085ace476..07d69b5fed2 100644 --- a/packages/console-extension/src/index.ts +++ b/packages/console-extension/src/index.ts @@ -9,7 +9,9 @@ import { import { IConsoleTracker } from '@jupyterlab/console'; -import { PageConfig } from '@jupyterlab/coreutils'; +import { PageConfig, URLExt } from '@jupyterlab/coreutils'; + +import { INotebookPathOpener, defaultNotebookPathOpener } from '@jupyter-notebook/application'; import { find } from '@lumino/algorithm'; @@ -52,9 +54,12 @@ const opener: JupyterFrontEndPlugin = { const redirect: JupyterFrontEndPlugin = { id: '@jupyter-notebook/console-extension:redirect', requires: [IConsoleTracker], + optional: [INotebookPathOpener], autoStart: true, - activate: (app: JupyterFrontEnd, tracker: IConsoleTracker) => { + activate: (app: JupyterFrontEnd, tracker: IConsoleTracker, notebookPathOpener: INotebookPathOpener | null) => { const baseUrl = PageConfig.getBaseUrl(); + const opener = notebookPathOpener ?? defaultNotebookPathOpener; + tracker.widgetAdded.connect(async (send, console) => { const { sessionContext } = console; await sessionContext.ready; @@ -66,7 +71,11 @@ const redirect: JupyterFrontEndPlugin = { // bail if the console is already added to the main area return; } - window.open(`${baseUrl}consoles/${sessionContext.path}`, '_blank'); + opener.open({ + route: URLExt.join(baseUrl, 'consoles'), + path: sessionContext.path, + target: '_blank', + }) // the widget is not needed anymore console.dispose(); diff --git a/packages/docmanager-extension/src/index.ts b/packages/docmanager-extension/src/index.ts index 4f352d29e32..32ad52d0122 100644 --- a/packages/docmanager-extension/src/index.ts +++ b/packages/docmanager-extension/src/index.ts @@ -6,13 +6,13 @@ import { JupyterFrontEndPlugin, } from '@jupyterlab/application'; -import { PageConfig, PathExt } from '@jupyterlab/coreutils'; +import { PageConfig, PathExt, URLExt } from '@jupyterlab/coreutils'; import { IDocumentWidgetOpener } from '@jupyterlab/docmanager'; import { IDocumentWidget, DocumentRegistry } from '@jupyterlab/docregistry'; -import { INotebookShell } from '@jupyter-notebook/application'; +import { INotebookPathOpener, INotebookShell, defaultNotebookPathOpener } from '@jupyter-notebook/application'; import { Signal } from '@lumino/signaling'; @@ -23,11 +23,12 @@ import { Signal } from '@lumino/signaling'; const opener: JupyterFrontEndPlugin = { id: '@jupyter-notebook/docmanager-extension:opener', autoStart: true, - optional: [INotebookShell], + optional: [INotebookPathOpener, INotebookShell], provides: IDocumentWidgetOpener, - activate: (app: JupyterFrontEnd, notebookShell: INotebookShell | null) => { + activate: (app: JupyterFrontEnd, notebookPathOpener: INotebookPathOpener, notebookShell: INotebookShell | null) => { const baseUrl = PageConfig.getBaseUrl(); const docRegistry = app.docRegistry; + const pathOpener = notebookPathOpener ?? defaultNotebookPathOpener; let id = 0; return new (class { open(widget: IDocumentWidget, options?: DocumentRegistry.IOpenOptions) { @@ -46,13 +47,21 @@ const opener: JupyterFrontEndPlugin = { ) { route = 'notebooks'; } - let url = `${baseUrl}${route}/${path}`; // append ?factory only if it's not the default const defaultFactory = docRegistry.defaultWidgetFactory(path); + let searchParams = undefined; if (widgetName !== defaultFactory.name) { - url = `${url}?factory=${widgetName}`; + searchParams = new URLSearchParams({ + factory: widgetName, + }); } - window.open(url); + + pathOpener.open({ + route: URLExt.join(baseUrl, route), + path, + searchParams, + }); + // dispose the widget since it is not used on this page widget.dispose(); return; diff --git a/packages/lab-extension/src/index.ts b/packages/lab-extension/src/index.ts index dea33fb3253..e78a4330838 100644 --- a/packages/lab-extension/src/index.ts +++ b/packages/lab-extension/src/index.ts @@ -9,7 +9,7 @@ import { import { ICommandPalette, IToolbarWidgetRegistry } from '@jupyterlab/apputils'; -import { PageConfig } from '@jupyterlab/coreutils'; +import { PageConfig, URLExt } from '@jupyterlab/coreutils'; import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook'; @@ -17,7 +17,7 @@ import { ITranslator } from '@jupyterlab/translation'; import { Menu, MenuBar, Widget } from '@lumino/widgets'; -import { INotebookShell } from '@jupyter-notebook/application'; +import { defaultNotebookPathOpener, INotebookPathOpener, INotebookShell } from '@jupyter-notebook/application'; import { caretDownIcon, @@ -67,6 +67,7 @@ const interfaceSwitcher: JupyterFrontEndPlugin = { requires: [ITranslator, INotebookTracker], optional: [ ICommandPalette, + INotebookPathOpener, INotebookShell, ILabShell, IToolbarWidgetRegistry, @@ -76,6 +77,7 @@ const interfaceSwitcher: JupyterFrontEndPlugin = { translator: ITranslator, notebookTracker: INotebookTracker, palette: ICommandPalette | null, + notebookPathOpener: INotebookPathOpener | null, notebookShell: INotebookShell | null, labShell: ILabShell | null, toolbarRegistry: IToolbarWidgetRegistry | null @@ -87,6 +89,7 @@ const interfaceSwitcher: JupyterFrontEndPlugin = { PageConfig.getOption('nbclassic_enabled') === 'true'; const switcher = new Menu({ commands }); const switcherOptions: ISwitcherChoice[] = []; + const opener = notebookPathOpener ?? defaultNotebookPathOpener; if (!notebookShell) { switcherOptions.push({ @@ -94,7 +97,7 @@ const interfaceSwitcher: JupyterFrontEndPlugin = { commandLabel: trans.__('Notebook'), commandDescription: trans.__('Open in %1', 'Jupyter Notebook'), buttonLabel: 'openNotebook', - urlPrefix: `${baseUrl}tree/`, + urlPrefix: `${baseUrl}tree`, }); } @@ -104,7 +107,7 @@ const interfaceSwitcher: JupyterFrontEndPlugin = { commandLabel: trans.__('JupyterLab'), commandDescription: trans.__('Open in %1', 'JupyterLab'), buttonLabel: 'openLab', - urlPrefix: `${baseUrl}doc/tree/`, + urlPrefix: `${baseUrl}doc/tree`, }); } @@ -114,7 +117,7 @@ const interfaceSwitcher: JupyterFrontEndPlugin = { commandLabel: trans.__('NbClassic'), commandDescription: trans.__('Open in %1', 'NbClassic'), buttonLabel: 'openNbClassic', - urlPrefix: `${baseUrl}nbclassic/notebooks/`, + urlPrefix: `${baseUrl}nbclassic/notebooks`, }); } @@ -133,7 +136,10 @@ const interfaceSwitcher: JupyterFrontEndPlugin = { if (!current) { return; } - window.open(`${urlPrefix}${current.context.path}`); + opener.open({ + route: urlPrefix, + path: current.context.path, + }) }; commands.addCommand(command, { @@ -223,7 +229,8 @@ const launchNotebookTree: JupyterFrontEndPlugin = { commands.addCommand(CommandIDs.launchNotebookTree, { label: trans.__('Launch Jupyter Notebook File Browser'), execute: () => { - window.open(PageConfig.getBaseUrl() + 'tree'); + const url = URLExt.join(PageConfig.getBaseUrl(), 'tree'); + window.open(url); }, }); diff --git a/packages/terminal-extension/src/index.ts b/packages/terminal-extension/src/index.ts index a8dc01fb935..d455716ba95 100644 --- a/packages/terminal-extension/src/index.ts +++ b/packages/terminal-extension/src/index.ts @@ -7,10 +7,12 @@ import { JupyterFrontEndPlugin, } from '@jupyterlab/application'; -import { PageConfig } from '@jupyterlab/coreutils'; +import { PageConfig, URLExt } from '@jupyterlab/coreutils'; import { ITerminalTracker } from '@jupyterlab/terminal'; +import { INotebookPathOpener, defaultNotebookPathOpener } from '@jupyter-notebook/application'; + import { find } from '@lumino/algorithm'; /** @@ -58,9 +60,12 @@ const opener: JupyterFrontEndPlugin = { const redirect: JupyterFrontEndPlugin = { id: '@jupyter-notebook/terminal-extension:redirect', requires: [ITerminalTracker], + optional: [INotebookPathOpener], autoStart: true, - activate: (app: JupyterFrontEnd, tracker: ITerminalTracker) => { + activate: (app: JupyterFrontEnd, tracker: ITerminalTracker, notebookPathOpener: INotebookPathOpener | null) => { const baseUrl = PageConfig.getBaseUrl(); + const opener = notebookPathOpener ?? defaultNotebookPathOpener; + tracker.widgetAdded.connect((send, terminal) => { const widget = find( app.shell.widgets('main'), @@ -71,7 +76,11 @@ const redirect: JupyterFrontEndPlugin = { return; } const name = terminal.content.session.name; - window.open(`${baseUrl}terminals/${name}`, '_blank'); + opener.open({ + route: URLExt.join(baseUrl, 'terminals'), + path: name, + target: '_blank', + }) // dispose the widget since it is not used on this page terminal.dispose();