@jupyterlab/running 4.2.0-alpha.1 → 4.2.0-beta.0
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.
- package/lib/index.d.ts +118 -7
- package/lib/index.js +485 -47
- package/lib/index.js.map +1 -1
- package/package.json +6 -4
- package/src/index.tsx +677 -64
- package/style/base.css +158 -11
package/lib/index.js
CHANGED
|
@@ -6,15 +6,21 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Dialog, showDialog } from '@jupyterlab/apputils';
|
|
8
8
|
import { nullTranslator } from '@jupyterlab/translation';
|
|
9
|
-
import { caretDownIcon, caretRightIcon, closeIcon, PanelWithToolbar, ReactWidget, refreshIcon, SidePanel, ToolbarButton, ToolbarButtonComponent, UseSignal } from '@jupyterlab/ui-components';
|
|
9
|
+
import { caretDownIcon, caretRightIcon, closeIcon, collapseAllIcon, expandAllIcon, FilterBox, PanelWithToolbar, ReactWidget, refreshIcon, SidePanel, tableRowsIcon, ToolbarButton, ToolbarButtonComponent, treeViewIcon, UseSignal } from '@jupyterlab/ui-components';
|
|
10
10
|
import { Token } from '@lumino/coreutils';
|
|
11
11
|
import { DisposableDelegate } from '@lumino/disposable';
|
|
12
|
+
import { ElementExt } from '@lumino/domutils';
|
|
12
13
|
import { Signal } from '@lumino/signaling';
|
|
13
|
-
import
|
|
14
|
+
import { Panel, Widget } from '@lumino/widgets';
|
|
15
|
+
import React, { isValidElement } from 'react';
|
|
14
16
|
/**
|
|
15
17
|
* The class name added to a running widget.
|
|
16
18
|
*/
|
|
17
19
|
const RUNNING_CLASS = 'jp-RunningSessions';
|
|
20
|
+
/**
|
|
21
|
+
* The class name added to a searchable widget.
|
|
22
|
+
*/
|
|
23
|
+
const SEARCHABLE_CLASS = 'jp-SearchableSessions';
|
|
18
24
|
/**
|
|
19
25
|
* The class name added to the running terminal sessions section.
|
|
20
26
|
*/
|
|
@@ -48,9 +54,37 @@ const SHUTDOWN_BUTTON_CLASS = 'jp-RunningSessions-itemShutdown';
|
|
|
48
54
|
*/
|
|
49
55
|
const SHUTDOWN_ALL_BUTTON_CLASS = 'jp-RunningSessions-shutdownAll';
|
|
50
56
|
/**
|
|
51
|
-
* The
|
|
57
|
+
* The class name added to a collapse/expand carets.
|
|
58
|
+
*/
|
|
59
|
+
const CARET_CLASS = 'jp-RunningSessions-caret';
|
|
60
|
+
/**
|
|
61
|
+
* The class name added to icons.
|
|
62
|
+
*/
|
|
63
|
+
const ITEM_ICON_CLASS = 'jp-RunningSessions-icon';
|
|
64
|
+
/**
|
|
65
|
+
* Modifier added to a section when flattened list view is requested.
|
|
66
|
+
*/
|
|
67
|
+
const LIST_VIEW_CLASS = 'jp-mod-running-list-view';
|
|
68
|
+
/**
|
|
69
|
+
* The class name added to button switching between nested and flat view.
|
|
70
|
+
*/
|
|
71
|
+
const VIEW_BUTTON_CLASS = 'jp-RunningSessions-viewButton';
|
|
72
|
+
/**
|
|
73
|
+
* The class name added to button switching between nested and flat view.
|
|
74
|
+
*/
|
|
75
|
+
const COLLAPSE_EXPAND_BUTTON_CLASS = 'jp-RunningSessions-collapseButton';
|
|
76
|
+
/**
|
|
77
|
+
* Identifier used in the state database.
|
|
78
|
+
*/
|
|
79
|
+
const STATE_DB_ID = 'jp-running-sessions';
|
|
80
|
+
/**
|
|
81
|
+
* The running sessions managers token.
|
|
52
82
|
*/
|
|
53
83
|
export const IRunningSessionManagers = new Token('@jupyterlab/running:IRunningSessionManagers', 'A service to add running session managers.');
|
|
84
|
+
/**
|
|
85
|
+
* The running sessions token.
|
|
86
|
+
*/
|
|
87
|
+
export const IRunningSessionSidebar = new Token('@jupyterlab/running:IRunningSessionsSidebar', 'A token allowing to modify the running sessions sidebar.');
|
|
54
88
|
export class RunningSessionManagers {
|
|
55
89
|
constructor() {
|
|
56
90
|
this._added = new Signal(this);
|
|
@@ -97,37 +131,112 @@ function Item(props) {
|
|
|
97
131
|
// Handle shutdown requests.
|
|
98
132
|
let stopPropagation = false;
|
|
99
133
|
const shutdownItemIcon = props.shutdownItemIcon || closeIcon;
|
|
100
|
-
const shutdownLabel = props.shutdownLabel
|
|
134
|
+
const shutdownLabel = (_b = (typeof props.shutdownLabel === 'function'
|
|
135
|
+
? props.shutdownLabel(runningItem)
|
|
136
|
+
: props.shutdownLabel)) !== null && _b !== void 0 ? _b : trans.__('Shut Down');
|
|
101
137
|
const shutdown = () => {
|
|
102
138
|
var _a;
|
|
103
139
|
stopPropagation = true;
|
|
104
140
|
(_a = runningItem.shutdown) === null || _a === void 0 ? void 0 : _a.call(runningItem);
|
|
105
141
|
};
|
|
142
|
+
// Materialise getter to avoid triggering it repeatedly
|
|
143
|
+
const children = runningItem.children;
|
|
106
144
|
// Manage collapsed state. Use the shutdown flag in lieu of `stopPropagation`.
|
|
107
145
|
const [collapsed, collapse] = React.useState(false);
|
|
108
|
-
const collapsible = !!(
|
|
146
|
+
const collapsible = !!(children === null || children === void 0 ? void 0 : children.length);
|
|
109
147
|
const onClick = collapsible
|
|
110
148
|
? () => !stopPropagation && collapse(!collapsed)
|
|
111
149
|
: undefined;
|
|
150
|
+
// Listen to signal to collapse from outside
|
|
151
|
+
props.collapseToggled.connect((_emitter, newCollapseState) => collapse(newCollapseState));
|
|
112
152
|
if (runningItem.className) {
|
|
113
153
|
classList.push(runningItem.className);
|
|
114
154
|
}
|
|
115
155
|
if (props.child) {
|
|
116
156
|
classList.push('jp-mod-running-child');
|
|
117
157
|
}
|
|
158
|
+
if (props.child && !children) {
|
|
159
|
+
classList.push('jp-mod-running-leaf');
|
|
160
|
+
}
|
|
118
161
|
return (React.createElement(React.Fragment, null,
|
|
119
162
|
React.createElement("li", null,
|
|
120
163
|
React.createElement("div", { className: classList.join(' '), onClick: onClick, "data-context": runningItem.context || '' },
|
|
121
164
|
collapsible &&
|
|
122
|
-
(collapsed ? (React.createElement(caretRightIcon.react, { tag: "span",
|
|
123
|
-
icon ? (typeof icon === 'string' ? (React.createElement("img", { src: icon })) : (React.createElement(icon.react, { tag: "span",
|
|
165
|
+
(collapsed ? (React.createElement(caretRightIcon.react, { tag: "span", className: CARET_CLASS })) : (React.createElement(caretDownIcon.react, { tag: "span", className: CARET_CLASS }))),
|
|
166
|
+
icon ? (typeof icon === 'string' ? (React.createElement("img", { src: icon, className: ITEM_ICON_CLASS })) : (React.createElement(icon.react, { tag: "span", className: ITEM_ICON_CLASS }))) : undefined,
|
|
124
167
|
React.createElement("span", { className: ITEM_LABEL_CLASS, title: title, onClick: runningItem.open && (() => runningItem.open()) }, runningItem.label()),
|
|
125
168
|
detail && React.createElement("span", { className: ITEM_DETAIL_CLASS }, detail),
|
|
126
169
|
runningItem.shutdown && (React.createElement(ToolbarButtonComponent, { className: SHUTDOWN_BUTTON_CLASS, icon: shutdownItemIcon, onClick: shutdown, tooltip: shutdownLabel }))),
|
|
127
|
-
collapsible && !collapsed && (React.createElement(List, { child: true, runningItems:
|
|
170
|
+
collapsible && !collapsed && (React.createElement(List, { child: true, runningItems: children, shutdownItemIcon: shutdownItemIcon, translator: translator, collapseToggled: props.collapseToggled })))));
|
|
128
171
|
}
|
|
129
172
|
function List(props) {
|
|
130
|
-
|
|
173
|
+
const filter = props.filter;
|
|
174
|
+
const items = filter
|
|
175
|
+
? props.runningItems
|
|
176
|
+
.map(item => {
|
|
177
|
+
return {
|
|
178
|
+
item,
|
|
179
|
+
score: filter(item)
|
|
180
|
+
};
|
|
181
|
+
})
|
|
182
|
+
.filter(({ score }) => score !== null)
|
|
183
|
+
.sort((a, b) => {
|
|
184
|
+
return a.score.score - b.score.score;
|
|
185
|
+
})
|
|
186
|
+
.map(({ item }) => item)
|
|
187
|
+
: props.runningItems;
|
|
188
|
+
return (React.createElement("ul", { className: LIST_CLASS }, items.map((item, i) => (React.createElement(Item, { child: props.child, key: i, runningItem: item, shutdownLabel: props.shutdownLabel, shutdownItemIcon: props.shutdownItemIcon, translator: props.translator, collapseToggled: props.collapseToggled })))));
|
|
189
|
+
}
|
|
190
|
+
class FilterWidget extends ReactWidget {
|
|
191
|
+
constructor(translator) {
|
|
192
|
+
super();
|
|
193
|
+
this._filterFn = (_) => {
|
|
194
|
+
return { score: 0 };
|
|
195
|
+
};
|
|
196
|
+
this._filterChanged = new Signal(this);
|
|
197
|
+
this.filter = this.filter.bind(this);
|
|
198
|
+
this._updateFilter = this._updateFilter.bind(this);
|
|
199
|
+
this._trans = translator.load('jupyterlab');
|
|
200
|
+
this.addClass('jp-SearchableSessions-filter');
|
|
201
|
+
}
|
|
202
|
+
get filterChanged() {
|
|
203
|
+
return this._filterChanged;
|
|
204
|
+
}
|
|
205
|
+
render() {
|
|
206
|
+
return (React.createElement(FilterBox, { placeholder: this._trans.__('Search'), updateFilter: this._updateFilter, useFuzzyFilter: false, caseSensitive: false }));
|
|
207
|
+
}
|
|
208
|
+
filter(item) {
|
|
209
|
+
var _a;
|
|
210
|
+
const labels = [this._getTextContent(item.label())];
|
|
211
|
+
for (const child of (_a = item.children) !== null && _a !== void 0 ? _a : []) {
|
|
212
|
+
labels.push(this._getTextContent(child.label()));
|
|
213
|
+
}
|
|
214
|
+
return this._filterFn(labels.join(' '));
|
|
215
|
+
}
|
|
216
|
+
_getTextContent(node) {
|
|
217
|
+
if (typeof node === 'string') {
|
|
218
|
+
return node;
|
|
219
|
+
}
|
|
220
|
+
if (typeof node === 'number') {
|
|
221
|
+
return '' + node;
|
|
222
|
+
}
|
|
223
|
+
if (typeof node === 'boolean') {
|
|
224
|
+
return '' + node;
|
|
225
|
+
}
|
|
226
|
+
if (Array.isArray(node)) {
|
|
227
|
+
return node.map(n => this._getTextContent(n)).join(' ');
|
|
228
|
+
}
|
|
229
|
+
if (node && isValidElement(node)) {
|
|
230
|
+
return node.props.children
|
|
231
|
+
.map((n) => this._getTextContent(n))
|
|
232
|
+
.join(' ');
|
|
233
|
+
}
|
|
234
|
+
return '';
|
|
235
|
+
}
|
|
236
|
+
_updateFilter(filterFn) {
|
|
237
|
+
this._filterFn = filterFn;
|
|
238
|
+
this._filterChanged.emit();
|
|
239
|
+
}
|
|
131
240
|
}
|
|
132
241
|
class ListWidget extends ReactWidget {
|
|
133
242
|
constructor(_options) {
|
|
@@ -135,9 +244,12 @@ class ListWidget extends ReactWidget {
|
|
|
135
244
|
this._options = _options;
|
|
136
245
|
this._update = new Signal(this);
|
|
137
246
|
_options.manager.runningChanged.connect(this._emitUpdate, this);
|
|
247
|
+
if (_options.filterProvider) {
|
|
248
|
+
_options.filterProvider.filterChanged.connect(this._emitUpdate, this);
|
|
249
|
+
}
|
|
138
250
|
}
|
|
139
251
|
dispose() {
|
|
140
|
-
|
|
252
|
+
Signal.clearData(this);
|
|
141
253
|
super.dispose();
|
|
142
254
|
}
|
|
143
255
|
onBeforeShow(msg) {
|
|
@@ -148,7 +260,8 @@ class ListWidget extends ReactWidget {
|
|
|
148
260
|
const options = this._options;
|
|
149
261
|
let cached = true;
|
|
150
262
|
return (React.createElement(UseSignal, { signal: this._update }, () => {
|
|
151
|
-
|
|
263
|
+
var _a;
|
|
264
|
+
// Cache the running items for the initial load and request from
|
|
152
265
|
// the service every subsequent load.
|
|
153
266
|
if (cached) {
|
|
154
267
|
cached = false;
|
|
@@ -157,7 +270,7 @@ class ListWidget extends ReactWidget {
|
|
|
157
270
|
options.runningItems = options.manager.running();
|
|
158
271
|
}
|
|
159
272
|
return (React.createElement("div", { className: CONTAINER_CLASS },
|
|
160
|
-
React.createElement(List, { runningItems: options.runningItems, shutdownLabel: options.manager.shutdownLabel, shutdownAllLabel: options.shutdownAllLabel, shutdownItemIcon: options.manager.shutdownItemIcon, translator: options.translator })));
|
|
273
|
+
React.createElement(List, { runningItems: options.runningItems, shutdownLabel: options.manager.shutdownLabel, shutdownAllLabel: options.shutdownAllLabel, shutdownItemIcon: options.manager.shutdownItemIcon, filter: (_a = options.filterProvider) === null || _a === void 0 ? void 0 : _a.filter, translator: options.translator, collapseToggled: options.collapseToggled })));
|
|
161
274
|
}));
|
|
162
275
|
}
|
|
163
276
|
/**
|
|
@@ -198,16 +311,67 @@ class ListWidget extends ReactWidget {
|
|
|
198
311
|
class Section extends PanelWithToolbar {
|
|
199
312
|
constructor(options) {
|
|
200
313
|
super();
|
|
314
|
+
this._buttons = null;
|
|
315
|
+
this._listView = false;
|
|
316
|
+
this._collapseToggled = new Signal(this);
|
|
317
|
+
this._viewChanged = new Signal(this);
|
|
201
318
|
this._manager = options.manager;
|
|
319
|
+
this._filterProvider = options.filterProvider;
|
|
202
320
|
const translator = options.translator || nullTranslator;
|
|
203
|
-
|
|
204
|
-
const shutdownAllLabel = options.manager.shutdownAllLabel || trans.__('Shut Down All');
|
|
205
|
-
const shutdownTitle = `${shutdownAllLabel}?`;
|
|
206
|
-
const shutdownAllConfirmationText = options.manager.shutdownAllConfirmationText ||
|
|
207
|
-
`${shutdownAllLabel} ${options.manager.name}`;
|
|
321
|
+
this._trans = translator.load('jupyterlab');
|
|
208
322
|
this.addClass(SECTION_CLASS);
|
|
209
323
|
this.title.label = options.manager.name;
|
|
210
|
-
|
|
324
|
+
this._manager.runningChanged.connect(this._onListChanged, this);
|
|
325
|
+
if (options.filterProvider) {
|
|
326
|
+
options.filterProvider.filterChanged.connect(this._onListChanged, this);
|
|
327
|
+
}
|
|
328
|
+
this._updateEmptyClass();
|
|
329
|
+
let runningItems = options.manager.running();
|
|
330
|
+
if (options.showToolbar !== false) {
|
|
331
|
+
this._initializeToolbar(runningItems);
|
|
332
|
+
}
|
|
333
|
+
this.addWidget(new ListWidget({
|
|
334
|
+
runningItems,
|
|
335
|
+
shutdownAllLabel: this._shutdownAllLabel,
|
|
336
|
+
collapseToggled: this._collapseToggled,
|
|
337
|
+
...options
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Toggle between list and tree view.
|
|
342
|
+
*/
|
|
343
|
+
toggleListView(forceOn) {
|
|
344
|
+
const newState = typeof forceOn !== 'undefined' ? forceOn : !this._listView;
|
|
345
|
+
this._listView = newState;
|
|
346
|
+
if (this._buttons) {
|
|
347
|
+
const switchViewButton = this._buttons['switch-view'];
|
|
348
|
+
switchViewButton.pressed = newState;
|
|
349
|
+
}
|
|
350
|
+
this._collapseToggled.emit(false);
|
|
351
|
+
this.toggleClass(LIST_VIEW_CLASS, newState);
|
|
352
|
+
this._updateButtons();
|
|
353
|
+
this._viewChanged.emit({ mode: newState ? 'list' : 'tree' });
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Dispose the resources held by the widget
|
|
357
|
+
*/
|
|
358
|
+
dispose() {
|
|
359
|
+
if (this.isDisposed) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
Signal.clearData(this);
|
|
363
|
+
super.dispose();
|
|
364
|
+
}
|
|
365
|
+
get _shutdownAllLabel() {
|
|
366
|
+
return this._manager.shutdownAllLabel || this._trans.__('Shut Down All');
|
|
367
|
+
}
|
|
368
|
+
_initializeToolbar(runningItems) {
|
|
369
|
+
const enabled = runningItems.length > 0;
|
|
370
|
+
const shutdownAllLabel = this._shutdownAllLabel;
|
|
371
|
+
const shutdownTitle = `${shutdownAllLabel}?`;
|
|
372
|
+
const shutdownAllConfirmationText = this._manager.shutdownAllConfirmationText ||
|
|
373
|
+
`${shutdownAllLabel} ${this._manager.name}`;
|
|
374
|
+
const onShutdown = () => {
|
|
211
375
|
void showDialog({
|
|
212
376
|
title: shutdownTitle,
|
|
213
377
|
body: shutdownAllConfirmationText,
|
|
@@ -217,43 +381,87 @@ class Section extends PanelWithToolbar {
|
|
|
217
381
|
]
|
|
218
382
|
}).then(result => {
|
|
219
383
|
if (result.button.accept) {
|
|
220
|
-
|
|
384
|
+
this._manager.shutdownAll();
|
|
221
385
|
}
|
|
222
386
|
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const enabled = runningItems.length > 0;
|
|
226
|
-
this._button = new ToolbarButton({
|
|
387
|
+
};
|
|
388
|
+
const shutdownAllButton = new ToolbarButton({
|
|
227
389
|
label: shutdownAllLabel,
|
|
228
390
|
className: `${SHUTDOWN_ALL_BUTTON_CLASS}${!enabled ? ' jp-mod-disabled' : ''}`,
|
|
229
391
|
enabled,
|
|
230
|
-
onClick: onShutdown
|
|
392
|
+
onClick: onShutdown.bind(this)
|
|
231
393
|
});
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
394
|
+
const switchViewButton = new ToolbarButton({
|
|
395
|
+
className: VIEW_BUTTON_CLASS,
|
|
396
|
+
enabled,
|
|
397
|
+
icon: tableRowsIcon,
|
|
398
|
+
pressedIcon: treeViewIcon,
|
|
399
|
+
onClick: () => this.toggleListView(),
|
|
400
|
+
tooltip: this._trans.__('Switch to List View'),
|
|
401
|
+
pressedTooltip: this._trans.__('Switch to Tree View')
|
|
402
|
+
});
|
|
403
|
+
const collapseExpandAllButton = new ToolbarButton({
|
|
404
|
+
className: COLLAPSE_EXPAND_BUTTON_CLASS,
|
|
405
|
+
enabled,
|
|
406
|
+
icon: collapseAllIcon,
|
|
407
|
+
pressedIcon: expandAllIcon,
|
|
408
|
+
onClick: () => {
|
|
409
|
+
const newState = !collapseExpandAllButton.pressed;
|
|
410
|
+
this._collapseToggled.emit(newState);
|
|
411
|
+
collapseExpandAllButton.pressed = newState;
|
|
412
|
+
},
|
|
413
|
+
tooltip: this._trans.__('Collapse All'),
|
|
414
|
+
pressedTooltip: this._trans.__('Expand All')
|
|
415
|
+
});
|
|
416
|
+
this._buttons = {
|
|
417
|
+
'switch-view': switchViewButton,
|
|
418
|
+
'collapse-expand': collapseExpandAllButton,
|
|
419
|
+
'shutdown-all': shutdownAllButton
|
|
420
|
+
};
|
|
421
|
+
// Update buttons once defined and before adding to DOM
|
|
422
|
+
this._updateButtons();
|
|
423
|
+
this._manager.runningChanged.connect(this._updateButtons, this);
|
|
424
|
+
for (const name of ['collapse-expand', 'switch-view', 'shutdown-all']) {
|
|
425
|
+
this.toolbar.addItem(name, this._buttons[name]);
|
|
242
426
|
}
|
|
243
|
-
this.
|
|
244
|
-
|
|
427
|
+
this.toolbar.addClass('jp-RunningSessions-toolbar');
|
|
428
|
+
}
|
|
429
|
+
_onListChanged() {
|
|
430
|
+
this._updateButtons();
|
|
431
|
+
this._updateEmptyClass();
|
|
245
432
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
433
|
+
_updateEmptyClass() {
|
|
434
|
+
if (this._filterProvider) {
|
|
435
|
+
const items = this._manager.running().filter(this._filterProvider.filter);
|
|
436
|
+
const empty = items.length === 0;
|
|
437
|
+
if (empty) {
|
|
438
|
+
this.node.classList.toggle('jp-mod-empty', true);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
this.node.classList.toggle('jp-mod-empty', false);
|
|
442
|
+
}
|
|
253
443
|
}
|
|
254
|
-
|
|
255
|
-
|
|
444
|
+
}
|
|
445
|
+
get viewChanged() {
|
|
446
|
+
return this._viewChanged;
|
|
447
|
+
}
|
|
448
|
+
_updateButtons() {
|
|
449
|
+
if (!this._buttons) {
|
|
450
|
+
return;
|
|
256
451
|
}
|
|
452
|
+
let runningItems = this._manager.running();
|
|
453
|
+
const enabled = runningItems.length > 0;
|
|
454
|
+
const hasNesting = runningItems.filter(item => item.children).length !== 0;
|
|
455
|
+
const inTreeView = hasNesting && !this._buttons['switch-view'].pressed;
|
|
456
|
+
this._buttons['switch-view'].node.style.display = hasNesting
|
|
457
|
+
? 'block'
|
|
458
|
+
: 'none';
|
|
459
|
+
this._buttons['collapse-expand'].node.style.display = inTreeView
|
|
460
|
+
? 'block'
|
|
461
|
+
: 'none';
|
|
462
|
+
this._buttons['collapse-expand'].enabled = enabled;
|
|
463
|
+
this._buttons['switch-view'].enabled = enabled;
|
|
464
|
+
this._buttons['shutdown-all'].enabled = enabled;
|
|
257
465
|
}
|
|
258
466
|
}
|
|
259
467
|
/**
|
|
@@ -263,9 +471,10 @@ export class RunningSessions extends SidePanel {
|
|
|
263
471
|
/**
|
|
264
472
|
* Construct a new running widget.
|
|
265
473
|
*/
|
|
266
|
-
constructor(managers, translator) {
|
|
474
|
+
constructor(managers, translator, stateDB) {
|
|
267
475
|
super();
|
|
268
476
|
this.managers = managers;
|
|
477
|
+
this._stateDB = stateDB !== null && stateDB !== void 0 ? stateDB : null;
|
|
269
478
|
this.translator = translator !== null && translator !== void 0 ? translator : nullTranslator;
|
|
270
479
|
const trans = this.translator.load('jupyterlab');
|
|
271
480
|
this.addClass(RUNNING_CLASS);
|
|
@@ -287,6 +496,225 @@ export class RunningSessions extends SidePanel {
|
|
|
287
496
|
this.managers.added.disconnect(this.addSection, this);
|
|
288
497
|
super.dispose();
|
|
289
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* Add a section for a new manager.
|
|
501
|
+
*
|
|
502
|
+
* @param managers Managers
|
|
503
|
+
* @param manager New manager
|
|
504
|
+
*/
|
|
505
|
+
async addSection(_, manager) {
|
|
506
|
+
const section = new Section({ manager, translator: this.translator });
|
|
507
|
+
this.addWidget(section);
|
|
508
|
+
const state = await this._getState();
|
|
509
|
+
const sectionsInListView = state.listViewSections;
|
|
510
|
+
const sectionId = manager.name;
|
|
511
|
+
if (sectionsInListView && sectionsInListView.includes(sectionId)) {
|
|
512
|
+
section.toggleListView(true);
|
|
513
|
+
}
|
|
514
|
+
section.viewChanged.connect(async (_emitter, viewState) => {
|
|
515
|
+
await this._updateState(sectionId, viewState.mode);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Update state database with the new state of a given section.
|
|
520
|
+
*/
|
|
521
|
+
async _updateState(sectionId, mode) {
|
|
522
|
+
var _a;
|
|
523
|
+
const state = await this._getState();
|
|
524
|
+
let listViewSections = (_a = state.listViewSections) !== null && _a !== void 0 ? _a : [];
|
|
525
|
+
if (mode === 'list' && !listViewSections.includes(sectionId)) {
|
|
526
|
+
listViewSections.push(sectionId);
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
listViewSections = listViewSections.filter(e => e !== sectionId);
|
|
530
|
+
}
|
|
531
|
+
const newState = { listViewSections };
|
|
532
|
+
if (this._stateDB) {
|
|
533
|
+
await this._stateDB.save(STATE_DB_ID, newState);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Get current state from the state database.
|
|
538
|
+
*/
|
|
539
|
+
async _getState() {
|
|
540
|
+
var _a;
|
|
541
|
+
if (!this._stateDB) {
|
|
542
|
+
return {};
|
|
543
|
+
}
|
|
544
|
+
return ((_a = (await this._stateDB.fetch(STATE_DB_ID))) !== null && _a !== void 0 ? _a : {});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Section but rendering its own title before the content
|
|
549
|
+
*/
|
|
550
|
+
class TitledSection extends Section {
|
|
551
|
+
constructor(options) {
|
|
552
|
+
super(options);
|
|
553
|
+
const titleNode = document.createElement('h3');
|
|
554
|
+
titleNode.className = 'jp-SearchableSessions-title';
|
|
555
|
+
const label = titleNode.appendChild(document.createElement('span'));
|
|
556
|
+
label.className = 'jp-SearchableSessions-titleLabel';
|
|
557
|
+
label.textContent = this.title.label;
|
|
558
|
+
this.node.insertAdjacentElement('afterbegin', titleNode);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
class EmptyIndicator extends Widget {
|
|
562
|
+
constructor(translator) {
|
|
563
|
+
super();
|
|
564
|
+
const trans = translator.load('jupyterlab');
|
|
565
|
+
this.addClass('jp-SearchableSessions-emptyIndicator');
|
|
566
|
+
this.node.textContent = trans.__('No matches');
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* A panel intended for use within `Dialog` to allow searching tabs and running sessions.
|
|
571
|
+
*/
|
|
572
|
+
export class SearchableSessions extends Panel {
|
|
573
|
+
constructor(managers, translator) {
|
|
574
|
+
super();
|
|
575
|
+
this._activeIndex = 0;
|
|
576
|
+
this._translator = translator !== null && translator !== void 0 ? translator : nullTranslator;
|
|
577
|
+
this.addClass(RUNNING_CLASS);
|
|
578
|
+
this.addClass(SEARCHABLE_CLASS);
|
|
579
|
+
this._filterWidget = new FilterWidget(this._translator);
|
|
580
|
+
this.addWidget(this._filterWidget);
|
|
581
|
+
this._list = new SearchableSessionsList(managers, this._filterWidget, translator);
|
|
582
|
+
this.addWidget(this._list);
|
|
583
|
+
this._filterWidget.filterChanged.connect(() => {
|
|
584
|
+
this._activeIndex = 0;
|
|
585
|
+
this._updateActive(0);
|
|
586
|
+
}, this);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Dispose the resources held by the widget
|
|
590
|
+
*/
|
|
591
|
+
dispose() {
|
|
592
|
+
if (this.isDisposed) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
Signal.clearData(this);
|
|
596
|
+
super.dispose();
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Click active element when the user confirmed the choice in the dialog.
|
|
600
|
+
*/
|
|
601
|
+
getValue() {
|
|
602
|
+
const items = [
|
|
603
|
+
...this.node.querySelectorAll('.' + ITEM_LABEL_CLASS)
|
|
604
|
+
];
|
|
605
|
+
const pos = Math.min(Math.max(this._activeIndex, 0), items.length - 1);
|
|
606
|
+
items[pos].click();
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Handle incoming events.
|
|
610
|
+
*/
|
|
611
|
+
handleEvent(event) {
|
|
612
|
+
switch (event.type) {
|
|
613
|
+
case 'keydown':
|
|
614
|
+
this._evtKeydown(event);
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* A message handler invoked on an `'after-attach'` message.
|
|
620
|
+
*/
|
|
621
|
+
onAfterAttach(_) {
|
|
622
|
+
this._forceFocusInput();
|
|
623
|
+
this.node.addEventListener('keydown', this);
|
|
624
|
+
setTimeout(() => {
|
|
625
|
+
this._updateActive(0);
|
|
626
|
+
}, 0);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* A message handler invoked on an `'after-detach'` message.
|
|
630
|
+
*/
|
|
631
|
+
onAfterDetach(_) {
|
|
632
|
+
this.node.removeEventListener('keydown', this);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Force focus on the filter input.
|
|
636
|
+
*
|
|
637
|
+
* Note: forces focus because this widget is intended to be used in `Dialog`,
|
|
638
|
+
* which does not support focusing React widget nested within a non-React
|
|
639
|
+
* widget (a limitation of `focusNodeSelector` option implementation).
|
|
640
|
+
*/
|
|
641
|
+
_forceFocusInput() {
|
|
642
|
+
var _a;
|
|
643
|
+
(_a = this._filterWidget.renderPromise) === null || _a === void 0 ? void 0 : _a.then(() => {
|
|
644
|
+
var _a;
|
|
645
|
+
(_a = this._filterWidget.node.querySelector('input')) === null || _a === void 0 ? void 0 : _a.focus();
|
|
646
|
+
}).catch(console.warn);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Navigate between items using up/down keys by shifting focus.
|
|
650
|
+
*/
|
|
651
|
+
_evtKeydown(event) {
|
|
652
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
653
|
+
const direction = event.key === 'ArrowDown' ? +1 : -1;
|
|
654
|
+
const wasSet = this._updateActive(direction);
|
|
655
|
+
if (wasSet) {
|
|
656
|
+
event.preventDefault();
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Set and mark active item relative to the current.
|
|
662
|
+
*
|
|
663
|
+
* Returns whether an active item was set.
|
|
664
|
+
*/
|
|
665
|
+
_updateActive(direction) {
|
|
666
|
+
const items = [...this.node.querySelectorAll('.' + ITEM_CLASS)].filter(e => e.checkVisibility());
|
|
667
|
+
if (!items.length) {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
for (const item of items) {
|
|
671
|
+
if (item.classList.contains('jp-mod-active')) {
|
|
672
|
+
item.classList.toggle('jp-mod-active', false);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const currentIndex = this._activeIndex;
|
|
676
|
+
let newIndex = null;
|
|
677
|
+
if (currentIndex === -1) {
|
|
678
|
+
// First or last
|
|
679
|
+
newIndex = direction === +1 ? 0 : items.length - 1;
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
newIndex = Math.min(Math.max(currentIndex + direction, 0), items.length - 1);
|
|
683
|
+
}
|
|
684
|
+
if (newIndex !== null) {
|
|
685
|
+
items[newIndex].classList.add('jp-mod-active');
|
|
686
|
+
ElementExt.scrollIntoViewIfNeeded(this._list.node, items[newIndex]);
|
|
687
|
+
this._activeIndex = newIndex;
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* A panel list of searchable sessions.
|
|
695
|
+
*/
|
|
696
|
+
export class SearchableSessionsList extends Panel {
|
|
697
|
+
constructor(managers, filterWidget, translator) {
|
|
698
|
+
super();
|
|
699
|
+
this._managers = managers;
|
|
700
|
+
this._translator = translator !== null && translator !== void 0 ? translator : nullTranslator;
|
|
701
|
+
this._filterWidget = filterWidget;
|
|
702
|
+
this.addClass('jp-SearchableSessions-list');
|
|
703
|
+
this._emptyIndicator = new EmptyIndicator(this._translator);
|
|
704
|
+
this.addWidget(this._emptyIndicator);
|
|
705
|
+
managers.items().forEach(manager => this.addSection(managers, manager));
|
|
706
|
+
managers.added.connect(this.addSection, this);
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Dispose the resources held by the widget
|
|
710
|
+
*/
|
|
711
|
+
dispose() {
|
|
712
|
+
if (this.isDisposed) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
this._managers.added.disconnect(this.addSection, this);
|
|
716
|
+
super.dispose();
|
|
717
|
+
}
|
|
290
718
|
/**
|
|
291
719
|
* Add a section for a new manager.
|
|
292
720
|
*
|
|
@@ -294,7 +722,17 @@ export class RunningSessions extends SidePanel {
|
|
|
294
722
|
* @param manager New manager
|
|
295
723
|
*/
|
|
296
724
|
addSection(_, manager) {
|
|
297
|
-
|
|
725
|
+
const section = new TitledSection({
|
|
726
|
+
manager,
|
|
727
|
+
translator: this._translator,
|
|
728
|
+
showToolbar: false,
|
|
729
|
+
filterProvider: this._filterWidget
|
|
730
|
+
});
|
|
731
|
+
// Do not use tree view in searchable list
|
|
732
|
+
section.toggleListView(true);
|
|
733
|
+
this.addWidget(section);
|
|
734
|
+
// Move empty indicator to the end
|
|
735
|
+
this.addWidget(this._emptyIndicator);
|
|
298
736
|
}
|
|
299
737
|
}
|
|
300
738
|
//# sourceMappingURL=index.js.map
|