Merge pull request #13 from jtpio/tree

Add the tree page
pull/6294/head
Jeremy Tuloup 5 years ago committed by GitHub
commit bdf3497387
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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.

@ -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')
);

@ -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",

@ -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 */

@ -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()

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{page_config['appName'] | e}}</title>
<title>{{page_config['appName'] | e}} - Notebook</title>
</head>
<body>
@ -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') %}
<script id="jupyter-config-data" type="application/json">
{{ page_config_full | tojson }}
</script>

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{page_config['appName'] | e}} - Tree</title>
</head>
<body>
{# 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') %}
<script id="jupyter-config-data" type="application/json">
{{ page_config_full | tojson }}
</script>
<script src="{{page_config['fullStaticUrl'] | e}}/bundle.js" main="index"></script>
<script type="text/javascript">
/* Remove token from URL. */
(function () {
var parsedUrl = new URL(window.location.href);
if (parsedUrl.searchParams.get('token')) {
parsedUrl.searchParams.delete('token');
window.history.replaceState({ }, '', parsedUrl.href);
}
})();
</script>
</body>
</html>

@ -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"

@ -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<void> = {
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<void> = {
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<void> = {
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<void> = {
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<void> = {
}
};
/**
* The main plugin.
*/
const main: JupyterFrontEndPlugin<void> = {
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<void> = {
}
};
/**
* A plugin to display the title of the notebook
*/
const title: JupyterFrontEndPlugin<void> = {
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<ITranslator> = {
provides: ITranslator
};
/**
* The default tree route resolver plugin.
*/
const tree: JupyterFrontEndPlugin<void> = {
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<void> = {
* Export the plugins as default.
*/
const plugins: JupyterFrontEndPlugin<any>[] = [
checkpoints,
kernelLogo,
kernelStatus,
logo,
main,
noTabsMenu,
paths,
router,
sessionDialogs,
shell,
spacer,
title,
topVisibility,
translator,
tree,
zen
];

@ -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));
}

@ -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);
}
/**

@ -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
}
}

@ -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<void> = {
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<Kernel.IModel>,
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<any>[] = [opener];
export default plugins;

@ -0,0 +1 @@
@import url('./base.css');

@ -0,0 +1,8 @@
{
"extends": "../../tsconfigbase",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": ["src/**/*"]
}

@ -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
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,3 @@
.jp-FileBrowser {
height: 100%;
}

@ -0,0 +1,3 @@
@import url('~@jupyterlab/filebrowser/style/index.css');
@import url('./base.css');

@ -0,0 +1,13 @@
{
"extends": "../../tsconfigbase",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": ["src/**/*"],
"references": [
{
"path": "../application"
}
]
}

@ -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
}
}

@ -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<void> = {
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<void> = {
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<void> = {
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<JupyterFrontEnd.IPaths> = {
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<IClassicShell> = {
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<void> = {
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<void> = {
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<any>[] = [
checkpoints,
kernelLogo,
kernelStatus,
title,
tree
];
export default plugins;

@ -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));
}

@ -0,0 +1 @@
@import url('./base.css');

@ -0,0 +1,8 @@
{
"extends": "../../tsconfigbase",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": ["src/**/*"]
}

@ -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"

Loading…
Cancel
Save