diff --git a/packages/running-extension/package.json b/packages/running-extension/package.json index 636f80f22833..0354ba916d7d 100644 --- a/packages/running-extension/package.json +++ b/packages/running-extension/package.json @@ -36,7 +36,10 @@ }, "dependencies": { "@jupyterlab/application": "^1.0.1", - "@jupyterlab/running": "^1.0.1" + "@jupyterlab/coreutils": "^3.0.0", + "@jupyterlab/running": "^1.0.1", + "@jupyterlab/services": "^4.0.1", + "@phosphor/algorithm": "^1.1.2" }, "devDependencies": { "rimraf": "~2.6.2", diff --git a/packages/running-extension/src/index.ts b/packages/running-extension/src/index.ts index 98821dad5cbd..373e4d571bbb 100644 --- a/packages/running-extension/src/index.ts +++ b/packages/running-extension/src/index.ts @@ -6,15 +6,39 @@ import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; +import { + IRunningSessions, + IRunningSessionManagers, + RunningSessionManagers, + RunningSessions +} from '@jupyterlab/running'; + +import { Session } from '@jupyterlab/services'; +import { PathExt } from '@jupyterlab/coreutils'; +import { toArray } from '@phosphor/algorithm'; + +/** + * The class name added to a notebook icon. + */ +const NOTEBOOK_ICON_CLASS = 'jp-mod-notebook'; + +/** + * The class name added to a console icon. + */ +const CONSOLE_ICON_CLASS = 'jp-mod-console'; -import { RunningSessions } from '@jupyterlab/running'; +/** + * The class name added to a file icon. + */ +const FILE_ICON_CLASS = 'jp-mod-file'; /** * The default running sessions extension. */ -const plugin: JupyterFrontEndPlugin = { +const plugin: JupyterFrontEndPlugin = { activate, id: '@jupyterlab/running-extension:plugin', + provides: IRunningSessionManagers, optional: [ILayoutRestorer], autoStart: true }; @@ -30,8 +54,9 @@ export default plugin; function activate( app: JupyterFrontEnd, restorer: ILayoutRestorer | null -): void { - let running = new RunningSessions({ manager: app.serviceManager }); +): IRunningSessionManagers { + let runningSessionManagers = new RunningSessionManagers(); + let running = new RunningSessions(runningSessionManagers); running.id = 'jp-running-sessions'; running.title.iconClass = 'jp-RunningIcon jp-SideBar-tabIcon'; running.title.caption = 'Running Terminals and Kernels'; @@ -42,21 +67,77 @@ function activate( if (restorer) { restorer.add(running, 'running-sessions'); } + addKernelRunningSessionManager(runningSessionManagers, app); + // Rank has been chosen somewhat arbitrarily to give priority to the running + // sessions widget in the sidebar. + app.shell.add(running, 'left', { rank: 200 }); - running.sessionOpenRequested.connect((sender, model) => { - let path = model.path; - if (model.type.toLowerCase() === 'console') { - void app.commands.execute('console:open', { path }); - } else { - void app.commands.execute('docmanager:open', { path }); - } - }); + return runningSessionManagers; +} - running.terminalOpenRequested.connect((sender, model) => { - void app.commands.execute('terminal:open', { name: model.name }); +/** + * Add the running kernel manager (notebooks & consoles) to the running panel. + */ +function addKernelRunningSessionManager( + managers: IRunningSessionManagers, + app: JupyterFrontEnd +) { + let manager = app.serviceManager.sessions; + function filterSessions(m: Session.IModel) { + return !!( + (m.name || PathExt.basename(m.path)).indexOf('.') !== -1 || m.name + ); + } + + managers.add({ + name: 'Kernel', + running: () => { + return toArray(manager.running()) + .filter(filterSessions) + .map(model => new RunningKernel(model)); + }, + shutdownAll: () => manager.shutdownAll(), + refreshRunning: () => manager.refreshRunning(), + runningChanged: manager.runningChanged }); - // Rank has been chosen somewhat arbitrarily to give priority to the running - // sessions widget in the sidebar. - app.shell.add(running, 'left', { rank: 200 }); + class RunningKernel implements IRunningSessions.IRunningItem { + constructor(model: Session.IModel) { + this._model = model; + } + open() { + let { path, type } = this._model; + if (type.toLowerCase() === 'console') { + app.commands.execute('console:open', { path }); + } else { + app.commands.execute('docmanager:open', { path }); + } + } + shutdown() { + return manager.shutdown(this._model.id); + } + iconClass() { + let { name, path, type } = this._model; + if ((name || PathExt.basename(path)).indexOf('.ipynb') !== -1) { + return NOTEBOOK_ICON_CLASS; + } else if (type.toLowerCase() === 'console') { + return CONSOLE_ICON_CLASS; + } + return FILE_ICON_CLASS; + } + label() { + return this._model.name || PathExt.basename(this._model.path); + } + labelTitle() { + let { kernel, path } = this._model; + let kernelName = kernel.name; + if (manager.specs) { + const spec = manager.specs.kernelspecs[kernelName]; + kernelName = spec ? spec.display_name : 'unknown'; + } + return `Path: ${path}\nKernel: ${kernelName}`; + } + + private _model: Session.IModel; + } } diff --git a/packages/running-extension/tsconfig.json b/packages/running-extension/tsconfig.json index b86f01df47b0..63ef53b98914 100644 --- a/packages/running-extension/tsconfig.json +++ b/packages/running-extension/tsconfig.json @@ -9,8 +9,14 @@ { "path": "../application" }, + { + "path": "../coreutils" + }, { "path": "../running" + }, + { + "path": "../services" } ] } diff --git a/packages/running/package.json b/packages/running/package.json index 9ed5c35d98f1..74db67c920d9 100644 --- a/packages/running/package.json +++ b/packages/running/package.json @@ -37,8 +37,11 @@ "dependencies": { "@jupyterlab/apputils": "^1.0.1", "@jupyterlab/coreutils": "^3.0.0", + "@jupyterlab/running": "^1.0.0-alpha.3", "@jupyterlab/services": "^4.0.1", "@phosphor/algorithm": "^1.1.3", + "@phosphor/coreutils": "^1.3.0", + "@phosphor/disposable": "^1.1.2", "@phosphor/signaling": "^1.2.3", "react": "~16.8.4" }, diff --git a/packages/running/src/index.tsx b/packages/running/src/index.tsx index ddc96aa839ee..183fa5506561 100644 --- a/packages/running/src/index.tsx +++ b/packages/running/src/index.tsx @@ -3,10 +3,7 @@ import * as React from 'react'; -import { IIterator, toArray } from '@phosphor/algorithm'; - -import { ISignal, Signal } from '@phosphor/signaling'; - +import { ISignal } from '@phosphor/signaling'; import { ReactWidget, UseSignal } from '@jupyterlab/apputils'; import { @@ -15,9 +12,7 @@ import { ToolbarButtonComponent } from '@jupyterlab/apputils'; -import { PathExt } from '@jupyterlab/coreutils'; - -import { ServiceManager, Session, TerminalSession } from '@jupyterlab/services'; +import { Token } from '@phosphor/coreutils'; /** * The class name added to a running widget. @@ -69,107 +64,68 @@ const ITEM_LABEL_CLASS = 'jp-RunningSessions-itemLabel'; */ const SHUTDOWN_BUTTON_CLASS = 'jp-RunningSessions-itemShutdown'; +/* tslint:disable */ /** - * The class name added to a notebook icon. - */ -const NOTEBOOK_ICON_CLASS = 'jp-mod-notebook'; - -/** - * The class name added to a console icon. - */ -const CONSOLE_ICON_CLASS = 'jp-mod-console'; - -/** - * The class name added to a file icon. + * The running sessions token. */ -const FILE_ICON_CLASS = 'jp-mod-file'; +export const IRunningSessionManagers = new Token( + '@jupyterlab/running:IRunningSessionManagers' +); +/* tslint:enable */ /** - * The class name added to a terminal icon. + * The running interface. */ -const TERMINAL_ICON_CLASS = 'jp-mod-terminal'; - -/** - * Properties for a session list displaying items of generic type `M`. - */ -type SessionProps = { - /** - * A signal that tracks when the `open` is clicked on a session item. - */ - openRequested: Signal; - - /** - * The session manager. - */ - manager: { - /** - * The function called when the shutdown all button is pressed. - */ - shutdownAll(): void; - - /** - * A signal that should emit a new list of items whenever they are changed. - */ - runningChanged: ISignal; - - /** - * Returns a list the running models. - */ - running(): IIterator; - }; - +export interface IRunningSessionManagers { /** - * The function called when the shutdown button is pressed on an item. + * Add a running item manager. + * + * @param manager - The running item manager. + * */ - shutdown: (model: M) => void; - - /** - * The filter that is applied to the items from `runningChanged`. - */ - filterRunning?: (model: M) => boolean; - - /** - * The name displayed to the user. - */ - name: string; - + add(manager: IRunningSessions.IManager): void; /** - * Returns the icon class for an item. + * Return an iterator of managers. */ - iconClass: (model: M) => string; + items(): ReadonlyArray; +} +export class RunningSessionManagers implements IRunningSessionManagers { /** - * Returns the label for an item. + * Add a running item manager. + * + * @param manager - The running item manager. + * */ - label: (model: M) => string; + add(manager: IRunningSessions.IManager): void { + this._managers.push(manager); + } /** - * Called to determine the `title` attribute for each item, which is revealed - * on hover. + * Return an iterator of launcher items. */ - labelTitle?: (model: M) => string; + items(): ReadonlyArray { + return this._managers; + } - /** - * Flag that sets whether it sessions should be displayed. - */ - available: boolean; -}; + private _managers: IRunningSessions.IManager[] = []; +} -function Item(props: SessionProps & { model: M }) { - const { model } = props; +function Item(props: { runningItem: IRunningSessions.IRunningItem }) { + const { runningItem } = props; return (
  • - + props.openRequested.emit(model)} + title={runningItem.labelTitle ? runningItem.labelTitle() : ''} + onClick={() => runningItem.open()} > - {props.label(model)} + {runningItem.label()} @@ -177,32 +133,20 @@ function Item(props: SessionProps & { model: M }) { ); } -function ListView(props: { models: M[] } & SessionProps) { - const { models, ...rest } = props; +function ListView(props: { runningItems: IRunningSessions.IRunningItem[] }) { return (
      - {models.map((m, i) => ( - + {props.runningItems.map((item, i) => ( + ))}
    ); } -function List(props: SessionProps) { - const initialModels = toArray(props.manager.running()); - const filterRunning = props.filterRunning || (_ => true); - function render(models: Array) { - return ; - } - if (!props.available) { - return render(initialModels); - } +function List(props: { manager: IRunningSessions.IManager }) { return ( - - {(sender: any, args: Array) => render(args)} + + {() => } ); } @@ -213,14 +157,11 @@ function List(props: SessionProps) { * * It is specialized for each based on it's props. */ -function Section(props: SessionProps) { +function Section(props: { manager: IRunningSessions.IManager }) { function onShutdown() { - void showDialog({ - title: `Shut Down All ${props.name} Sessions?`, - buttons: [ - Dialog.cancelButton(), - Dialog.warnButton({ label: 'Shut Down All' }) - ] + showDialog({ + title: `Shutdown All ${props.manager.name} Sessions?`, + buttons: [Dialog.cancelButton(), Dialog.warnButton({ label: 'SHUTDOWN' })] }).then(result => { if (result.button.accept) { props.manager.shutdownAll(); @@ -229,89 +170,41 @@ function Section(props: SessionProps) { } return (
    - {props.available && ( - <> -
    -

    {props.name} Sessions

    - -
    - -
    - -
    - - )} + <> +
    +

    {props.manager.name} Sessions

    + +
    + +
    + +
    +
    ); } -interface IRunningSessionsProps { - manager: ServiceManager.IManager; - sessionOpenRequested: Signal; - terminalOpenRequested: Signal; -} - -function RunningSessionsComponent({ - manager, - sessionOpenRequested, - terminalOpenRequested -}: IRunningSessionsProps) { - const terminalsAvailable = manager.terminals.isAvailable(); - +function RunningSessionsComponent(props: { + managers: IRunningSessionManagers; +}) { return ( <>
    { - if (terminalsAvailable) { - void manager.terminals.refreshRunning(); - } - void manager.sessions.refreshRunning(); - }} + iconClassName="jp-RefreshIcon jp-Icon jp-Icon-16" + onClick={() => + props.managers.items().forEach(manager => manager.refreshRunning()) + } />
    -
    `${ITEM_ICON_CLASS} ${TERMINAL_ICON_CLASS}`} - label={m => `terminals/${m.name}`} - available={terminalsAvailable} - shutdown={m => manager.terminals.shutdown(m.name)} - /> -
    - !!((m.name || PathExt.basename(m.path)).indexOf('.') !== -1 || m.name) - } - name="Kernel" - iconClass={m => { - if ((m.name || PathExt.basename(m.path)).indexOf('.ipynb') !== -1) { - return NOTEBOOK_ICON_CLASS; - } else if (m.type.toLowerCase() === 'console') { - return CONSOLE_ICON_CLASS; - } - return FILE_ICON_CLASS; - }} - label={m => m.name || PathExt.basename(m.path)} - available={true} - labelTitle={m => { - let kernelName = m.kernel.name; - if (manager.specs) { - const spec = manager.specs.kernelspecs[kernelName]; - kernelName = spec ? spec.display_name : 'unknown'; - } - return `Path: ${m.path}\nKernel: ${kernelName}`; - }} - shutdown={m => manager.sessions.shutdown(m.id)} - /> + {props.managers.items().map(manager => ( +
    + ))} ); } @@ -323,56 +216,54 @@ export class RunningSessions extends ReactWidget { /** * Construct a new running widget. */ - constructor(options: RunningSessions.IOptions) { + constructor(managers: IRunningSessionManagers) { super(); - this.options = options; + this.managers = managers; // this can't be in the react element, because then it would be too nested this.addClass(RUNNING_CLASS); } protected render() { - return ( - - ); + return ; } - /** - * A signal emitted when a kernel session open is requested. - */ - get sessionOpenRequested(): ISignal { - return this._sessionOpenRequested; - } + private managers: IRunningSessionManagers; +} +/** + * The namespace for the `IRunningSessions` class statics. + */ +export namespace IRunningSessions { /** - * A signal emitted when a terminal session open is requested. + * A manager of running items grouped under a single section. */ - get terminalOpenRequested(): ISignal { - return this._terminalOpenRequested; + export interface IManager { + // Name that is shown to the user + name: string; + // called when the shutdown all button is pressed + shutdownAll(): void; + // list the running models. + running(): IRunningItem[]; + // Force a refresh of the running models. + refreshRunning(): void; + // A signal that should be emitted when the item list has changed. + runningChanged: ISignal; } - private _sessionOpenRequested = new Signal(this); - private _terminalOpenRequested = new Signal( - this - ); - private options: RunningSessions.IOptions; -} - -/** - * The namespace for the `RunningSessions` class statics. - */ -export namespace RunningSessions { /** - * An options object for creating a running sessions widget. + * A running item. */ - export interface IOptions { - /** - * A service manager instance. - */ - manager: ServiceManager.IManager; + export interface IRunningItem { + // called when the running item is clicked + open: () => void; + // called when the shutdown button is pressed on a particular item + shutdown: () => void; + // Class for the icon + iconClass: () => string; + // called to determine the label for each item + label: () => string; + // called to determine the `title` attribute for each item, which is revealed on hover + labelTitle?: () => string; } } diff --git a/packages/running/tsconfig.json b/packages/running/tsconfig.json index 96596a1f9c8e..9ab2e331b844 100644 --- a/packages/running/tsconfig.json +++ b/packages/running/tsconfig.json @@ -6,12 +6,18 @@ }, "include": ["src/*"], "references": [ + { + "path": "../application" + }, { "path": "../apputils" }, { "path": "../coreutils" }, + { + "path": "" + }, { "path": "../services" } diff --git a/packages/terminal-extension/package.json b/packages/terminal-extension/package.json index 417bd8d7ac7c..ba8a96ff1da7 100644 --- a/packages/terminal-extension/package.json +++ b/packages/terminal-extension/package.json @@ -41,8 +41,11 @@ "@jupyterlab/coreutils": "^3.0.0", "@jupyterlab/launcher": "^1.0.1", "@jupyterlab/mainmenu": "^1.0.1", + "@jupyterlab/running": "^1.0.0-alpha.3", + "@jupyterlab/services": "^4.0.1", "@jupyterlab/terminal": "^1.0.1", - "@phosphor/widgets": "^1.8.0" + "@phosphor/widgets": "^1.8.0", + "@phosphor/algorithm": "^1.1.3" }, "devDependencies": { "@types/webpack-env": "^1.13.9", diff --git a/packages/terminal-extension/src/index.ts b/packages/terminal-extension/src/index.ts index 527ed598add1..b3ee4c4b6492 100644 --- a/packages/terminal-extension/src/index.ts +++ b/packages/terminal-extension/src/index.ts @@ -16,17 +16,22 @@ import { WidgetTracker } from '@jupyterlab/apputils'; +import { TerminalSession } from '@jupyterlab/services'; + import { ILauncher } from '@jupyterlab/launcher'; import { IFileMenu, IMainMenu } from '@jupyterlab/mainmenu'; -import { ITerminalTracker, ITerminal } from '@jupyterlab/terminal'; +import { ITerminalTracker, Terminal } from '@jupyterlab/terminal'; +import { IRunningSessionManagers, IRunningSessions } from '@jupyterlab/running'; // Name-only import so as to not trigger inclusion in main bundle import * as WidgetModuleType from '@jupyterlab/terminal/lib/widget'; import { Menu } from '@phosphor/widgets'; +import { toArray } from '@phosphor/algorithm'; + /** * The command IDs used by the terminal plugin. */ @@ -49,6 +54,11 @@ namespace CommandIDs { */ const TERMINAL_ICON_CLASS = 'jp-TerminalIcon'; +/** + * The class name added to a running session item icon. + */ +const ITEM_ICON_CLASS = 'jp-RunningSessions-itemIcon'; + /** * The default terminal extension. */ @@ -62,7 +72,7 @@ const plugin: JupyterFrontEndPlugin = { ILauncher, ILayoutRestorer, IMainMenu, - IThemeManager + IRunningSessionManagers ], autoStart: true }; @@ -82,7 +92,7 @@ function activate( launcher: ILauncher | null, restorer: ILayoutRestorer | null, mainMenu: IMainMenu | null, - themeManager: IThemeManager + runningSessionManagers: IRunningSessionManagers | null ): ITerminalTracker { const { serviceManager, commands } = app; const category = 'Terminal'; @@ -248,6 +258,11 @@ function activate( }); } + // Add a sessions manager if the running extension is available + if (runningSessionManagers) { + addRunningSessionManager(runningSessionManagers, app); + } + app.contextMenu.addItem({ command: CommandIDs.refresh, selector: '.jp-Terminal', @@ -257,6 +272,45 @@ function activate( return tracker; } +/** + * Add the running terminal manager to the running panel. + */ +function addRunningSessionManager( + managers: IRunningSessionManagers, + app: JupyterFrontEnd +) { + let manager = app.serviceManager.terminals; + + managers.add({ + name: 'Terminal', + running: () => + toArray(manager.running()).map(model => new RunningTerminal(model)), + shutdownAll: () => manager.shutdownAll(), + refreshRunning: () => manager.refreshRunning(), + runningChanged: manager.runningChanged + }); + + class RunningTerminal implements IRunningSessions.IRunningItem { + constructor(model: TerminalSession.IModel) { + this._model = model; + } + open() { + app.commands.execute('terminal:open', { name: this._model.name }); + } + iconClass() { + return `${ITEM_ICON_CLASS} ${TERMINAL_ICON_CLASS}`; + } + label() { + return `terminals/${this._model.name}`; + } + shutdown() { + return manager.shutdown(this._model.name); + } + + private _model: TerminalSession.IModel; + } +} + /** * Add the commands for the terminal. */ diff --git a/packages/terminal-extension/tsconfig.json b/packages/terminal-extension/tsconfig.json index 827e4678fce2..c4f42d12458a 100644 --- a/packages/terminal-extension/tsconfig.json +++ b/packages/terminal-extension/tsconfig.json @@ -22,6 +22,12 @@ { "path": "../mainmenu" }, + { + "path": "../running" + }, + { + "path": "../services" + }, { "path": "../terminal" }