diff --git a/README.md b/README.md index 8452f3602..b051714b2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # jupyterlab-classic ![Github Actions Status](https://github.com/jtpio/jupyterlab-classic/workflows/Build/badge.svg) -[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/jtpio/jupyterlab-classic/main?urlpath=classic/notebooks/binder/example.ipynb) +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/jtpio/jupyterlab-classic/main?urlpath=classic/tree) The next gen old-school Notebook UI. @@ -37,6 +37,10 @@ jupyter labextension list Should also be available when starting `jupyterlab-classic`. +This package also adds a toolbar button by default, to make it easier to switch to the classic view: + +![open-jupyterlab-classic](https://user-images.githubusercontent.com/591645/101534129-02844580-3997-11eb-962a-5475bcc831bb.gif) + ## Motivation JupyterLab is the next-gen UI for Project Jupyter. Approaching version 3.0, it is becoming more mature and provides an advanced computational environment, that can sometimes be compared to what traditional IDEs offer. diff --git a/builder/index.js b/builder/index.js index e5b1a8607..81e274d90 100644 --- a/builder/index.js +++ b/builder/index.js @@ -67,8 +67,13 @@ async function main() { const app = new App(); // TODO: formalize the way the set of initial extensions and plugins are specified - const mods = [ + let mods = [ + // @jupyterlab-classic plugins require('@jupyterlab-classic/application-extension'), + require('@jupyterlab-classic/docmanager-extension'), + require('@jupyterlab-classic/notebook-extension'), + + // @jupyterlab plugins require('@jupyterlab/apputils-extension').default.filter(({ id }) => [ '@jupyterlab/apputils-extension:palette', @@ -80,36 +85,52 @@ async function main() { require('@jupyterlab/codemirror-extension').default.filter(({ id }) => ['@jupyterlab/codemirror-extension:services'].includes(id) ), - require('@jupyterlab/completer-extension').default.filter(({ id }) => - [ - '@jupyterlab/completer-extension:manager', - '@jupyterlab/completer-extension:notebooks' - ].includes(id) - ), require('@jupyterlab/docmanager-extension').default.filter(({ id }) => ['@jupyterlab/docmanager-extension:plugin'].includes(id) ), require('@jupyterlab/mainmenu-extension'), require('@jupyterlab/mathjax2-extension'), - require('@jupyterlab/rendermime-extension'), require('@jupyterlab/notebook-extension').default.filter(({ id }) => [ '@jupyterlab/notebook-extension:factory', - '@jupyterlab/notebook-extension:widget-factory', - '@jupyterlab/notebook-extension:tracker' + '@jupyterlab/notebook-extension:tracker', + '@jupyterlab/notebook-extension:widget-factory' ].includes(id) ), + require('@jupyterlab/rendermime-extension'), require('@jupyterlab/shortcuts-extension'), - require('@jupyterlab/tooltip-extension').default.filter(({ id }) => - [ - '@jupyterlab/tooltip-extension:manager', - '@jupyterlab/tooltip-extension:notebooks' - ].includes(id) - ), require('@jupyterlab/theme-light-extension'), require('@jupyterlab/theme-dark-extension') ]; + const page = PageConfig.getOption('classicPage'); + if (page === 'tree') { + mods = mods.concat([ + require('@jupyterlab-classic/filebrowser-extension').default.filter( + ({ id }) => + [ + '@jupyterlab-classic/filebrowser-extension:browser', + '@jupyterlab-classic/filebrowser-extension:factory' + ].includes(id) + ) + ]); + } else if (page === 'notebooks') { + mods = mods.concat([ + require('@jupyterlab/completer-extension').default.filter(({ id }) => + [ + '@jupyterlab/completer-extension:manager', + '@jupyterlab/completer-extension:notebooks' + ].includes(id) + ), + require('@jupyterlab/tooltip-extension').default.filter(({ id }) => + [ + '@jupyterlab/tooltip-extension:manager', + '@jupyterlab/tooltip-extension:notebooks' + ].includes(id) + ) + ]); + } + const extension_data = JSON.parse( PageConfig.getOption('federated_extensions') ); diff --git a/builder/package.json b/builder/package.json index be0888ab5..f9a5c0ce5 100644 --- a/builder/package.json +++ b/builder/package.json @@ -12,6 +12,9 @@ "dependencies": { "@jupyterlab-classic/application": "^0.1.0", "@jupyterlab-classic/application-extension": "^0.1.0", + "@jupyterlab-classic/docmanager-extension": "^0.1.0", + "@jupyterlab-classic/filebrowser-extension": "^0.1.0", + "@jupyterlab-classic/notebook-extension": "^0.1.0", "@jupyterlab-classic/ui-components": "^0.1.0", "@jupyterlab/apputils-extension": "^3.0.0-rc.12", "@jupyterlab/codemirror-extension": "^3.0.0-rc.12", diff --git a/builder/style.css b/builder/style.css index 9d04b781c..7a8664963 100644 --- a/builder/style.css +++ b/builder/style.css @@ -1,4 +1,6 @@ @import url('~@jupyterlab-classic/application-extension/style/index.css'); +@import url('~@jupyterlab-classic/filebrowser-extension/style/index.css'); +@import url('~@jupyterlab-classic/notebook-extension/style/index.css'); @import url('~@jupyterlab-classic/ui-components/style/index.css'); /* TODO: check is the the extension package can be used directly */ diff --git a/jupyterlab_classic/app.py b/jupyterlab_classic/app.py index 91a84eb61..da4d5c7f3 100644 --- a/jupyterlab_classic/app.py +++ b/jupyterlab_classic/app.py @@ -24,9 +24,8 @@ app_dir = get_app_dir() version = __version__ -class ClassicHandler(ExtensionHandlerJinjaMixin, ExtensionHandlerMixin, JupyterHandler): - @web.authenticated - def get(self, path=None): +class ClassicPageConfigMixin: + def get_page_config(self): config = LabConfig() app = self.extensionapp base_url = self.settings.get("base_url") @@ -71,7 +70,27 @@ class ClassicHandler(ExtensionHandlerJinjaMixin, ExtensionHandlerMixin, JupyterH logger=self.log, ), ) + return page_config + + +class ClassicTreeHandler(ClassicPageConfigMixin, ExtensionHandlerJinjaMixin, ExtensionHandlerMixin, JupyterHandler): + @web.authenticated + def get(self, path=None): + page_config = self.get_page_config() + return self.write( + self.render_template( + "tree.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): + page_config = self.get_page_config() return self.write( self.render_template( "notebooks.html", @@ -97,7 +116,8 @@ class ClassicApp(NBClassicConfigShimMixin, LabServerApp): def initialize_handlers(self): super().initialize_handlers() - self.handlers.append(("/classic/notebooks(.*)", ClassicHandler)) + self.handlers.append(("/classic/tree(.*)", ClassicTreeHandler)) + self.handlers.append(("/classic/notebooks(.*)", ClassicNotebookHandler)) def initialize_templates(self): super().initialize_templates() diff --git a/jupyterlab_classic/templates/notebooks.html b/jupyterlab_classic/templates/notebooks.html index e59e3e604..e0d63b637 100644 --- a/jupyterlab_classic/templates/notebooks.html +++ b/jupyterlab_classic/templates/notebooks.html @@ -3,7 +3,7 @@ - {{page_config['appName'] | e}} + {{page_config['appName'] | e}} - Notebook @@ -13,6 +13,9 @@ {# 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='notebooks') %} + diff --git a/jupyterlab_classic/templates/tree.html b/jupyterlab_classic/templates/tree.html new file mode 100644 index 000000000..afd11eab5 --- /dev/null +++ b/jupyterlab_classic/templates/tree.html @@ -0,0 +1,36 @@ + + + + + + {{page_config['appName'] | e}} - Tree + + + + {# 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='tree') %} + + + + + + + + diff --git a/packages/application-extension/package.json b/packages/application-extension/package.json index c33d3d32c..fd75796dc 100644 --- a/packages/application-extension/package.json +++ b/packages/application-extension/package.json @@ -36,16 +36,15 @@ "watch": "tsc -b --watch" }, "dependencies": { - "@jupyterlab-classic/application": "0.1.0", - "@jupyterlab-classic/ui-components": "0.1.0", + "@jupyterlab-classic/application": "^0.1.0", + "@jupyterlab-classic/ui-components": "^0.1.0", "@jupyterlab/application": "^3.0.0-rc.12", "@jupyterlab/apputils": "^3.0.0-rc.12", "@jupyterlab/codeeditor": "^3.0.0-rc.12", "@jupyterlab/codemirror": "^3.0.0-rc.12", + "@jupyterlab/coreutils": "^5.0.0-rc.12", "@jupyterlab/docregistry": "^3.0.0-rc.12", - "@jupyterlab/docmanager": "^3.0.0-rc.12", "@jupyterlab/mainmenu": "^3.0.0-rc.12", - "@jupyterlab/notebook": "^3.0.0-rc.12", "@jupyterlab/settingregistry": "^3.0.0-rc.12", "@jupyterlab/translation": "^3.0.0-rc.12", "@lumino/widgets": "^1.14.0" diff --git a/packages/application-extension/src/index.ts b/packages/application-extension/src/index.ts index 2a4525959..e9330693b 100644 --- a/packages/application-extension/src/index.ts +++ b/packages/application-extension/src/index.ts @@ -11,19 +11,14 @@ import { import { sessionContextDialogs, ISessionContextDialogs, - ISessionContext, DOMUtils, ICommandPalette } from '@jupyterlab/apputils'; -import { PageConfig, Text, Time } from '@jupyterlab/coreutils'; - -import { IDocumentManager, renameDialog } from '@jupyterlab/docmanager'; +import { PageConfig } from '@jupyterlab/coreutils'; import { IMainMenu } from '@jupyterlab/mainmenu'; -import { NotebookPanel } from '@jupyterlab/notebook'; - import { ITranslator, TranslationManager } from '@jupyterlab/translation'; import { @@ -36,31 +31,6 @@ import { jupyterIcon } from '@jupyterlab-classic/ui-components'; import { Widget } from '@lumino/widgets'; -/** - * The default notebook factory. - */ -const NOTEBOOK_FACTORY = 'Notebook'; - -/** - * The class for kernel status errors. - */ -const KERNEL_STATUS_ERROR_CLASS = 'jp-ClassicKernelStatus-error'; - -/** - * The class for kernel status warnings. - */ -const KERNEL_STATUS_WARN_CLASS = 'jp-ClassicKernelStatus-warn'; - -/** - * The class for kernel status infos. - */ -const KERNEL_STATUS_INFO_CLASS = 'jp-ClassicKernelStatus-info'; - -/** - * The class to fade out the kernel status. - */ -const KERNEL_STATUS_FADE_OUT_CLASS = 'jp-ClassicKernelStatus-fade'; - /** * The command IDs used by the application plugin. */ @@ -76,168 +46,6 @@ namespace CommandIDs { export const toggleZen = 'application:toggle-zen'; } -/** - * A plugin for the checkpoint indicator - */ -const checkpoints: JupyterFrontEndPlugin = { - id: '@jupyterlab-classic/application-extension:checkpoints', - autoStart: true, - requires: [IDocumentManager], - optional: [IClassicShell], - activate: ( - app: JupyterFrontEnd, - docManager: IDocumentManager, - classicShell: IClassicShell - ) => { - const { shell } = app; - const widget = new Widget(); - widget.id = DOMUtils.createDomID(); - widget.addClass('jp-ClassicCheckpoint'); - app.shell.add(widget, 'top', { rank: 100 }); - - const onChange = async () => { - const current = shell.currentWidget; - if (!current) { - return; - } - const context = docManager.contextForWidget(current); - - context?.fileChanged.disconnect(onChange); - context?.fileChanged.connect(onChange); - - const checkpoints = await context?.listCheckpoints(); - if (!checkpoints) { - return; - } - const checkpoint = checkpoints[checkpoints.length - 1]; - widget.node.textContent = `Last Checkpoint: ${Time.formatHuman( - new Date(checkpoint.last_modified) - )}`; - }; - - if (classicShell) { - classicShell.currentChanged.connect(onChange); - } - // TODO: replace by a Poll - onChange(); - setInterval(onChange, 2000); - } -}; - -/** - * The kernel logo plugin. - */ -const kernelLogo: JupyterFrontEndPlugin = { - id: '@jupyterlab-classic/application-extension:kernel-logo', - autoStart: true, - requires: [IClassicShell], - activate: (app: JupyterFrontEnd, shell: IClassicShell) => { - const { serviceManager } = app; - const baseUrl = PageConfig.getBaseUrl(); - - let widget: Widget; - // TODO: this signal might not be needed if we assume there is always only - // one notebook in the main area - const onChange = async () => { - if (widget) { - widget.dispose(); - widget.parent = null; - } - const current = shell.currentWidget; - if (!(current instanceof NotebookPanel)) { - return; - } - - await current.sessionContext.ready; - current.sessionContext.kernelChanged.disconnect(onChange); - current.sessionContext.kernelChanged.connect(onChange); - - const name = current.sessionContext.session?.kernel?.name ?? ''; - const spec = serviceManager.kernelspecs?.specs?.kernelspecs[name]; - if (!spec) { - return; - } - - let kernelIconUrl = spec.resources['logo-64x64']; - if (!kernelIconUrl) { - return; - } - - const index = kernelIconUrl.indexOf('kernelspecs'); - kernelIconUrl = baseUrl + kernelIconUrl.slice(index); - const node = document.createElement('div'); - const img = document.createElement('img'); - img.src = kernelIconUrl; - img.title = spec.display_name; - node.appendChild(img); - widget = new Widget({ node }); - widget.addClass('jp-ClassicKernelLogo'); - app.shell.add(widget, 'top', { rank: 10_010 }); - }; - - shell.currentChanged.connect(onChange); - } -}; - -/** - * A plugin to display the kernel status; - */ -const kernelStatus: JupyterFrontEndPlugin = { - id: '@jupyterlab-classic/application-extension:kernel-status', - autoStart: true, - requires: [IClassicShell], - activate: (app: JupyterFrontEnd, shell: IClassicShell) => { - const widget = new Widget(); - widget.addClass('jp-ClassicKernelStatus'); - app.shell.add(widget, 'menu', { rank: 10_010 }); - - const removeClasses = () => { - widget.removeClass(KERNEL_STATUS_ERROR_CLASS); - widget.removeClass(KERNEL_STATUS_WARN_CLASS); - widget.removeClass(KERNEL_STATUS_INFO_CLASS); - widget.removeClass(KERNEL_STATUS_FADE_OUT_CLASS); - }; - - const onStatusChanged = (sessionContext: ISessionContext) => { - const status = sessionContext.kernelDisplayStatus; - let text = `Kernel ${Text.titleCase(status)}`; - removeClasses(); - switch (status) { - case 'busy': - case 'idle': - text = ''; - widget.addClass(KERNEL_STATUS_FADE_OUT_CLASS); - break; - case 'dead': - case 'terminating': - widget.addClass(KERNEL_STATUS_ERROR_CLASS); - break; - case 'unknown': - widget.addClass(KERNEL_STATUS_WARN_CLASS); - break; - default: - widget.addClass(KERNEL_STATUS_INFO_CLASS); - widget.addClass(KERNEL_STATUS_FADE_OUT_CLASS); - break; - } - widget.node.textContent = text; - }; - - // TODO: this signal might not be needed if we assume there is always only - // one notebook in the main area - const onChange = async () => { - const current = shell.currentWidget; - if (!(current instanceof NotebookPanel)) { - return; - } - const sessionContext = current.sessionContext; - sessionContext.statusChanged.connect(onStatusChanged); - }; - - shell.currentChanged.connect(onChange); - } -}; - /** * A plugin to dispose the Tabs menu */ @@ -257,9 +65,14 @@ const logo: JupyterFrontEndPlugin = { id: '@jupyterlab-classic/application-extension:logo', autoStart: true, activate: (app: JupyterFrontEnd) => { - const logo = new Widget(); + const baseUrl = PageConfig.getBaseUrl(); + const node = document.createElement('a'); + node.href = `${baseUrl}classic/tree`; + node.target = '_blank'; + node.rel = 'noopener noreferrer'; + const logo = new Widget({ node }); jupyterIcon.element({ - container: logo.node, + container: node, elementPosition: 'center', padding: '2px 2px 2px 8px', height: '28px', @@ -270,17 +83,6 @@ const logo: JupyterFrontEndPlugin = { } }; -/** - * The main plugin. - */ -const main: JupyterFrontEndPlugin = { - id: '@jupyterlab-classic/application-extension:main', - autoStart: true, - activate: () => { - console.log(main.id, 'activated'); - } -}; - /** * The default paths for a JupyterLab Classic app. */ @@ -365,56 +167,6 @@ const spacer: JupyterFrontEndPlugin = { } }; -/** - * 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 - }); - } - } - }; - } - }); - } -}; - /** * Plugin to toggle the top header visibility. */ @@ -457,40 +209,6 @@ const translator: JupyterFrontEndPlugin = { provides: ITranslator }; -/** - * The default tree route resolver plugin. - */ -const tree: JupyterFrontEndPlugin = { - id: '@jupyterlab-classic/application-extension:tree-resolver', - autoStart: true, - requires: [IRouter], - activate: (app: JupyterFrontEnd, router: IRouter): 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(() => { - commands.execute('docmanager:open', { - path, - factory: NOTEBOOK_FACTORY - }); - }); - } - }); - - router.register({ command, pattern: treePattern }); - } -}; - /** * Zen mode plugin */ @@ -555,21 +273,15 @@ const zen: JupyterFrontEndPlugin = { * Export the plugins as default. */ const plugins: JupyterFrontEndPlugin[] = [ - checkpoints, - kernelLogo, - kernelStatus, logo, - main, noTabsMenu, paths, router, sessionDialogs, shell, spacer, - title, topVisibility, translator, - tree, zen ]; diff --git a/packages/application-extension/style/base.css b/packages/application-extension/style/base.css index fac71eb20..70f46ed68 100644 --- a/packages/application-extension/style/base.css +++ b/packages/application-extension/style/base.css @@ -8,77 +8,3 @@ flex-grow: 1; flex-shrink: 1; } - -.jp-ClassicKernelLogo { - flex: 0 0 auto; - display: flex; - align-items: center; - text-align: center; - margin-right: 8px; -} - -.jp-ClassicKernelLogo img { - max-width: 28px; - max-height: 28px; - display: flex; -} - -.jp-ClassicKernelStatus { - font-size: 12px; - margin: 0; - font-weight: normal; - color: var(--jp-ui-font-color0); - font-family: var(--jp-ui-font-family); - line-height: var(--jp-private-title-panel-height); - padding-left: 5px; - padding-right: 5px; -} - -.jp-ClassicKernelStatus-error { - background-color: var(--jp-error-color0); -} - -.jp-ClassicKernelStatus-warn { - background-color: var(--jp-warn-color0); -} - -.jp-ClassicKernelStatus-info { - background-color: var(--jp-info-color0); -} - -.jp-ClassicKernelStatus-fade { - animation: 0.5s fade-out forwards; -} - -@keyframes fade-out { - 0% { - opacity: 1; - } - 100% { - opacity: 0; - } -} - -#jp-title h1 { - cursor: pointer; - font-size: 18px; - margin: 0; - font-weight: normal; - color: var(--jp-ui-font-color0); - font-family: var(--jp-ui-font-family); - line-height: calc(1.5 * var(--jp-private-title-panel-height)); -} - -#jp-title h1:hover { - background: var(--jp-layout-color2); -} - -.jp-ClassicCheckpoint { - font-size: 14px; - margin-left: 5px; - margin-right: 5px; - font-weight: normal; - color: var(--jp-ui-font-color0); - font-family: var(--jp-ui-font-family); - line-height: calc(1.5 * var(--jp-private-title-panel-height)); -} diff --git a/packages/application/src/shell.ts b/packages/application/src/shell.ts index 6caa25dd8..24ded8f43 100644 --- a/packages/application/src/shell.ts +++ b/packages/application/src/shell.ts @@ -102,13 +102,17 @@ export class ClassicShell extends Widget implements JupyterFrontEnd.IShell { if (area === 'menu') { return this._menuHandler.addWidget(widget, rank); } - if (this._main.widgets.length > 0) { - // do not add the widget if there is already one - return; + if (area === 'main') { + if (this._main.widgets.length > 0) { + // do not add the widget if there is already one + // TODO: should the widget be closed? + widget.close(); + return; + } + this._main.addWidget(widget); + this._main.update(); + this._currentChanged.emit(void 0); } - this._main.addWidget(widget); - this._main.update(); - this._currentChanged.emit(void 0); } /** diff --git a/packages/docmanager-extension/package.json b/packages/docmanager-extension/package.json new file mode 100644 index 000000000..38fc3bd2a --- /dev/null +++ b/packages/docmanager-extension/package.json @@ -0,0 +1,56 @@ +{ + "name": "@jupyterlab-classic/docmanager-extension", + "version": "0.1.0", + "description": "JupyterLab Classic - Document Manager Extension", + "homepage": "https://github.com/jtpio/jupyterlab-classic", + "bugs": { + "url": "https://github.com/jtpio/jupyterlab-classic/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/jtpio/jupyterlab-classic.git" + }, + "license": "BSD-3-Clause", + "author": "Project Jupyter", + "sideEffects": [ + "style/**/*.css" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "style": "style/index.css", + "directories": { + "lib": "lib/" + }, + "files": [ + "lib/*.d.ts", + "lib/*.js.map", + "lib/*.js", + "schema/*.json", + "style/**/*.css" + ], + "scripts": { + "build": "tsc -b", + "clean": "rimraf lib && rimraf tsconfig.tsbuildinfo", + "docs": "typedoc src", + "prepublishOnly": "npm run build", + "watch": "tsc -b --watch" + }, + "dependencies": { + "@jupyterlab/application": "^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/services": "^6.0.0-rc.12", + "@lumino/algorithm": "^1.3.3" + }, + "devDependencies": { + "rimraf": "~3.0.0", + "typescript": "~4.0.2" + }, + "publishConfig": { + "access": "public" + }, + "jupyterlab": { + "extension": true + } +} diff --git a/packages/docmanager-extension/src/index.ts b/packages/docmanager-extension/src/index.ts new file mode 100644 index 000000000..8c93a0ff0 --- /dev/null +++ b/packages/docmanager-extension/src/index.ts @@ -0,0 +1,53 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; + +import { PageConfig } from '@jupyterlab/coreutils'; + +import { IDocumentManager } from '@jupyterlab/docmanager'; + +import { IDocumentWidget, DocumentRegistry } from '@jupyterlab/docregistry'; + +import { Kernel } from '@jupyterlab/services'; + +/** + * A plugin to open document in a new browser tab. + * + * TODO: remove and use a custom doc manager? + */ +const opener: JupyterFrontEndPlugin = { + id: '@jupyterlab-classic/docmanager-extension:opener', + requires: [IDocumentManager], + autoStart: true, + activate: (app: JupyterFrontEnd, docManager: IDocumentManager) => { + const baseUrl = PageConfig.getBaseUrl(); + + // patch the `docManager.open` option to prevent the default behavior + const docOpen = docManager.open; + docManager.open = ( + path: string, + widgetName = 'default', + kernel?: Partial, + options?: DocumentRegistry.IOpenOptions + ): IDocumentWidget | undefined => { + const ref = options?.ref; + if (ref === 'noref') { + docOpen.call(docManager, path, widgetName, kernel, options); + return; + } + window.open(`${baseUrl}classic/notebooks/${path}`); + return undefined; + }; + } +}; + +/** + * Export the plugins as default. + */ +const plugins: JupyterFrontEndPlugin[] = [opener]; + +export default plugins; diff --git a/packages/docmanager-extension/style/base.css b/packages/docmanager-extension/style/base.css new file mode 100644 index 000000000..e69de29bb diff --git a/packages/docmanager-extension/style/index.css b/packages/docmanager-extension/style/index.css new file mode 100644 index 000000000..f5246e666 --- /dev/null +++ b/packages/docmanager-extension/style/index.css @@ -0,0 +1 @@ +@import url('./base.css'); diff --git a/packages/docmanager-extension/tsconfig.json b/packages/docmanager-extension/tsconfig.json new file mode 100644 index 000000000..399b75b7a --- /dev/null +++ b/packages/docmanager-extension/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfigbase", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, + "include": ["src/**/*"] +} diff --git a/packages/filebrowser-extension/package.json b/packages/filebrowser-extension/package.json new file mode 100644 index 000000000..7f018fa7b --- /dev/null +++ b/packages/filebrowser-extension/package.json @@ -0,0 +1,67 @@ +{ + "name": "@jupyterlab-classic/filebrowser-extension", + "version": "0.1.0", + "description": "JupyterLab Classic - File browser Extension", + "homepage": "https://github.com/jtpio/jupyterlab-classic", + "bugs": { + "url": "https://github.com/jtpio/jupyterlab-classic/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/jtpio/jupyterlab-classic.git" + }, + "license": "BSD-3-Clause", + "author": "Project Jupyter", + "sideEffects": [ + "style/**/*.css" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "style": "style/index.css", + "directories": { + "lib": "lib/" + }, + "files": [ + "lib/*.d.ts", + "lib/*.js.map", + "lib/*.js", + "schema/*.json", + "style/**/*.css" + ], + "scripts": { + "build": "tsc -b", + "clean": "rimraf lib && rimraf tsconfig.tsbuildinfo", + "docs": "typedoc src", + "prepublishOnly": "npm run build", + "watch": "tsc -b --watch" + }, + "dependencies": { + "@jupyterlab-classic/application": "^0.1.0", + "@jupyterlab/application": "^3.0.0-rc.13", + "@jupyterlab/apputils": "^3.0.0-rc.13", + "@jupyterlab/coreutils": "^5.0.0-rc.13", + "@jupyterlab/docmanager": "^3.0.0-rc.13", + "@jupyterlab/filebrowser": "^3.0.0-rc.13", + "@jupyterlab/launcher": "^3.0.0-rc.13", + "@jupyterlab/mainmenu": "^3.0.0-rc.13", + "@jupyterlab/services": "^6.0.0-rc.13", + "@jupyterlab/settingregistry": "^3.0.0-rc.13", + "@jupyterlab/statedb": "^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/messaging": "^1.4.3", + "@lumino/widgets": "^1.16.1" + }, + "devDependencies": { + "rimraf": "~3.0.0", + "typescript": "~4.0.2" + }, + "publishConfig": { + "access": "public" + }, + "jupyterlab": { + "extension": true + } +} diff --git a/packages/filebrowser-extension/src/index.ts b/packages/filebrowser-extension/src/index.ts new file mode 100644 index 000000000..ac10b6c85 --- /dev/null +++ b/packages/filebrowser-extension/src/index.ts @@ -0,0 +1,1170 @@ +// Vendored from https://github.com/jupyterlab/jupyterlab/blob/b9511a52557d6ae56e597d0f48392993bdc432ae/packages/filebrowser-extension/src/index.ts +// Because of ILabShell being a required dependency in some of the plugins +// TODO: remove this package when ILabShell is optional upstream + +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { + ILabShell, + ILayoutRestorer, + ITreePathUpdater, + IRouter, + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; + +import { + Clipboard, + MainAreaWidget, + ToolbarButton, + WidgetTracker, + ICommandPalette, + InputDialog, + showErrorMessage, + DOMUtils +} from '@jupyterlab/apputils'; + +import { PageConfig, PathExt } from '@jupyterlab/coreutils'; + +import { IDocumentManager } from '@jupyterlab/docmanager'; + +import { + FilterFileBrowserModel, + FileBrowser, + IFileBrowserFactory +} from '@jupyterlab/filebrowser'; + +import { Launcher } from '@jupyterlab/launcher'; + +import { IMainMenu } from '@jupyterlab/mainmenu'; + +import { Contents } from '@jupyterlab/services'; + +import { ISettingRegistry } from '@jupyterlab/settingregistry'; + +import { IStateDB } from '@jupyterlab/statedb'; + +import { ITranslator } from '@jupyterlab/translation'; + +import { + addIcon, + closeIcon, + copyIcon, + cutIcon, + downloadIcon, + editIcon, + fileIcon, + folderIcon, + markdownIcon, + newFolderIcon, + pasteIcon, + stopIcon, + textEditorIcon +} from '@jupyterlab/ui-components'; + +import { IIterator, map, reduce, toArray, find } from '@lumino/algorithm'; + +import { CommandRegistry } from '@lumino/commands'; + +import { Message } from '@lumino/messaging'; + +import { Menu } from '@lumino/widgets'; + +/** + * The command IDs used by the file browser plugin. + */ +namespace CommandIDs { + export const copy = 'filebrowser:copy'; + + export const copyDownloadLink = 'filebrowser:copy-download-link'; + + // For main browser only. + export const createLauncher = 'filebrowser:create-main-launcher'; + + export const cut = 'filebrowser:cut'; + + export const del = 'filebrowser:delete'; + + export const download = 'filebrowser:download'; + + export const duplicate = 'filebrowser:duplicate'; + + // For main browser only. + export const hideBrowser = 'filebrowser:hide-main'; + + export const goToPath = 'filebrowser:go-to-path'; + + export const openPath = 'filebrowser:open-path'; + + export const open = 'filebrowser:open'; + + export const openBrowserTab = 'filebrowser:open-browser-tab'; + + export const paste = 'filebrowser:paste'; + + export const createNewDirectory = 'filebrowser:create-new-directory'; + + export const createNewFile = 'filebrowser:create-new-file'; + + export const createNewMarkdownFile = 'filebrowser:create-new-markdown-file'; + + export const rename = 'filebrowser:rename'; + + // For main browser only. + export const share = 'filebrowser:share-main'; + + // For main browser only. + export const copyPath = 'filebrowser:copy-path'; + + export const showBrowser = 'filebrowser:activate'; + + export const shutdown = 'filebrowser:shutdown'; + + // For main browser only. + export const toggleBrowser = 'filebrowser:toggle-main'; + + export const toggleNavigateToCurrentDirectory = + 'filebrowser:toggle-navigate-to-current-directory'; + + export const toggleLastModified = 'filebrowser:toggle-last-modified'; + + export const search = 'filebrowser:search'; +} + +/** + * The default file browser extension. + */ +const browser: JupyterFrontEndPlugin = { + activate: activateBrowser, + id: '@jupyterlab-classic/filebrowser-extension:browser', + requires: [ + IFileBrowserFactory, + IDocumentManager, + ISettingRegistry, + ITranslator + ], + optional: [ + ILabShell, + ICommandPalette, + IMainMenu, + ILayoutRestorer, + ITreePathUpdater + ], + autoStart: true +}; + +/** + * The default file browser factory provider. + */ +const factory: JupyterFrontEndPlugin = { + activate: activateFactory, + id: '@jupyterlab-classic/filebrowser-extension:factory', + provides: IFileBrowserFactory, + requires: [IDocumentManager, ITranslator], + optional: [ILabShell, IStateDB, IRouter, JupyterFrontEnd.ITreeResolver] +}; + +/** + * The file browser namespace token. + */ +const namespace = 'filebrowser'; + +/** + * Export the plugins as default. + */ +const plugins: JupyterFrontEndPlugin[] = [factory, browser]; +export default plugins; + +/** + * Activate the file browser factory provider. + */ +async function activateFactory( + app: JupyterFrontEnd, + docManager: IDocumentManager, + translator: ITranslator, + labShell: ILabShell | null, + state: IStateDB | null, + router: IRouter | null, + tree: JupyterFrontEnd.ITreeResolver | null +): Promise { + const trans = translator.load('jupyterlab'); + const { commands } = app; + const tracker = new WidgetTracker({ namespace }); + const createFileBrowser = ( + id: string, + options: IFileBrowserFactory.IOptions = {} + ) => { + const model = new FilterFileBrowserModel({ + translator: translator, + auto: options.auto ?? true, + manager: docManager, + driveName: options.driveName || '', + refreshInterval: options.refreshInterval, + state: + options.state === null ? undefined : options.state || state || undefined + }); + const restore = options.restore; + const widget = new FileBrowser({ id, model, restore, translator }); + + // Add a launcher toolbar item. + if (labShell) { + const launcher = new ToolbarButton({ + icon: addIcon, + onClick: () => { + if ( + labShell.mode === 'multiple-document' && + commands.hasCommand('launcher:create') + ) { + return Private.createLauncher(commands, widget); + } else { + const newUrl = PageConfig.getUrl({ + mode: labShell.mode, + workspace: PageConfig.defaultWorkspace, + treePath: model.path + }); + window.open(newUrl, '_blank'); + } + }, + tooltip: trans.__('New Launcher'), + actualOnClick: true + }); + widget.toolbar.insertItem(0, 'launch', launcher); + } + + // Track the newly created file browser. + void tracker.add(widget); + + return widget; + }; + + // Manually restore and load the default file browser. + const defaultBrowser = createFileBrowser('filebrowser', { + auto: false, + restore: false + }); + void Private.restoreBrowser(defaultBrowser, commands, router, tree); + + return { createFileBrowser, defaultBrowser, tracker }; +} + +/** + * Activate the default file browser in the sidebar. + */ +function activateBrowser( + app: JupyterFrontEnd, + factory: IFileBrowserFactory, + docManager: IDocumentManager, + settingRegistry: ISettingRegistry, + translator: ITranslator, + labShell: ILabShell | null, + commandPalette: ICommandPalette | null, + mainMenu: IMainMenu | null, + restorer: ILayoutRestorer | null, + treePathUpdater: ITreePathUpdater | null +): void { + const trans = translator.load('jupyterlab'); + const browser = factory.defaultBrowser; + const { commands } = app; + + // Let the application restorer track the primary file browser (that is + // automatically created) for restoration of application state (e.g. setting + // the file browser as the current side bar widget). + // + // All other file browsers created by using the factory function are + // responsible for their own restoration behavior, if any. + if (restorer) { + restorer.add(browser, namespace); + } + + addCommands( + app, + factory, + settingRegistry, + translator, + labShell, + commandPalette, + mainMenu + ); + + browser.title.icon = folderIcon; + // Show the current file browser shortcut in its title. + const updateBrowserTitle = () => { + const binding = find( + app.commands.keyBindings, + b => b.command === CommandIDs.toggleBrowser + ); + if (binding) { + const ks = CommandRegistry.formatKeystroke(binding.keys.join(' ')); + browser.title.caption = trans.__('File Browser (%1)', ks); + } else { + browser.title.caption = trans.__('File Browser'); + } + }; + updateBrowserTitle(); + app.commands.keyBindingChanged.connect(() => { + updateBrowserTitle(); + }); + + app.shell.add(browser, 'main', { rank: 100 }); + + // If the layout is a fresh session without saved data and not in single document + // mode, open file browser. + if (labShell) { + void labShell.restored.then(layout => { + if (layout.fresh && labShell.mode !== 'single-document') { + void commands.execute(CommandIDs.showBrowser, void 0); + } + }); + } + + void Promise.all([app.restored, browser.model.restored]).then(() => { + function maybeCreate() { + // Create a launcher if there are no open items. + if ( + toArray(app.shell.widgets('main')).length === 0 && + commands.hasCommand('launcher:create') + ) { + void Private.createLauncher(commands, browser); + } + } + + // When layout is modified, create a launcher if there are no open items. + if (labShell) { + labShell.layoutModified.connect(() => { + maybeCreate(); + }); + } + + let navigateToCurrentDirectory = false; + let useFuzzyFilter = true; + + void settingRegistry + .load('@jupyterlab/filebrowser-extension:browser') + .then(settings => { + settings.changed.connect(settings => { + navigateToCurrentDirectory = settings.get( + 'navigateToCurrentDirectory' + ).composite as boolean; + browser.navigateToCurrentDirectory = navigateToCurrentDirectory; + }); + navigateToCurrentDirectory = settings.get('navigateToCurrentDirectory') + .composite as boolean; + browser.navigateToCurrentDirectory = navigateToCurrentDirectory; + settings.changed.connect(settings => { + useFuzzyFilter = settings.get('useFuzzyFilter').composite as boolean; + browser.useFuzzyFilter = useFuzzyFilter; + }); + useFuzzyFilter = settings.get('useFuzzyFilter').composite as boolean; + browser.useFuzzyFilter = useFuzzyFilter; + }); + + // Whether to automatically navigate to a document's current directory + if (labShell) { + labShell.currentChanged.connect(async (_, change) => { + if (navigateToCurrentDirectory && change.newValue) { + const { newValue } = change; + const context = docManager.contextForWidget(newValue); + if (context) { + const { path } = context; + try { + await Private.navigateToPath(path, factory, translator); + labShell.currentWidget?.activate(); + } catch (reason) { + console.warn( + `${CommandIDs.goToPath} failed to open: ${path}`, + reason + ); + } + } + } + }); + } + + if (treePathUpdater) { + browser.model.pathChanged.connect((sender, args) => { + treePathUpdater(args.newValue); + }); + } + + maybeCreate(); + }); +} + +/** + * Add the main file browser commands to the application's command registry. + */ +function addCommands( + app: JupyterFrontEnd, + factory: IFileBrowserFactory, + settingRegistry: ISettingRegistry, + translator: ITranslator, + labShell: ILabShell | null, + commandPalette: ICommandPalette | null, + mainMenu: IMainMenu | null +): void { + const trans = translator.load('jupyterlab'); + const { docRegistry: registry, commands } = app; + const { defaultBrowser: browser, tracker } = factory; + + commands.addCommand(CommandIDs.del, { + execute: () => { + const widget = tracker.currentWidget; + + if (widget) { + return widget.delete(); + } + }, + icon: closeIcon.bindprops({ stylesheet: 'menuItem' }), + label: trans.__('Delete'), + mnemonic: 0 + }); + + commands.addCommand(CommandIDs.copy, { + execute: () => { + const widget = tracker.currentWidget; + + if (widget) { + return widget.copy(); + } + }, + icon: copyIcon.bindprops({ stylesheet: 'menuItem' }), + label: trans.__('Copy'), + mnemonic: 0 + }); + + commands.addCommand(CommandIDs.cut, { + execute: () => { + const widget = tracker.currentWidget; + + if (widget) { + return widget.cut(); + } + }, + icon: cutIcon.bindprops({ stylesheet: 'menuItem' }), + label: trans.__('Cut') + }); + + commands.addCommand(CommandIDs.download, { + execute: () => { + const widget = tracker.currentWidget; + + if (widget) { + return widget.download(); + } + }, + icon: downloadIcon.bindprops({ stylesheet: 'menuItem' }), + label: trans.__('Download') + }); + + commands.addCommand(CommandIDs.duplicate, { + execute: () => { + const widget = tracker.currentWidget; + + if (widget) { + return widget.duplicate(); + } + }, + icon: copyIcon.bindprops({ stylesheet: 'menuItem' }), + label: trans.__('Duplicate') + }); + + if (labShell) { + commands.addCommand(CommandIDs.hideBrowser, { + execute: () => { + const widget = tracker.currentWidget; + if (widget && !widget.isHidden) { + labShell.collapseLeft(); + } + } + }); + } + + commands.addCommand(CommandIDs.goToPath, { + execute: async args => { + const path = (args.path as string) || ''; + const showBrowser = !(args?.dontShowBrowser ?? false); + try { + const item = await Private.navigateToPath(path, factory, translator); + if (item.type !== 'directory' && showBrowser) { + const browserForPath = Private.getBrowserForPath(path, factory); + if (browserForPath) { + browserForPath.clearSelectedItems(); + const parts = path.split('/'); + const name = parts[parts.length - 1]; + if (name) { + await browserForPath.selectItemByName(name); + } + } + } + } catch (reason) { + console.warn(`${CommandIDs.goToPath} failed to go to: ${path}`, reason); + } + if (showBrowser) { + return commands.execute(CommandIDs.showBrowser, { path }); + } + } + }); + + commands.addCommand(CommandIDs.openPath, { + label: args => + args.path ? trans.__('Open %1', args.path) : trans.__('Open from Path…'), + caption: args => + args.path ? trans.__('Open %1', args.path) : trans.__('Open from path'), + execute: async args => { + let path: string | undefined; + if (args?.path) { + path = args.path as string; + } else { + path = + ( + await InputDialog.getText({ + label: trans.__('Path'), + placeholder: '/path/relative/to/jlab/root', + title: trans.__('Open Path'), + okLabel: trans.__('Open') + }) + ).value ?? undefined; + } + if (!path) { + return; + } + try { + const trailingSlash = path !== '/' && path.endsWith('/'); + if (trailingSlash) { + // The normal contents service errors on paths ending in slash + path = path.slice(0, path.length - 1); + } + const browserForPath = Private.getBrowserForPath(path, factory)!; + const { services } = browserForPath.model.manager; + const item = await services.contents.get(path, { + content: false + }); + if (trailingSlash && item.type !== 'directory') { + throw new Error(`Path ${path}/ is not a directory`); + } + await commands.execute(CommandIDs.goToPath, { + path, + dontShowBrowser: args.dontShowBrowser + }); + if (item.type === 'directory') { + return; + } + return commands.execute('docmanager:open', { path }); + } catch (reason) { + if (reason.response && reason.response.status === 404) { + reason.message = trans.__('Could not find path: %1', path); + } + return showErrorMessage(trans.__('Cannot open'), reason); + } + } + }); + // Add the openPath command to the command palette + if (commandPalette) { + commandPalette.addItem({ + command: CommandIDs.openPath, + category: trans.__('File Operations') + }); + } + + commands.addCommand(CommandIDs.open, { + execute: args => { + const factory = (args['factory'] as string) || void 0; + const widget = tracker.currentWidget; + + if (!widget) { + return; + } + + const { contents } = widget.model.manager.services; + return Promise.all( + toArray( + map(widget.selectedItems(), item => { + if (item.type === 'directory') { + const localPath = contents.localPath(item.path); + return widget.model.cd(`/${localPath}`); + } + + return commands.execute('docmanager:open', { + factory: factory, + path: item.path + }); + }) + ) + ); + }, + icon: args => { + const factory = (args['factory'] as string) || void 0; + if (factory) { + // if an explicit factory is passed... + const ft = registry.getFileType(factory); + // ...set an icon if the factory name corresponds to a file type name... + // ...or leave the icon blank + return ft?.icon?.bindprops({ stylesheet: 'menuItem' }); + } else { + return folderIcon.bindprops({ stylesheet: 'menuItem' }); + } + }, + // FIXME-TRANS: Is this localizable? + label: args => + (args['label'] || args['factory'] || trans.__('Open')) as string, + mnemonic: 0 + }); + + commands.addCommand(CommandIDs.openBrowserTab, { + execute: () => { + const widget = tracker.currentWidget; + + if (!widget) { + return; + } + + return Promise.all( + toArray( + map(widget.selectedItems(), item => { + return commands.execute('docmanager:open-browser-tab', { + path: item.path + }); + }) + ) + ); + }, + icon: addIcon.bindprops({ stylesheet: 'menuItem' }), + label: trans.__('Open in New Browser Tab'), + mnemonic: 0 + }); + + commands.addCommand(CommandIDs.copyDownloadLink, { + execute: () => { + const widget = tracker.currentWidget; + if (!widget) { + return; + } + + return widget.model.manager.services.contents + .getDownloadUrl(widget.selectedItems().next()!.path) + .then(url => { + Clipboard.copyToSystem(url); + }); + }, + icon: copyIcon.bindprops({ stylesheet: 'menuItem' }), + label: trans.__('Copy Download Link'), + mnemonic: 0 + }); + + commands.addCommand(CommandIDs.paste, { + execute: () => { + const widget = tracker.currentWidget; + + if (widget) { + return widget.paste(); + } + }, + icon: pasteIcon.bindprops({ stylesheet: 'menuItem' }), + label: trans.__('Paste'), + mnemonic: 0 + }); + + commands.addCommand(CommandIDs.createNewDirectory, { + execute: () => { + const widget = tracker.currentWidget; + + if (widget) { + return widget.createNewDirectory(); + } + }, + icon: newFolderIcon.bindprops({ stylesheet: 'menuItem' }), + label: trans.__('New Folder') + }); + + commands.addCommand(CommandIDs.createNewFile, { + execute: () => { + const { + model: { path } + } = browser; + void commands.execute('docmanager:new-untitled', { + path, + type: 'file', + ext: 'txt' + }); + }, + icon: textEditorIcon.bindprops({ stylesheet: 'menuItem' }), + label: trans.__('New File') + }); + + commands.addCommand(CommandIDs.createNewMarkdownFile, { + execute: () => { + const { + model: { path } + } = browser; + void commands.execute('docmanager:new-untitled', { + path, + type: 'file', + ext: 'md' + }); + }, + icon: markdownIcon.bindprops({ stylesheet: 'menuItem' }), + label: trans.__('New Markdown File') + }); + + commands.addCommand(CommandIDs.rename, { + execute: args => { + const widget = tracker.currentWidget; + + if (widget) { + return widget.rename(); + } + }, + icon: editIcon.bindprops({ stylesheet: 'menuItem' }), + label: trans.__('Rename'), + mnemonic: 0 + }); + + commands.addCommand(CommandIDs.copyPath, { + execute: () => { + const widget = tracker.currentWidget; + if (!widget) { + return; + } + const item = widget.selectedItems().next(); + if (!item) { + return; + } + + Clipboard.copyToSystem(item.path); + }, + isVisible: () => + !!tracker.currentWidget && + tracker.currentWidget.selectedItems().next !== undefined, + icon: fileIcon.bindprops({ stylesheet: 'menuItem' }), + label: trans.__('Copy Path') + }); + + commands.addCommand(CommandIDs.showBrowser, { + execute: args => { + const path = (args.path as string) || ''; + const browserForPath = Private.getBrowserForPath(path, factory); + + // Check for browser not found + if (!browserForPath) { + return; + } + // Shortcut if we are using the main file browser + if (browser === browserForPath) { + app.shell.activateById(browser.id); + return; + } else { + const areas: ILabShell.Area[] = ['left', 'right']; + for (const area of areas) { + const it = app.shell.widgets(area); + let widget = it.next(); + while (widget) { + if (widget.contains(browserForPath)) { + app.shell.activateById(widget.id); + return; + } + widget = it.next(); + } + } + } + } + }); + + commands.addCommand(CommandIDs.shutdown, { + execute: () => { + const widget = tracker.currentWidget; + + if (widget) { + return widget.shutdownKernels(); + } + }, + icon: stopIcon.bindprops({ stylesheet: 'menuItem' }), + label: trans.__('Shut Down Kernel') + }); + + commands.addCommand(CommandIDs.toggleBrowser, { + execute: () => { + if (browser.isHidden) { + return commands.execute(CommandIDs.showBrowser, void 0); + } + + return commands.execute(CommandIDs.hideBrowser, void 0); + } + }); + + commands.addCommand(CommandIDs.createLauncher, { + label: trans.__('New Launcher'), + execute: () => Private.createLauncher(commands, browser) + }); + + commands.addCommand(CommandIDs.toggleNavigateToCurrentDirectory, { + label: trans.__('Show Active File in File Browser'), + isToggled: () => browser.navigateToCurrentDirectory, + execute: () => { + const value = !browser.navigateToCurrentDirectory; + const key = 'navigateToCurrentDirectory'; + return settingRegistry + .set('@jupyterlab/filebrowser-extension:browser', key, value) + .catch((reason: Error) => { + console.error('Failed to set navigateToCurrentDirectory setting'); + }); + } + }); + + commands.addCommand(CommandIDs.toggleLastModified, { + label: trans.__('Toggle Last Modified Column'), + execute: () => { + const header = DOMUtils.findElement(document.body, 'jp-id-modified'); + const column = DOMUtils.findElements( + document.body, + 'jp-DirListing-itemModified' + ); + if (header.classList.contains('jp-LastModified-hidden')) { + header.classList.remove('jp-LastModified-hidden'); + for (let i = 0; i < column.length; i++) { + column[i].classList.remove('jp-LastModified-hidden'); + } + } else { + header.classList.add('jp-LastModified-hidden'); + for (let i = 0; i < column.length; i++) { + column[i].classList.add('jp-LastModified-hidden'); + } + } + } + }); + + commands.addCommand(CommandIDs.search, { + label: trans.__('Search on File Names'), + execute: () => alert('search') + }); + + if (mainMenu) { + mainMenu.settingsMenu.addGroup( + [{ command: CommandIDs.toggleNavigateToCurrentDirectory }], + 5 + ); + } + + if (commandPalette) { + commandPalette.addItem({ + command: CommandIDs.toggleNavigateToCurrentDirectory, + category: trans.__('File Operations') + }); + } + + /** + * A menu widget that dynamically populates with different widget factories + * based on current filebrowser selection. + */ + class OpenWithMenu extends Menu { + protected onBeforeAttach(msg: Message): void { + // clear the current menu items + this.clearItems(); + + // get the widget factories that could be used to open all of the items + // in the current filebrowser selection + const factories = tracker.currentWidget + ? OpenWithMenu._intersection( + map(tracker.currentWidget.selectedItems(), i => { + return OpenWithMenu._getFactories(i); + }) + ) + : undefined; + + if (factories) { + // make new menu items from the widget factories + factories.forEach(factory => { + this.addItem({ + args: { factory: factory }, + command: CommandIDs.open + }); + }); + } + + super.onBeforeAttach(msg); + } + + static _getFactories(item: Contents.IModel): Array { + const factories = registry + .preferredWidgetFactories(item.path) + .map(f => f.name); + const notebookFactory = registry.getWidgetFactory('notebook')?.name; + if ( + notebookFactory && + item.type === 'notebook' && + factories.indexOf(notebookFactory) === -1 + ) { + factories.unshift(notebookFactory); + } + + return factories; + } + + static _intersection(iter: IIterator>): Set | void { + // pop the first element of iter + const first = iter.next(); + // first will be undefined if iter is empty + if (!first) { + return; + } + + // "initialize" the intersection from first + const isect = new Set(first); + // reduce over the remaining elements of iter + return reduce( + iter, + (isect, subarr) => { + // filter out all elements not present in both isect and subarr, + // accumulate result in new set + return new Set(subarr.filter(x => isect.has(x))); + }, + isect + ); + } + } + + // matches anywhere on filebrowser + const selectorContent = '.jp-DirListing-content'; + // matches all filebrowser items + const selectorItem = '.jp-DirListing-item[data-isdir]'; + // matches only non-directory items + const selectorNotDir = '.jp-DirListing-item[data-isdir="false"]'; + + // If the user did not click on any file, we still want to show paste and new folder, + // so target the content rather than an item. + app.contextMenu.addItem({ + command: CommandIDs.createNewDirectory, + selector: selectorContent, + rank: 1 + }); + + app.contextMenu.addItem({ + command: CommandIDs.createNewFile, + selector: selectorContent, + rank: 2 + }); + + app.contextMenu.addItem({ + command: CommandIDs.createNewMarkdownFile, + selector: selectorContent, + rank: 3 + }); + + app.contextMenu.addItem({ + command: CommandIDs.paste, + selector: selectorContent, + rank: 4 + }); + + app.contextMenu.addItem({ + command: CommandIDs.open, + selector: selectorItem, + rank: 1 + }); + + const openWith = new OpenWithMenu({ commands }); + openWith.title.label = trans.__('Open With'); + app.contextMenu.addItem({ + type: 'submenu', + submenu: openWith, + selector: selectorNotDir, + rank: 2 + }); + + app.contextMenu.addItem({ + command: CommandIDs.openBrowserTab, + selector: selectorNotDir, + rank: 3 + }); + + app.contextMenu.addItem({ + command: CommandIDs.rename, + selector: selectorItem, + rank: 4 + }); + app.contextMenu.addItem({ + command: CommandIDs.del, + selector: selectorItem, + rank: 5 + }); + app.contextMenu.addItem({ + command: CommandIDs.cut, + selector: selectorItem, + rank: 6 + }); + + app.contextMenu.addItem({ + command: CommandIDs.copy, + selector: selectorNotDir, + rank: 7 + }); + + app.contextMenu.addItem({ + command: CommandIDs.duplicate, + selector: selectorNotDir, + rank: 8 + }); + app.contextMenu.addItem({ + command: CommandIDs.download, + selector: selectorNotDir, + rank: 9 + }); + app.contextMenu.addItem({ + command: CommandIDs.shutdown, + selector: selectorNotDir, + rank: 10 + }); + + app.contextMenu.addItem({ + command: CommandIDs.share, + selector: selectorItem, + rank: 11 + }); + app.contextMenu.addItem({ + command: CommandIDs.copyPath, + selector: selectorItem, + rank: 12 + }); + app.contextMenu.addItem({ + command: CommandIDs.copyDownloadLink, + selector: selectorNotDir, + rank: 13 + }); + app.contextMenu.addItem({ + command: CommandIDs.toggleLastModified, + selector: '.jp-DirListing-header', + rank: 14 + }); +} + +/** + * A namespace for private module data. + */ +namespace Private { + /** + * Create a launcher for a given filebrowser widget. + */ + export function createLauncher( + commands: CommandRegistry, + browser: FileBrowser + ): Promise> { + const { model } = browser; + + return commands + .execute('launcher:create', { cwd: model.path }) + .then((launcher: MainAreaWidget) => { + model.pathChanged.connect(() => { + if (launcher.content) { + launcher.content.cwd = model.path; + } + }, launcher); + return launcher; + }); + } + + /** + * Get browser object given file path. + */ + export function getBrowserForPath( + path: string, + factory: IFileBrowserFactory + ): FileBrowser | undefined { + const { defaultBrowser: browser, tracker } = factory; + const driveName = browser.model.manager.services.contents.driveName(path); + + if (driveName) { + const browserForPath = tracker.find( + _path => _path.model.driveName === driveName + ); + + if (!browserForPath) { + // warn that no filebrowser could be found for this driveName + console.warn( + `${CommandIDs.goToPath} failed to find filebrowser for path: ${path}` + ); + return; + } + + return browserForPath; + } + + // if driveName is empty, assume the main filebrowser + return browser; + } + + /** + * Navigate to a path or the path containing a file. + */ + export async function navigateToPath( + path: string, + factory: IFileBrowserFactory, + translator: ITranslator + ): Promise { + const trans = translator.load('jupyterlab'); + const browserForPath = Private.getBrowserForPath(path, factory); + if (!browserForPath) { + throw new Error(trans.__('No browser for path')); + } + const { services } = browserForPath.model.manager; + const localPath = services.contents.localPath(path); + + await services.ready; + const item = await services.contents.get(path, { content: false }); + const { model } = browserForPath; + await model.restored; + if (item.type === 'directory') { + await model.cd(`/${localPath}`); + } else { + await model.cd(`/${PathExt.dirname(localPath)}`); + } + return item; + } + + /** + * Restores file browser state and overrides state if tree resolver resolves. + */ + export async function restoreBrowser( + browser: FileBrowser, + commands: CommandRegistry, + router: IRouter | null, + tree: JupyterFrontEnd.ITreeResolver | null + ): Promise { + const restoring = 'jp-mod-restoring'; + + browser.addClass(restoring); + + if (!router) { + await browser.model.restore(browser.id); + await browser.model.refresh(); + browser.removeClass(restoring); + return; + } + + const listener = async () => { + router.routed.disconnect(listener); + + const paths = await tree?.paths; + if (paths?.file || paths?.browser) { + // Restore the model without populating it. + await browser.model.restore(browser.id, false); + if (paths.file) { + await commands.execute(CommandIDs.openPath, { + path: paths.file, + dontShowBrowser: true + }); + } + if (paths.browser) { + await commands.execute(CommandIDs.openPath, { + path: paths.browser, + dontShowBrowser: true + }); + } + } else { + await browser.model.restore(browser.id); + await browser.model.refresh(); + } + browser.removeClass(restoring); + }; + router.routed.connect(listener); + } +} diff --git a/packages/filebrowser-extension/style/base.css b/packages/filebrowser-extension/style/base.css new file mode 100644 index 000000000..c0b120aa8 --- /dev/null +++ b/packages/filebrowser-extension/style/base.css @@ -0,0 +1,3 @@ +.jp-FileBrowser { + height: 100%; +} diff --git a/packages/filebrowser-extension/style/index.css b/packages/filebrowser-extension/style/index.css new file mode 100644 index 000000000..c6a335ca5 --- /dev/null +++ b/packages/filebrowser-extension/style/index.css @@ -0,0 +1,3 @@ +@import url('~@jupyterlab/filebrowser/style/index.css'); + +@import url('./base.css'); diff --git a/packages/filebrowser-extension/tsconfig.json b/packages/filebrowser-extension/tsconfig.json new file mode 100644 index 000000000..b223e1a1b --- /dev/null +++ b/packages/filebrowser-extension/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfigbase", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, + "include": ["src/**/*"], + "references": [ + { + "path": "../application" + } + ] +} diff --git a/packages/notebook-extension/package.json b/packages/notebook-extension/package.json new file mode 100644 index 000000000..c649fd008 --- /dev/null +++ b/packages/notebook-extension/package.json @@ -0,0 +1,56 @@ +{ + "name": "@jupyterlab-classic/notebook-extension", + "version": "0.1.0", + "description": "JupyterLab Classic - Notebook Extension", + "homepage": "https://github.com/jtpio/jupyterlab-classic", + "bugs": { + "url": "https://github.com/jtpio/jupyterlab-classic/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/jtpio/jupyterlab-classic.git" + }, + "license": "BSD-3-Clause", + "author": "Project Jupyter", + "sideEffects": [ + "style/**/*.css" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "style": "style/index.css", + "directories": { + "lib": "lib/" + }, + "files": [ + "lib/*.d.ts", + "lib/*.js.map", + "lib/*.js", + "schema/*.json", + "style/**/*.css" + ], + "scripts": { + "build": "tsc -b", + "clean": "rimraf lib && rimraf tsconfig.tsbuildinfo", + "docs": "typedoc src", + "prepublishOnly": "npm run build", + "watch": "tsc -b --watch" + }, + "dependencies": { + "@jupyterlab-classic/application": "^0.1.0", + "@jupyterlab/application": "^3.0.0-rc.12", + "@jupyterlab/apputils": "^3.0.0-rc.12", + "@jupyterlab/docmanager": "^3.0.0-rc.12", + "@jupyterlab/notebook": "^3.0.0-rc.12", + "@lumino/widgets": "^1.14.0" + }, + "devDependencies": { + "rimraf": "~3.0.0", + "typescript": "~4.0.2" + }, + "publishConfig": { + "access": "public" + }, + "jupyterlab": { + "extension": true + } +} diff --git a/packages/notebook-extension/src/index.ts b/packages/notebook-extension/src/index.ts new file mode 100644 index 000000000..38074f2a4 --- /dev/null +++ b/packages/notebook-extension/src/index.ts @@ -0,0 +1,339 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { + IRouter, + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; + +import { ISessionContext, DOMUtils } from '@jupyterlab/apputils'; + +import { PageConfig, Text, Time } from '@jupyterlab/coreutils'; + +import { IDocumentManager, renameDialog } from '@jupyterlab/docmanager'; + +import { NotebookPanel } from '@jupyterlab/notebook'; + +import { + App, + ClassicShell, + IClassicShell +} from '@jupyterlab-classic/application'; + +import { Widget } from '@lumino/widgets'; + +/** + * The default notebook factory. + */ +const NOTEBOOK_FACTORY = 'Notebook'; + +/** + * The class for kernel status errors. + */ +const KERNEL_STATUS_ERROR_CLASS = 'jp-ClassicKernelStatus-error'; + +/** + * The class for kernel status warnings. + */ +const KERNEL_STATUS_WARN_CLASS = 'jp-ClassicKernelStatus-warn'; + +/** + * The class for kernel status infos. + */ +const KERNEL_STATUS_INFO_CLASS = 'jp-ClassicKernelStatus-info'; + +/** + * The class to fade out the kernel status. + */ +const KERNEL_STATUS_FADE_OUT_CLASS = 'jp-ClassicKernelStatus-fade'; + +/** + * A plugin for the checkpoint indicator + */ +const checkpoints: JupyterFrontEndPlugin = { + id: '@jupyterlab-classic/application-extension:checkpoints', + autoStart: true, + requires: [IDocumentManager], + optional: [IClassicShell], + activate: ( + app: JupyterFrontEnd, + docManager: IDocumentManager, + classicShell: IClassicShell + ) => { + const { shell } = app; + const widget = new Widget(); + widget.id = DOMUtils.createDomID(); + widget.addClass('jp-ClassicCheckpoint'); + app.shell.add(widget, 'top', { rank: 100 }); + + const onChange = async () => { + const current = shell.currentWidget; + if (!current) { + return; + } + const context = docManager.contextForWidget(current); + + context?.fileChanged.disconnect(onChange); + context?.fileChanged.connect(onChange); + + const checkpoints = await context?.listCheckpoints(); + if (!checkpoints) { + return; + } + const checkpoint = checkpoints[checkpoints.length - 1]; + widget.node.textContent = `Last Checkpoint: ${Time.formatHuman( + new Date(checkpoint.last_modified) + )}`; + }; + + if (classicShell) { + classicShell.currentChanged.connect(onChange); + } + // TODO: replace by a Poll + onChange(); + setInterval(onChange, 2000); + } +}; + +/** + * The kernel logo plugin. + */ +const kernelLogo: JupyterFrontEndPlugin = { + id: '@jupyterlab-classic/application-extension:kernel-logo', + autoStart: true, + requires: [IClassicShell], + activate: (app: JupyterFrontEnd, shell: IClassicShell) => { + const { serviceManager } = app; + const baseUrl = PageConfig.getBaseUrl(); + + let widget: Widget; + // TODO: this signal might not be needed if we assume there is always only + // one notebook in the main area + const onChange = async () => { + if (widget) { + widget.dispose(); + widget.parent = null; + } + const current = shell.currentWidget; + if (!(current instanceof NotebookPanel)) { + return; + } + + await current.sessionContext.ready; + current.sessionContext.kernelChanged.disconnect(onChange); + current.sessionContext.kernelChanged.connect(onChange); + + const name = current.sessionContext.session?.kernel?.name ?? ''; + const spec = serviceManager.kernelspecs?.specs?.kernelspecs[name]; + if (!spec) { + return; + } + + let kernelIconUrl = spec.resources['logo-64x64']; + if (!kernelIconUrl) { + return; + } + + const index = kernelIconUrl.indexOf('kernelspecs'); + kernelIconUrl = baseUrl + kernelIconUrl.slice(index); + const node = document.createElement('div'); + const img = document.createElement('img'); + img.src = kernelIconUrl; + img.title = spec.display_name; + node.appendChild(img); + widget = new Widget({ node }); + widget.addClass('jp-ClassicKernelLogo'); + app.shell.add(widget, 'top', { rank: 10_010 }); + }; + + shell.currentChanged.connect(onChange); + } +}; + +/** + * A plugin to display the kernel status; + */ +const kernelStatus: JupyterFrontEndPlugin = { + id: '@jupyterlab-classic/application-extension:kernel-status', + autoStart: true, + requires: [IClassicShell], + activate: (app: JupyterFrontEnd, shell: IClassicShell) => { + const widget = new Widget(); + widget.addClass('jp-ClassicKernelStatus'); + app.shell.add(widget, 'menu', { rank: 10_010 }); + + const removeClasses = () => { + widget.removeClass(KERNEL_STATUS_ERROR_CLASS); + widget.removeClass(KERNEL_STATUS_WARN_CLASS); + widget.removeClass(KERNEL_STATUS_INFO_CLASS); + widget.removeClass(KERNEL_STATUS_FADE_OUT_CLASS); + }; + + const onStatusChanged = (sessionContext: ISessionContext) => { + const status = sessionContext.kernelDisplayStatus; + let text = `Kernel ${Text.titleCase(status)}`; + removeClasses(); + switch (status) { + case 'busy': + case 'idle': + text = ''; + widget.addClass(KERNEL_STATUS_FADE_OUT_CLASS); + break; + case 'dead': + case 'terminating': + widget.addClass(KERNEL_STATUS_ERROR_CLASS); + break; + case 'unknown': + widget.addClass(KERNEL_STATUS_WARN_CLASS); + break; + default: + widget.addClass(KERNEL_STATUS_INFO_CLASS); + widget.addClass(KERNEL_STATUS_FADE_OUT_CLASS); + break; + } + widget.node.textContent = text; + }; + + // TODO: this signal might not be needed if we assume there is always only + // one notebook in the main area + const onChange = async () => { + const current = shell.currentWidget; + if (!(current instanceof NotebookPanel)) { + return; + } + const sessionContext = current.sessionContext; + sessionContext.statusChanged.connect(onStatusChanged); + }; + + shell.currentChanged.connect(onChange); + } +}; + +/** + * The default paths for a JupyterLab Classic app. + */ +const paths: JupyterFrontEndPlugin = { + id: '@jupyterlab-classic/application-extension:paths', + activate: (app: JupyterFrontEnd): JupyterFrontEnd.IPaths => { + if (!(app instanceof App)) { + throw new Error(`${paths.id} must be activated in JupyterLab Classic.`); + } + return app.paths; + }, + autoStart: true, + provides: JupyterFrontEnd.IPaths +}; + +/** + * The default JupyterLab Classic application shell. + */ +const shell: JupyterFrontEndPlugin = { + id: '@jupyterlab-classic/application-extension:shell', + activate: (app: JupyterFrontEnd) => { + if (!(app.shell instanceof ClassicShell)) { + throw new Error(`${shell.id} did not find a ClassicShell instance.`); + } + return app.shell; + }, + autoStart: true, + 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 +]; + +export default plugins; diff --git a/packages/notebook-extension/style/base.css b/packages/notebook-extension/style/base.css new file mode 100644 index 000000000..80215fcd2 --- /dev/null +++ b/packages/notebook-extension/style/base.css @@ -0,0 +1,79 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ + +.jp-ClassicKernelLogo { + flex: 0 0 auto; + display: flex; + align-items: center; + text-align: center; + margin-right: 8px; +} + +.jp-ClassicKernelLogo img { + max-width: 28px; + max-height: 28px; + display: flex; +} + +.jp-ClassicKernelStatus { + font-size: 12px; + margin: 0; + font-weight: normal; + color: var(--jp-ui-font-color0); + font-family: var(--jp-ui-font-family); + line-height: var(--jp-private-title-panel-height); + padding-left: 5px; + padding-right: 5px; +} + +.jp-ClassicKernelStatus-error { + background-color: var(--jp-error-color0); +} + +.jp-ClassicKernelStatus-warn { + background-color: var(--jp-warn-color0); +} + +.jp-ClassicKernelStatus-info { + background-color: var(--jp-info-color0); +} + +.jp-ClassicKernelStatus-fade { + animation: 0.5s fade-out forwards; +} + +@keyframes fade-out { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +#jp-title h1 { + cursor: pointer; + font-size: 18px; + margin: 0; + font-weight: normal; + color: var(--jp-ui-font-color0); + font-family: var(--jp-ui-font-family); + line-height: calc(1.5 * var(--jp-private-title-panel-height)); +} + +#jp-title h1:hover { + background: var(--jp-layout-color2); +} + +.jp-ClassicCheckpoint { + font-size: 14px; + margin-left: 5px; + margin-right: 5px; + font-weight: normal; + color: var(--jp-ui-font-color0); + font-family: var(--jp-ui-font-family); + line-height: calc(1.5 * var(--jp-private-title-panel-height)); +} diff --git a/packages/notebook-extension/style/index.css b/packages/notebook-extension/style/index.css new file mode 100644 index 000000000..f5246e666 --- /dev/null +++ b/packages/notebook-extension/style/index.css @@ -0,0 +1 @@ +@import url('./base.css'); diff --git a/packages/notebook-extension/tsconfig.json b/packages/notebook-extension/tsconfig.json new file mode 100644 index 000000000..399b75b7a --- /dev/null +++ b/packages/notebook-extension/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfigbase", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, + "include": ["src/**/*"] +} diff --git a/yarn.lock b/yarn.lock index 50cae548d..162a64f03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1806,6 +1806,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" @@ -1852,6 +1868,20 @@ "@lumino/disposable" "^1.4.3" "@lumino/widgets" "^1.16.1" +"@jupyterlab/mainmenu@^3.0.0-rc.13": + version "3.0.0-rc.13" + resolved "https://registry.yarnpkg.com/@jupyterlab/mainmenu/-/mainmenu-3.0.0-rc.13.tgz#2a56ebff92d052e79947753fdc5c1c6fe32ed816" + integrity sha512-lgNL6XZdmgFIifiePB6T62N3qiiDSaWNy3S60/smJnvys89oV5b5M3VIxFgR/JgV8Dg1lelFoyrE2zur3GJn/g== + dependencies: + "@jupyterlab/apputils" "^3.0.0-rc.13" + "@jupyterlab/services" "^6.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/widgets" "^1.16.1" + "@jupyterlab/mathjax2-extension@^3.0.0-rc.12": version "3.0.0-rc.13" resolved "https://registry.yarnpkg.com/@jupyterlab/mathjax2-extension/-/mathjax2-extension-3.0.0-rc.13.tgz#acdf4cea112e7d4b7ade3f56b3f4385e72d0bf9f"