parent
95b39d037e
commit
ebfeaf35c7
@ -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,298 @@
|
||||
// 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 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
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Export the plugins as default.
|
||||
*/
|
||||
const plugins: JupyterFrontEndPlugin<any>[] = [
|
||||
checkpoints,
|
||||
kernelLogo,
|
||||
kernelStatus,
|
||||
title
|
||||
];
|
||||
|
||||
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,13 @@
|
||||
{
|
||||
"extends": "../../tsconfigbase",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../application"
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in new issue