@jupyterlab/apputils-extension 4.0.0-alpha.9 → 4.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/lib/announcements.d.ts +2 -0
  2. package/lib/announcements.js +227 -0
  3. package/lib/announcements.js.map +1 -0
  4. package/lib/index.js +66 -12
  5. package/lib/index.js.map +1 -1
  6. package/lib/notificationplugin.d.ts +5 -0
  7. package/lib/notificationplugin.js +504 -0
  8. package/lib/notificationplugin.js.map +1 -0
  9. package/lib/palette.js +3 -3
  10. package/lib/palette.js.map +1 -1
  11. package/lib/settingconnector.d.ts +4 -1
  12. package/lib/settingconnector.js +8 -1
  13. package/lib/settingconnector.js.map +1 -1
  14. package/lib/settingsplugin.js +6 -5
  15. package/lib/settingsplugin.js.map +1 -1
  16. package/lib/shortcuts.d.ts +21 -0
  17. package/lib/shortcuts.js +137 -0
  18. package/lib/shortcuts.js.map +1 -0
  19. package/lib/statusbarplugin.js +10 -9
  20. package/lib/statusbarplugin.js.map +1 -1
  21. package/lib/themesplugins.js +24 -0
  22. package/lib/themesplugins.js.map +1 -1
  23. package/lib/toolbarregistryplugin.js +4 -0
  24. package/lib/toolbarregistryplugin.js.map +1 -1
  25. package/lib/workspacesplugin.js +6 -6
  26. package/lib/workspacesplugin.js.map +1 -1
  27. package/package.json +28 -23
  28. package/schema/notification.json +48 -0
  29. package/schema/palette.json +2 -0
  30. package/schema/sanitizer.json +25 -0
  31. package/schema/utilityCommands.json +35 -0
  32. package/src/announcements.ts +297 -0
  33. package/src/index.ts +721 -0
  34. package/src/notificationplugin.tsx +902 -0
  35. package/src/palette.ts +213 -0
  36. package/src/settingconnector.ts +73 -0
  37. package/src/settingsplugin.ts +66 -0
  38. package/src/shortcuts.tsx +191 -0
  39. package/src/statusbarplugin.ts +145 -0
  40. package/src/themesplugins.ts +290 -0
  41. package/src/toolbarregistryplugin.ts +29 -0
  42. package/src/typings.d.ts +9 -0
  43. package/src/workspacesplugin.ts +306 -0
  44. package/style/base.css +2 -1
  45. package/style/contextualhelp.css +42 -0
  46. package/style/notification.css +227 -0
  47. package/style/scrollbar.raw.css +64 -0
  48. package/style/redirect.css +0 -15
package/src/palette.ts ADDED
@@ -0,0 +1,213 @@
1
+ /* -----------------------------------------------------------------------------
2
+ | Copyright (c) Jupyter Development Team.
3
+ | Distributed under the terms of the Modified BSD License.
4
+ |----------------------------------------------------------------------------*/
5
+
6
+ import { ILayoutRestorer, JupyterFrontEnd } from '@jupyterlab/application';
7
+ import {
8
+ ICommandPalette,
9
+ IPaletteItem,
10
+ ModalCommandPalette
11
+ } from '@jupyterlab/apputils';
12
+ import { ISettingRegistry } from '@jupyterlab/settingregistry';
13
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
14
+ import { CommandPaletteSvg, paletteIcon } from '@jupyterlab/ui-components';
15
+ import { find } from '@lumino/algorithm';
16
+ import { CommandRegistry } from '@lumino/commands';
17
+ import { DisposableDelegate, IDisposable } from '@lumino/disposable';
18
+ import { CommandPalette } from '@lumino/widgets';
19
+
20
+ /**
21
+ * The command IDs used by the apputils extension.
22
+ */
23
+ namespace CommandIDs {
24
+ export const activate = 'apputils:activate-command-palette';
25
+ }
26
+
27
+ const PALETTE_PLUGIN_ID = '@jupyterlab/apputils-extension:palette';
28
+
29
+ /**
30
+ * A thin wrapper around the `CommandPalette` class to conform with the
31
+ * JupyterLab interface for the application-wide command palette.
32
+ */
33
+ export class Palette implements ICommandPalette {
34
+ /**
35
+ * Create a palette instance.
36
+ */
37
+ constructor(palette: CommandPalette, translator?: ITranslator) {
38
+ this.translator = translator || nullTranslator;
39
+ const trans = this.translator.load('jupyterlab');
40
+ this._palette = palette;
41
+ this._palette.title.label = '';
42
+ this._palette.title.caption = trans.__('Command Palette');
43
+ }
44
+
45
+ /**
46
+ * The placeholder text of the command palette's search input.
47
+ */
48
+ set placeholder(placeholder: string) {
49
+ this._palette.inputNode.placeholder = placeholder;
50
+ }
51
+ get placeholder(): string {
52
+ return this._palette.inputNode.placeholder;
53
+ }
54
+
55
+ /**
56
+ * Activate the command palette for user input.
57
+ */
58
+ activate(): void {
59
+ this._palette.activate();
60
+ }
61
+
62
+ /**
63
+ * Add a command item to the command palette.
64
+ *
65
+ * @param options - The options for creating the command item.
66
+ *
67
+ * @returns A disposable that will remove the item from the palette.
68
+ */
69
+ addItem(options: IPaletteItem): IDisposable {
70
+ const item = this._palette.addItem(options as CommandPalette.IItemOptions);
71
+ return new DisposableDelegate(() => {
72
+ this._palette.removeItem(item);
73
+ });
74
+ }
75
+
76
+ protected translator: ITranslator;
77
+ private _palette: CommandPalette;
78
+ }
79
+
80
+ /**
81
+ * A namespace for `Palette` statics.
82
+ */
83
+ export namespace Palette {
84
+ /**
85
+ * Activate the command palette.
86
+ */
87
+ export function activate(
88
+ app: JupyterFrontEnd,
89
+ translator: ITranslator,
90
+ settingRegistry: ISettingRegistry | null
91
+ ): ICommandPalette {
92
+ const { commands, shell } = app;
93
+ const trans = translator.load('jupyterlab');
94
+ const palette = Private.createPalette(app, translator);
95
+ const modalPalette = new ModalCommandPalette({ commandPalette: palette });
96
+ let modal = false;
97
+
98
+ palette.node.setAttribute('role', 'region');
99
+ palette.node.setAttribute(
100
+ 'aria-label',
101
+ trans.__('Command Palette Section')
102
+ );
103
+ shell.add(palette, 'left', { rank: 300, type: 'Command Palette' });
104
+ if (settingRegistry) {
105
+ const loadSettings = settingRegistry.load(PALETTE_PLUGIN_ID);
106
+ const updateSettings = (settings: ISettingRegistry.ISettings): void => {
107
+ const newModal = settings.get('modal').composite as boolean;
108
+ if (modal && !newModal) {
109
+ palette.parent = null;
110
+ modalPalette.detach();
111
+ shell.add(palette, 'left', { rank: 300, type: 'Command Palette' });
112
+ } else if (!modal && newModal) {
113
+ palette.parent = null;
114
+ modalPalette.palette = palette;
115
+ palette.show();
116
+ modalPalette.attach();
117
+ }
118
+ modal = newModal;
119
+ };
120
+
121
+ Promise.all([loadSettings, app.restored])
122
+ .then(([settings]) => {
123
+ updateSettings(settings);
124
+ settings.changed.connect(settings => {
125
+ updateSettings(settings);
126
+ });
127
+ })
128
+ .catch((reason: Error) => {
129
+ console.error(reason.message);
130
+ });
131
+ }
132
+
133
+ // Show the current palette shortcut in its title.
134
+ const updatePaletteTitle = () => {
135
+ const binding = find(
136
+ app.commands.keyBindings,
137
+ b => b.command === CommandIDs.activate
138
+ );
139
+ if (binding) {
140
+ const ks = binding.keys.map(CommandRegistry.formatKeystroke).join(', ');
141
+ palette.title.caption = trans.__('Commands (%1)', ks);
142
+ } else {
143
+ palette.title.caption = trans.__('Commands');
144
+ }
145
+ };
146
+ updatePaletteTitle();
147
+ app.commands.keyBindingChanged.connect(() => {
148
+ updatePaletteTitle();
149
+ });
150
+
151
+ commands.addCommand(CommandIDs.activate, {
152
+ execute: () => {
153
+ if (modal) {
154
+ modalPalette.activate();
155
+ } else {
156
+ shell.activateById(palette.id);
157
+ }
158
+ },
159
+ label: trans.__('Activate Command Palette')
160
+ });
161
+
162
+ palette.inputNode.placeholder = trans.__('SEARCH');
163
+
164
+ return new Palette(palette, translator);
165
+ }
166
+
167
+ /**
168
+ * Restore the command palette.
169
+ */
170
+ export function restore(
171
+ app: JupyterFrontEnd,
172
+ restorer: ILayoutRestorer,
173
+ translator: ITranslator
174
+ ): void {
175
+ const palette = Private.createPalette(app, translator);
176
+ // Let the application restorer track the command palette for restoration of
177
+ // application state (e.g. setting the command palette as the current side bar
178
+ // widget).
179
+ restorer.add(palette, 'command-palette');
180
+ }
181
+ }
182
+
183
+ /**
184
+ * The namespace for module private data.
185
+ */
186
+ namespace Private {
187
+ /**
188
+ * The private command palette instance.
189
+ */
190
+ let palette: CommandPalette;
191
+
192
+ /**
193
+ * Create the application-wide command palette.
194
+ */
195
+ export function createPalette(
196
+ app: JupyterFrontEnd,
197
+ translator: ITranslator
198
+ ): CommandPalette {
199
+ if (!palette) {
200
+ // use a renderer tweaked to use inline svg icons
201
+ palette = new CommandPalette({
202
+ commands: app.commands,
203
+ renderer: CommandPaletteSvg.defaultRenderer
204
+ });
205
+ palette.id = 'command-palette';
206
+ palette.title.icon = paletteIcon;
207
+ const trans = translator.load('jupyterlab');
208
+ palette.title.label = trans.__('Commands');
209
+ }
210
+
211
+ return palette;
212
+ }
213
+ }
@@ -0,0 +1,73 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import { PageConfig } from '@jupyterlab/coreutils';
7
+ import { ISettingRegistry } from '@jupyterlab/settingregistry';
8
+ import { DataConnector, IDataConnector } from '@jupyterlab/statedb';
9
+ import { Throttler } from '@lumino/polling';
10
+
11
+ /**
12
+ * A data connector for fetching settings.
13
+ *
14
+ * #### Notes
15
+ * This connector adds a query parameter to the base services setting manager.
16
+ */
17
+ export class SettingConnector extends DataConnector<
18
+ ISettingRegistry.IPlugin,
19
+ string
20
+ > {
21
+ constructor(connector: IDataConnector<ISettingRegistry.IPlugin, string>) {
22
+ super();
23
+ this._connector = connector;
24
+ }
25
+
26
+ /**
27
+ * Fetch settings for a plugin.
28
+ * @param id - The plugin ID
29
+ *
30
+ * #### Notes
31
+ * The REST API requests are throttled at one request per plugin per 100ms.
32
+ */
33
+ fetch(id: string): Promise<ISettingRegistry.IPlugin | undefined> {
34
+ const throttlers = this._throttlers;
35
+ if (!(id in throttlers)) {
36
+ throttlers[id] = new Throttler(() => this._connector.fetch(id), 100);
37
+ }
38
+ return throttlers[id].invoke();
39
+ }
40
+
41
+ async list(query: 'ids'): Promise<{ ids: string[] }>;
42
+ async list(
43
+ query: 'active' | 'all'
44
+ ): Promise<{ ids: string[]; values: ISettingRegistry.IPlugin[] }>;
45
+ async list(
46
+ query: 'active' | 'all' | 'ids' = 'all'
47
+ ): Promise<{ ids: string[]; values?: ISettingRegistry.IPlugin[] }> {
48
+ const { isDeferred, isDisabled } = PageConfig.Extension;
49
+ const { ids, values } = await this._connector.list(
50
+ query === 'ids' ? 'ids' : undefined
51
+ );
52
+
53
+ if (query === 'all') {
54
+ return { ids, values };
55
+ }
56
+
57
+ if (query === 'ids') {
58
+ return { ids };
59
+ }
60
+
61
+ return {
62
+ ids: ids.filter(id => !isDeferred(id) && !isDisabled(id)),
63
+ values: values.filter(({ id }) => !isDeferred(id) && !isDisabled(id))
64
+ };
65
+ }
66
+
67
+ async save(id: string, raw: string): Promise<void> {
68
+ await this._connector.save(id, raw);
69
+ }
70
+
71
+ private _connector: IDataConnector<ISettingRegistry.IPlugin, string>;
72
+ private _throttlers: { [key: string]: Throttler } = Object.create(null);
73
+ }
@@ -0,0 +1,66 @@
1
+ /* -----------------------------------------------------------------------------
2
+ | Copyright (c) Jupyter Development Team.
3
+ | Distributed under the terms of the Modified BSD License.
4
+ |----------------------------------------------------------------------------*/
5
+
6
+ import {
7
+ JupyterFrontEnd,
8
+ JupyterFrontEndPlugin
9
+ } from '@jupyterlab/application';
10
+ import { PageConfig } from '@jupyterlab/coreutils';
11
+ import { ISettingRegistry, SettingRegistry } from '@jupyterlab/settingregistry';
12
+ import { SettingConnector } from './settingconnector';
13
+
14
+ /**
15
+ * The default setting registry provider.
16
+ */
17
+ export const settingsPlugin: JupyterFrontEndPlugin<ISettingRegistry> = {
18
+ id: '@jupyterlab/apputils-extension:settings',
19
+ activate: async (app: JupyterFrontEnd): Promise<ISettingRegistry> => {
20
+ const { isDisabled } = PageConfig.Extension;
21
+ const connector = new SettingConnector(app.serviceManager.settings);
22
+
23
+ // On startup, check if a plugin is available in the application.
24
+ // This helps avoid loading plugin files from other lab-based applications
25
+ // that have placed their schemas next to the JupyterLab schemas. Different lab-based
26
+ // applications might not have the same set of plugins loaded on the page.
27
+ // As an example this helps prevent having new toolbar items added by another application
28
+ // appear in JupyterLab as a side-effect when they are defined via the settings system.
29
+ const registry = new SettingRegistry({
30
+ connector,
31
+ plugins: (await connector.list('active')).values.filter(value =>
32
+ app.hasPlugin(value.id)
33
+ )
34
+ });
35
+
36
+ // If there are plugins that have schemas that are not in the setting
37
+ // registry after the application has restored, try to load them manually
38
+ // because otherwise, its settings will never become available in the
39
+ // setting registry.
40
+ void app.restored.then(async () => {
41
+ const plugins = await connector.list('ids');
42
+ plugins.ids.forEach(async id => {
43
+ if (!app.hasPlugin(id) || isDisabled(id) || id in registry.plugins) {
44
+ return;
45
+ }
46
+
47
+ try {
48
+ await registry.load(id);
49
+ } catch (error) {
50
+ console.warn(`Settings failed to load for (${id})`, error);
51
+ if (!app.isPluginActivated(id)) {
52
+ console.warn(
53
+ `If 'jupyter.lab.transform=true' in the plugin schema, this ` +
54
+ `may happen if {autoStart: false} in (${id}) or if it is ` +
55
+ `one of the deferredExtensions in page config.`
56
+ );
57
+ }
58
+ }
59
+ });
60
+ });
61
+
62
+ return registry;
63
+ },
64
+ autoStart: true,
65
+ provides: ISettingRegistry
66
+ };
@@ -0,0 +1,191 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import { CommandRegistry } from '@lumino/commands';
7
+ import { Selector } from '@lumino/domutils';
8
+ import * as React from 'react';
9
+ import { Dialog, showDialog } from '@jupyterlab/apputils';
10
+ import { TranslationBundle } from '@jupyterlab/translation';
11
+
12
+ /**
13
+ * The class name for each row of ContextShortcutTable
14
+ */
15
+ const SHORTCUT_TABLE_ROW_CLASS = 'jp-ContextualShortcut-TableRow';
16
+ /**
17
+ * The class name for the last row of ContextShortcutTable
18
+ */
19
+ const SHORTCUT_TABLE_LAST_ROW_CLASS = 'jp-ContextualShortcut-TableLastRow';
20
+ /**
21
+ * The class name for each item of ContextShortcutTable
22
+ */
23
+ const SHORTCUT_TABLE_ITEM_CLASS = 'jp-ContextualShortcut-TableItem';
24
+ /**
25
+ * The class name for each button-like symbol representing a key used in a shortcut in the ContextShortcutTable
26
+ */
27
+ const SHORTCUT_KEY_CLASS = 'jp-ContextualShortcut-Key';
28
+
29
+ /**
30
+ * Display shortcuts options
31
+ */
32
+ export interface IOptions {
33
+ /**
34
+ * Application commands registry
35
+ */
36
+ commands: CommandRegistry;
37
+ /**
38
+ * Translation object
39
+ */
40
+ trans: TranslationBundle;
41
+ /**
42
+ * Element on which to display the keyboard shortcuts help
43
+ */
44
+ activeElement?: Element;
45
+ }
46
+
47
+ export function displayShortcuts(options: IOptions) {
48
+ const { commands, trans, activeElement } = options;
49
+ const elt = activeElement ?? document.activeElement;
50
+
51
+ /**
52
+ * Find the distance from the target node to the first matching node.
53
+ *
54
+ * Based on Lumino private function commands.Private.targetDistance
55
+ * This traverses the DOM path from `elt` to the root
56
+ * computes the distance from `elt` to the first node which matches
57
+ * the CSS selector. If no match is found, `-1` is returned.
58
+ *
59
+ * It also stops traversal if the `data-lm-suppress-shortcuts` or
60
+ * `data-p-suppress-shortcuts` attributes are found.
61
+ */
62
+
63
+ function formatKeys(keys: string[]): JSX.Element {
64
+ const topContainer: JSX.Element[] = [];
65
+ for (const key of keys) {
66
+ const container: JSX.Element[] = [];
67
+ for (const ch of key.split(' ')) {
68
+ container.push(
69
+ <span className={SHORTCUT_KEY_CLASS}>
70
+ <kbd>{ch}</kbd>
71
+ </span>,
72
+ <> + </>
73
+ );
74
+ }
75
+ /*topContainer.push(<span>{container.slice(0, -1)}</span>, <>,</>);*/
76
+ topContainer.push(<span>{container.slice(0, -1)}</span>, <> + </>);
77
+ }
78
+ return <span>{topContainer.slice(0, -1)}</span>;
79
+ }
80
+
81
+ function capitalizeString(str: string) {
82
+ const capitalizedStr = str.charAt(0).toUpperCase() + str.slice(1);
83
+ return capitalizedStr;
84
+ }
85
+
86
+ function formatLabel(b: CommandRegistry.IKeyBinding) {
87
+ const label = commands.label(b.command);
88
+ const commandID = b.command.split(':')[1];
89
+ const automaticLabel = commandID.split('-');
90
+ let capitalizedLabel = '';
91
+ for (let i = 0; i < automaticLabel.length; i++) {
92
+ const str = capitalizeString(automaticLabel[i]);
93
+ capitalizedLabel = capitalizedLabel + ' ' + str;
94
+ }
95
+
96
+ if (label.length > 0) {
97
+ return label;
98
+ } else {
99
+ return capitalizedLabel;
100
+ }
101
+ }
102
+
103
+ function matchDistance(selector: string, elt: Element | null): number {
104
+ let targ = elt;
105
+ for (
106
+ let dist = 0;
107
+ targ !== null && targ !== targ.parentElement;
108
+ targ = targ.parentElement, ++dist
109
+ ) {
110
+ if (targ.hasAttribute('data-lm-suppress-shortcuts')) {
111
+ return -1;
112
+ }
113
+ if (targ.matches(selector)) {
114
+ return dist;
115
+ }
116
+ }
117
+ return -1;
118
+ }
119
+
120
+ // Find active keybindings for target element
121
+ const activeBindings = new Map<
122
+ string,
123
+ [number, CommandRegistry.IKeyBinding]
124
+ >();
125
+ for (let i = 0; i < commands.keyBindings.length; i++) {
126
+ const kb = commands.keyBindings[i];
127
+ let distance = matchDistance(kb.selector, elt);
128
+ if (distance < 0) {
129
+ continue;
130
+ }
131
+ let formatted = CommandRegistry.formatKeystroke(kb.keys);
132
+ if (activeBindings.has(formatted)) {
133
+ let oldBinding = activeBindings.get(formatted)!;
134
+ // if the existing binding takes precedence, ignore this binding by continuing
135
+ if (
136
+ oldBinding[0] < distance ||
137
+ (oldBinding[0] === distance &&
138
+ Selector.calculateSpecificity(oldBinding[1].selector) >
139
+ Selector.calculateSpecificity(kb.selector))
140
+ ) {
141
+ continue;
142
+ }
143
+ }
144
+ activeBindings.set(formatted, [distance, kb]);
145
+ }
146
+
147
+ // Group shortcuts by distance
148
+ let maxDistance = -1;
149
+ const groupedBindings = new Map<number, CommandRegistry.IKeyBinding[]>();
150
+ for (let [distance, binding] of activeBindings.values()) {
151
+ maxDistance = Math.max(distance, maxDistance);
152
+ if (!groupedBindings.has(distance)) {
153
+ groupedBindings.set(distance, []);
154
+ }
155
+ groupedBindings.get(distance)!.push(binding);
156
+ }
157
+
158
+ // Display shortcuts by group
159
+ const bindingTable: any = [];
160
+ for (let d = 0; d <= maxDistance; d++) {
161
+ if (groupedBindings.has(d)) {
162
+ bindingTable.push(
163
+ groupedBindings.get(d)!.map(b => (
164
+ <tr className={SHORTCUT_TABLE_ROW_CLASS} key={b.command}>
165
+ <td className={SHORTCUT_TABLE_ITEM_CLASS}>{formatLabel(b)}</td>
166
+ <td className={SHORTCUT_TABLE_ITEM_CLASS}>
167
+ {formatKeys([...b.keys])}
168
+ </td>
169
+ </tr>
170
+ ))
171
+ );
172
+ bindingTable.push(<tr className={SHORTCUT_TABLE_LAST_ROW_CLASS}></tr>);
173
+ }
174
+ }
175
+
176
+ const body = (
177
+ <table>
178
+ <tbody>{bindingTable}</tbody>
179
+ </table>
180
+ );
181
+
182
+ return showDialog({
183
+ title: trans.__('Keyboard Shortcuts'),
184
+ body,
185
+ buttons: [
186
+ Dialog.cancelButton({
187
+ label: trans.__('Close')
188
+ })
189
+ ]
190
+ });
191
+ }
@@ -0,0 +1,145 @@
1
+ /* -----------------------------------------------------------------------------
2
+ | Copyright (c) Jupyter Development Team.
3
+ | Distributed under the terms of the Modified BSD License.
4
+ |----------------------------------------------------------------------------*/
5
+
6
+ import {
7
+ ILabShell,
8
+ JupyterFrontEnd,
9
+ JupyterFrontEndPlugin
10
+ } from '@jupyterlab/application';
11
+ import {
12
+ IKernelStatusModel,
13
+ ISessionContext,
14
+ ISessionContextDialogs,
15
+ KernelStatus,
16
+ RunningSessions,
17
+ SessionContextDialogs
18
+ } from '@jupyterlab/apputils';
19
+ import { IStatusBar } from '@jupyterlab/statusbar';
20
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
21
+ import { Title, Widget } from '@lumino/widgets';
22
+
23
+ /**
24
+ * A plugin that provides a kernel status item to the status bar.
25
+ */
26
+ export const kernelStatus: JupyterFrontEndPlugin<IKernelStatusModel> = {
27
+ id: '@jupyterlab/apputils-extension:kernel-status',
28
+ autoStart: true,
29
+ requires: [IStatusBar],
30
+ provides: IKernelStatusModel,
31
+ optional: [ISessionContextDialogs, ITranslator, ILabShell],
32
+ activate: (
33
+ app: JupyterFrontEnd,
34
+ statusBar: IStatusBar,
35
+ sessionDialogs_: ISessionContextDialogs | null,
36
+ translator_: ITranslator | null,
37
+ labShell: ILabShell | null
38
+ ): IKernelStatusModel => {
39
+ const translator = translator_ ?? nullTranslator;
40
+ const sessionDialogs =
41
+ sessionDialogs_ ?? new SessionContextDialogs({ translator });
42
+ // When the status item is clicked, launch the kernel
43
+ // selection dialog for the current session.
44
+ const changeKernel = async () => {
45
+ if (!item.model.sessionContext) {
46
+ return;
47
+ }
48
+ await sessionDialogs.selectKernel(item.model.sessionContext);
49
+ };
50
+
51
+ // Create the status item.
52
+ const item = new KernelStatus({ onClick: changeKernel }, translator);
53
+
54
+ const providers = new Set<(w: Widget | null) => ISessionContext | null>();
55
+
56
+ const addSessionProvider = (
57
+ provider: (w: Widget | null) => ISessionContext | null
58
+ ): void => {
59
+ providers.add(provider);
60
+
61
+ if (app.shell.currentWidget) {
62
+ updateSession(app.shell, {
63
+ newValue: app.shell.currentWidget,
64
+ oldValue: null
65
+ });
66
+ }
67
+ };
68
+
69
+ function updateSession(
70
+ shell: JupyterFrontEnd.IShell,
71
+ changes: ILabShell.IChangedArgs
72
+ ) {
73
+ const { oldValue, newValue } = changes;
74
+
75
+ // Clean up after the old value if it exists,
76
+ // listen for changes to the title of the activity
77
+ if (oldValue) {
78
+ oldValue.title.changed.disconnect(onTitleChanged);
79
+ }
80
+
81
+ item.model.sessionContext =
82
+ [...providers]
83
+ .map(provider => provider(changes.newValue))
84
+ .filter(session => session !== null)[0] ?? null;
85
+
86
+ if (newValue && item.model.sessionContext) {
87
+ onTitleChanged(newValue.title);
88
+ newValue.title.changed.connect(onTitleChanged);
89
+ }
90
+ }
91
+
92
+ // When the title of the active widget changes, update the label
93
+ // of the hover text.
94
+ const onTitleChanged = (title: Title<Widget>) => {
95
+ item.model!.activityName = title.label;
96
+ };
97
+
98
+ if (labShell) {
99
+ labShell.currentChanged.connect(updateSession);
100
+ }
101
+
102
+ statusBar.registerStatusItem(kernelStatus.id, {
103
+ item,
104
+ align: 'left',
105
+ rank: 1,
106
+ isActive: () => item.model!.sessionContext !== null
107
+ });
108
+
109
+ return { addSessionProvider };
110
+ }
111
+ };
112
+
113
+ /*
114
+ * A plugin providing running terminals and sessions information
115
+ * to the status bar.
116
+ */
117
+ export const runningSessionsStatus: JupyterFrontEndPlugin<void> = {
118
+ id: '@jupyterlab/apputils-extension:running-sessions-status',
119
+ autoStart: true,
120
+ requires: [IStatusBar, ITranslator],
121
+ activate: (
122
+ app: JupyterFrontEnd,
123
+ statusBar: IStatusBar,
124
+ translator: ITranslator
125
+ ) => {
126
+ const item = new RunningSessions({
127
+ onClick: () => app.shell.activateById('jp-running-sessions'),
128
+ serviceManager: app.serviceManager,
129
+ translator
130
+ });
131
+
132
+ item.model.sessions = Array.from(
133
+ app.serviceManager.sessions.running()
134
+ ).length;
135
+ item.model.terminals = Array.from(
136
+ app.serviceManager.terminals.running()
137
+ ).length;
138
+
139
+ statusBar.registerStatusItem(runningSessionsStatus.id, {
140
+ item,
141
+ align: 'left',
142
+ rank: 0
143
+ });
144
+ }
145
+ };