From fdc55393cb1c26a4ee77bcd85f2bc59fd2cf6da5 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Wed, 9 Dec 2020 16:31:21 +0100 Subject: [PATCH] Add Edit page --- builder/index.js | 9 ++ builder/package.json | 1 + builder/style.css | 1 + jupyterlab_classic/app.py | 14 +++ jupyterlab_classic/templates/edit.html | 36 ++++++ packages/application-extension/package.json | 1 + packages/application-extension/src/index.ts | 114 +++++++++++++++++- packages/application-extension/style/base.css | 4 + packages/docmanager-extension/src/index.ts | 6 +- packages/notebook-extension/src/index.ts | 97 +-------------- yarn.lock | 40 ++++++ 11 files changed, 225 insertions(+), 98 deletions(-) create mode 100644 jupyterlab_classic/templates/edit.html diff --git a/builder/index.js b/builder/index.js index 9091dfcdb..0bfa6af9c 100644 --- a/builder/index.js +++ b/builder/index.js @@ -129,6 +129,15 @@ async function main() { ].includes(id) ) ]); + } else if (page === 'edit') { + mods = mods.concat([ + require('@jupyterlab/fileeditor-extension').default.filter(({ id }) => + ['@jupyterlab/fileeditor-extension:plugin'].includes(id) + ), + require('@jupyterlab-classic/tree-extension').default.filter(({ id }) => + ['@jupyterlab-classic/tree-extension:factory'].includes(id) + ) + ]); } const extension_data = JSON.parse( diff --git a/builder/package.json b/builder/package.json index 26c48f00c..a06fd39f4 100644 --- a/builder/package.json +++ b/builder/package.json @@ -20,6 +20,7 @@ "@jupyterlab/codemirror-extension": "^3.0.0-rc.12", "@jupyterlab/completer-extension": "^3.0.0-rc.12", "@jupyterlab/docmanager-extension": "^3.0.0-rc.12", + "@jupyterlab/fileeditor-extension": "^3.0.0-rc.12", "@jupyterlab/mainmenu-extension": "^3.0.0-rc.12", "@jupyterlab/mathjax2-extension": "^3.0.0-rc.12", "@jupyterlab/notebook-extension": "^3.0.0-rc.12", diff --git a/builder/style.css b/builder/style.css index 41f777daa..cc75c1bbf 100644 --- a/builder/style.css +++ b/builder/style.css @@ -10,6 +10,7 @@ @import url('~@jupyterlab/codemirror-extension/style/index.css'); @import url('~@jupyterlab/docmanager-extension/style/index.css'); +@import url('~@jupyterlab/fileeditor-extension/style/index.css'); @import url('~@jupyterlab/mainmenu-extension/style/index.css'); @import url('~@jupyterlab/notebook-extension/style/index.css'); @import url('~@jupyterlab/rendermime-extension/style/index.css'); diff --git a/jupyterlab_classic/app.py b/jupyterlab_classic/app.py index da4d5c7f3..d8e06f0cd 100644 --- a/jupyterlab_classic/app.py +++ b/jupyterlab_classic/app.py @@ -87,6 +87,19 @@ class ClassicTreeHandler(ClassicPageConfigMixin, ExtensionHandlerJinjaMixin, Ext ) +class ClassicFileHandler(ClassicPageConfigMixin, ExtensionHandlerJinjaMixin, ExtensionHandlerMixin, JupyterHandler): + @web.authenticated + def get(self, path=None): + page_config = self.get_page_config() + return self.write( + self.render_template( + "edit.html", + base_url=self.base_url, + token=self.settings["token"], + page_config=page_config, + ) + ) + class ClassicNotebookHandler(ClassicPageConfigMixin, ExtensionHandlerJinjaMixin, ExtensionHandlerMixin, JupyterHandler): @web.authenticated def get(self, path=None): @@ -118,6 +131,7 @@ class ClassicApp(NBClassicConfigShimMixin, LabServerApp): super().initialize_handlers() self.handlers.append(("/classic/tree(.*)", ClassicTreeHandler)) self.handlers.append(("/classic/notebooks(.*)", ClassicNotebookHandler)) + self.handlers.append(("/classic/edit(.*)", ClassicFileHandler)) def initialize_templates(self): super().initialize_templates() diff --git a/jupyterlab_classic/templates/edit.html b/jupyterlab_classic/templates/edit.html new file mode 100644 index 000000000..8f4e0f94e --- /dev/null +++ b/jupyterlab_classic/templates/edit.html @@ -0,0 +1,36 @@ + + + + + + {{page_config['appName'] | e}} - Edit + + + + {# Copy so we do not modify the page_config with updates. #} + {% set page_config_full = page_config.copy() %} + + {# Set a dummy variable - we just want the side effect of the update. #} + {% set _ = page_config_full.update(baseUrl=base_url, wsUrl=ws_url) %} + + {# Sentinel value to say that we are on the tree page #} + {% set _ = page_config_full.update(classicPage='edit') %} + + + + + + + + diff --git a/packages/application-extension/package.json b/packages/application-extension/package.json index fd75796dc..6e1bd1578 100644 --- a/packages/application-extension/package.json +++ b/packages/application-extension/package.json @@ -43,6 +43,7 @@ "@jupyterlab/codeeditor": "^3.0.0-rc.12", "@jupyterlab/codemirror": "^3.0.0-rc.12", "@jupyterlab/coreutils": "^5.0.0-rc.12", + "@jupyterlab/docmanager": "^3.0.0-rc.12", "@jupyterlab/docregistry": "^3.0.0-rc.12", "@jupyterlab/mainmenu": "^3.0.0-rc.12", "@jupyterlab/settingregistry": "^3.0.0-rc.12", diff --git a/packages/application-extension/src/index.ts b/packages/application-extension/src/index.ts index d8a1d9128..e82afdb1d 100644 --- a/packages/application-extension/src/index.ts +++ b/packages/application-extension/src/index.ts @@ -15,7 +15,11 @@ import { ICommandPalette } from '@jupyterlab/apputils'; -import { PageConfig } from '@jupyterlab/coreutils'; +import { PageConfig, PathExt } from '@jupyterlab/coreutils'; + +import { IDocumentManager, renameDialog } from '@jupyterlab/docmanager'; + +import { DocumentWidget } from '@jupyterlab/docregistry'; import { IMainMenu } from '@jupyterlab/mainmenu'; @@ -31,6 +35,16 @@ import { jupyterIcon } from '@jupyterlab-classic/ui-components'; import { Widget } from '@lumino/widgets'; +/** + * The default notebook factory. + */ +const NOTEBOOK_FACTORY = 'Notebook'; + +/** + * The editor factory. + */ +const EDITOR_FACTORY = 'Editor'; + /** * The command IDs used by the application plugin. */ @@ -81,6 +95,55 @@ const logo: JupyterFrontEndPlugin = { } }; +/** + * A plugin to open document in the main area. + */ +const opener: JupyterFrontEndPlugin = { + id: '@jupyterlab-classic/application-extension:opener', + autoStart: true, + requires: [IRouter, IDocumentManager], + activate: ( + app: JupyterFrontEnd, + router: IRouter, + docManager: IDocumentManager + ): void => { + const { commands } = app; + const treePattern = new RegExp('/(notebooks|edit)/(.*)'); + + const command = 'router:tree'; + commands.addCommand(command, { + execute: (args: any) => { + const parsed = args as IRouter.ILocation; + const matches = parsed.path.match(treePattern); + if (!matches) { + return; + } + + const [, , file] = matches; + if (!file) { + return; + } + + const ext = PathExt.extname(file); + app.restored.then(() => { + // TODO: get factory from file type instead? + if (ext === '.ipynb') { + docManager.open(file, NOTEBOOK_FACTORY, undefined, { + ref: '_noref' + }); + } else { + docManager.open(file, EDITOR_FACTORY, undefined, { + ref: '_noref' + }); + } + }); + } + }); + + router.register({ command, pattern: treePattern }); + } +}; + /** * A plugin to dispose the Tabs menu */ @@ -220,6 +283,53 @@ const spacer: JupyterFrontEndPlugin = { } }; +/** + * A plugin to display and rename the title of a file + */ +const title: JupyterFrontEndPlugin = { + id: '@jupyterlab-classic/application-extension:title', + autoStart: true, + requires: [IClassicShell], + optional: [IDocumentManager, IRouter], + activate: ( + app: JupyterFrontEnd, + shell: IClassicShell, + docManager: IDocumentManager | null, + router: IRouter | null + ) => { + // TODO: this signal might not be needed if we assume there is always only + // one notebook in the main area + const widget = new Widget(); + widget.id = 'jp-title'; + app.shell.add(widget, 'top', { rank: 10 }); + + shell.currentChanged.connect(async () => { + const current = shell.currentWidget; + if (!(current instanceof DocumentWidget)) { + return; + } + const h = document.createElement('h1'); + h.textContent = current.title.label; + widget.node.appendChild(h); + widget.node.style.marginLeft = '10px'; + if (docManager) { + widget.node.onclick = async () => { + const result = await renameDialog(docManager, current.context.path); + if (result) { + h.textContent = result.path; + if (router) { + // TODO: better handle this + router.navigate(`/classic/notebooks/${result.path}`, { + skipRouting: true + }); + } + } + }; + } + }); + } +}; + /** * Plugin to toggle the top header visibility. */ @@ -328,12 +438,14 @@ const zen: JupyterFrontEndPlugin = { const plugins: JupyterFrontEndPlugin[] = [ logo, noTabsMenu, + opener, pages, paths, router, sessionDialogs, shell, spacer, + title, topVisibility, translator, zen diff --git a/packages/application-extension/style/base.css b/packages/application-extension/style/base.css index 70f46ed68..49ae28fcf 100644 --- a/packages/application-extension/style/base.css +++ b/packages/application-extension/style/base.css @@ -8,3 +8,7 @@ flex-grow: 1; flex-shrink: 1; } + +.jp-Document { + height: 100%; +} diff --git a/packages/docmanager-extension/src/index.ts b/packages/docmanager-extension/src/index.ts index 656556cae..44d1967d7 100644 --- a/packages/docmanager-extension/src/index.ts +++ b/packages/docmanager-extension/src/index.ts @@ -6,7 +6,7 @@ import { JupyterFrontEndPlugin } from '@jupyterlab/application'; -import { PageConfig } from '@jupyterlab/coreutils'; +import { PageConfig, PathExt } from '@jupyterlab/coreutils'; import { IDocumentManager } from '@jupyterlab/docmanager'; @@ -39,7 +39,9 @@ const opener: JupyterFrontEndPlugin = { docOpen.call(docManager, path, widgetName, kernel, options); return; } - window.open(`${baseUrl}classic/notebooks/${path}`); + const ext = PathExt.extname(path); + const route = ext === '.ipynb' ? 'notebooks' : 'edit'; + window.open(`${baseUrl}classic/${route}/${path}`); return undefined; }; } diff --git a/packages/notebook-extension/src/index.ts b/packages/notebook-extension/src/index.ts index 31600b520..c50fcb873 100644 --- a/packages/notebook-extension/src/index.ts +++ b/packages/notebook-extension/src/index.ts @@ -2,7 +2,6 @@ // Distributed under the terms of the Modified BSD License. import { - IRouter, JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; @@ -11,7 +10,7 @@ import { ISessionContext, DOMUtils } from '@jupyterlab/apputils'; import { PageConfig, Text, Time } from '@jupyterlab/coreutils'; -import { IDocumentManager, renameDialog } from '@jupyterlab/docmanager'; +import { IDocumentManager } from '@jupyterlab/docmanager'; import { NotebookPanel } from '@jupyterlab/notebook'; @@ -23,11 +22,6 @@ import { import { Widget } from '@lumino/widgets'; -/** - * The default notebook factory. - */ -const NOTEBOOK_FACTORY = 'Notebook'; - /** * The class for kernel status errors. */ @@ -240,100 +234,13 @@ const shell: JupyterFrontEndPlugin = { provides: IClassicShell }; -/** - * A plugin to display the title of the notebook - */ -const title: JupyterFrontEndPlugin = { - id: '@jupyterlab-classic/application-extension:title', - autoStart: true, - requires: [IClassicShell], - optional: [IDocumentManager, IRouter], - activate: ( - app: JupyterFrontEnd, - shell: IClassicShell, - docManager: IDocumentManager | null, - router: IRouter | null - ) => { - // TODO: this signal might not be needed if we assume there is always only - // one notebook in the main area - const widget = new Widget(); - widget.id = 'jp-title'; - app.shell.add(widget, 'top', { rank: 10 }); - - shell.currentChanged.connect(async () => { - const current = shell.currentWidget; - if (!(current instanceof NotebookPanel)) { - return; - } - const h = document.createElement('h1'); - h.textContent = current.title.label; - widget.node.appendChild(h); - widget.node.style.marginLeft = '10px'; - if (docManager) { - widget.node.onclick = async () => { - const result = await renameDialog( - docManager, - current.sessionContext.path - ); - if (result) { - h.textContent = result.path; - if (router) { - // TODO: better handle this - router.navigate(`/classic/notebooks/${result.path}`, { - skipRouting: true - }); - } - } - }; - } - }); - } -}; - -/** - * The default tree route resolver plugin. - */ -const tree: JupyterFrontEndPlugin = { - id: '@jupyterlab-classic/application-extension:tree-resolver', - autoStart: true, - requires: [IRouter, IDocumentManager], - activate: ( - app: JupyterFrontEnd, - router: IRouter, - docManager: IDocumentManager - ): void => { - const { commands } = app; - const treePattern = new RegExp('/notebooks/(.*)'); - - const command = 'router:tree'; - commands.addCommand(command, { - execute: (args: any) => { - const parsed = args as IRouter.ILocation; - const matches = parsed.path.match(treePattern); - if (!matches) { - return; - } - const [, path] = matches; - - app.restored.then(() => { - docManager.open(path, NOTEBOOK_FACTORY, undefined, { ref: '_noref' }); - }); - } - }); - - router.register({ command, pattern: treePattern }); - } -}; - /** * Export the plugins as default. */ const plugins: JupyterFrontEndPlugin[] = [ checkpoints, kernelLogo, - kernelStatus, - title, - tree + kernelStatus ]; export default plugins; diff --git a/yarn.lock b/yarn.lock index c07523441..4244c057e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1758,6 +1758,30 @@ "@lumino/widgets" "^1.16.1" react "^17.0.1" +"@jupyterlab/fileeditor-extension@^3.0.0-rc.12": + version "3.0.0-rc.13" + resolved "https://registry.yarnpkg.com/@jupyterlab/fileeditor-extension/-/fileeditor-extension-3.0.0-rc.13.tgz#faa580b0883da4548bbb0b993e2b9db4d1473750" + integrity sha512-YlvZA6R+Mqe+oAeMgfPZwlqCbv8XUNLVVIOz0YNvF8qBhsR8WjvMBYqeUUkPm7cr7kHlnZ+EvWmHwEYl01fqLQ== + dependencies: + "@jupyterlab/application" "^3.0.0-rc.13" + "@jupyterlab/apputils" "^3.0.0-rc.13" + "@jupyterlab/codeeditor" "^3.0.0-rc.13" + "@jupyterlab/codemirror" "^3.0.0-rc.13" + "@jupyterlab/console" "^3.0.0-rc.13" + "@jupyterlab/coreutils" "^5.0.0-rc.13" + "@jupyterlab/docregistry" "^3.0.0-rc.13" + "@jupyterlab/filebrowser" "^3.0.0-rc.13" + "@jupyterlab/fileeditor" "^3.0.0-rc.13" + "@jupyterlab/launcher" "^3.0.0-rc.13" + "@jupyterlab/mainmenu" "^3.0.0-rc.13" + "@jupyterlab/settingregistry" "^3.0.0-rc.13" + "@jupyterlab/statusbar" "^3.0.0-rc.13" + "@jupyterlab/translation" "^3.0.0-rc.13" + "@jupyterlab/ui-components" "^3.0.0-rc.13" + "@lumino/commands" "^1.12.0" + "@lumino/coreutils" "^1.5.3" + "@lumino/widgets" "^1.16.1" + "@jupyterlab/fileeditor@^3.0.0-rc.12": version "3.0.0-rc.12" resolved "https://registry.yarnpkg.com/@jupyterlab/fileeditor/-/fileeditor-3.0.0-rc.12.tgz#e95711adfd83f4dcb62d6bf253de386f4823b086" @@ -1806,6 +1830,22 @@ "@lumino/widgets" "^1.16.1" react "^17.0.1" +"@jupyterlab/launcher@^3.0.0-rc.13": + version "3.0.0-rc.13" + resolved "https://registry.yarnpkg.com/@jupyterlab/launcher/-/launcher-3.0.0-rc.13.tgz#f128c1bbb0d23b44ed38a9ff08c4f7f6e8b4b2fd" + integrity sha512-Rh0tELQhHcxEUtsDPaNLA2GLOBFW9U5kXqrGXs8imLyDoxxfgwjugcfab79IltDWX6c6brTHFu6Uei9zaDwdmQ== + dependencies: + "@jupyterlab/apputils" "^3.0.0-rc.13" + "@jupyterlab/translation" "^3.0.0-rc.13" + "@jupyterlab/ui-components" "^3.0.0-rc.13" + "@lumino/algorithm" "^1.3.3" + "@lumino/commands" "^1.12.0" + "@lumino/coreutils" "^1.5.3" + "@lumino/disposable" "^1.4.3" + "@lumino/properties" "^1.2.3" + "@lumino/widgets" "^1.16.1" + react "^17.0.1" + "@jupyterlab/logconsole@^3.0.0-rc.12": version "3.0.0-rc.12" resolved "https://registry.yarnpkg.com/@jupyterlab/logconsole/-/logconsole-3.0.0-rc.12.tgz#cb3b9e48577542bdeeb4221c17218b59909ce5cd"