diff --git a/README.md b/README.md
index 8452f3602..b051714b2 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# jupyterlab-classic

-[](https://mybinder.org/v2/gh/jtpio/jupyterlab-classic/main?urlpath=classic/notebooks/binder/example.ipynb)
+[](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:
+
+
+
## 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"